A remote actor is believed only from its own origin
S1 and S2 of the roadmap. RemoteActorService: - FetchObject accepts a document only when its id is the address it was served from; a same-origin document naming another address is asked for at that address once (how GoToSocial serves its key URIs), anything else is dropped; - GetActorByKeyId accepts a key only when the actor lists it, its owner is the actor and it lives on the actor's origin, whether the keyId points at the actor or at a key document; - a refetch for a key or an actor happens at most once per five minutes, so a bad signature cannot make us hammer a host; - the cache row is written by one atomic upsert on ActorURI; - every fetch is signed by the instance actor, never by the persona that happened to receive the activity. The inbox refuses an activity whose id is not on its actor's origin, and an Undo of someone else's activity; a Create's object, an Update and a Delete must be on the actor's origin too, and a cross-origin object is refetched from its own origin before it is trusted. Tests: a fake peer on two origins serves forged actors, foreign-owned keys, cross-origin key documents, aliases and a GoToSocial-style key address (integration, PRIVAPUB_TEST_MONGOD=1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
ccc3597699
commit
a060204dd6
9 files changed
+593
-81
No files matched your search
@@ -14,6 +14,7 @@ using PostEntity = PrivaPub.Models.Post.Post;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Signing;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Federation.Outbox;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox
|
||||
@@ -81,14 +82,13 @@ namespace PrivaPub.Federation.Inbox
|
||||
if (requestProblem != default)
|
||||
return new(StatusCodes.Status401Unauthorized, requestProblem);
|
||||
|
||||
var signAs = recipient ?? await _localActors.GetInstanceActor(token);
|
||||
var signingString = HttpSignatures.SigningString(request, parameters);
|
||||
var keyOwner = await _remoteActors.GetActorByKeyId(parameters.KeyId, signAs, refresh: false, token);
|
||||
var keyOwner = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: false, token);
|
||||
if (keyOwner == default && type == "Delete" && Id(activity["object"]) == actorUri)
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
if (keyOwner == default || !HttpSignatures.Verify(keyOwner.PublicKey, signingString, parameters.Signature))
|
||||
{
|
||||
keyOwner = await _remoteActors.GetActorByKeyId(parameters.KeyId, signAs, refresh: true, token);
|
||||
keyOwner = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: true, token);
|
||||
if (keyOwner == default || !HttpSignatures.Verify(keyOwner.PublicKey, signingString, parameters.Signature))
|
||||
return new(StatusCodes.Status401Unauthorized, "the signature does not verify");
|
||||
}
|
||||
@@ -96,15 +96,19 @@ namespace PrivaPub.Federation.Inbox
|
||||
if (!string.Equals(keyOwner.ActorURI, actorUri, StringComparison.Ordinal))
|
||||
return new(StatusCodes.Status401Unauthorized, "the activity's actor is not the key's owner");
|
||||
|
||||
var activityId = Id(activity);
|
||||
if (activityId != default && !Origin.Same(activityId, actorUri))
|
||||
return new(StatusCodes.Status400BadRequest, "the activity's id is not on its actor's origin");
|
||||
|
||||
_logger.LogInformation("Inbox {Recipient}: {Type} from {Actor}", recipient?.Handle ?? "shared", type, actorUri);
|
||||
|
||||
return type switch
|
||||
{
|
||||
"Follow" => await Follow(activity, keyOwner, token),
|
||||
"Undo" => await Undo(activity, keyOwner, token),
|
||||
"Create" => await Create(activity, keyOwner, signAs, token),
|
||||
"Create" => await Create(activity, keyOwner, token),
|
||||
"Delete" => await Delete(activity, keyOwner, token),
|
||||
"Update" => await Update(activity, keyOwner, signAs, token),
|
||||
"Update" => await Update(activity, keyOwner, token),
|
||||
_ => new(StatusCodes.Status202Accepted)
|
||||
};
|
||||
}
|
||||
@@ -147,6 +151,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
var innerType = inner is JsonObject ? Value(inner, "type") : default;
|
||||
var innerId = Id(inner);
|
||||
|
||||
if (inner is JsonObject && Id(inner["actor"]) != actor.ActorURI)
|
||||
return new(StatusCodes.Status400BadRequest, "an actor can only undo its own activities");
|
||||
if (innerType is not (null or "Follow"))
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
|
||||
@@ -167,13 +173,13 @@ namespace PrivaPub.Federation.Inbox
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
}
|
||||
|
||||
async Task<InboxResult> Create(JsonNode create, ForeignAvatar author, LocalActor signAs, CancellationToken token)
|
||||
async Task<InboxResult> Create(JsonNode create, ForeignAvatar author, CancellationToken token)
|
||||
{
|
||||
var note = create["object"];
|
||||
if (note is JsonValue)
|
||||
if (note is not JsonObject || !Origin.Same(Id(note), author.ActorURI))
|
||||
{
|
||||
using var fetched = await _remoteActors.Fetch(Id(note), signAs, token);
|
||||
note = fetched == default ? default : JsonNode.Parse(fetched.RootElement.GetRawText());
|
||||
using var fetched = await _remoteActors.FetchObject(Id(note), token);
|
||||
note = fetched == default ? default : JsonNode.Parse(fetched.Root.GetRawText());
|
||||
}
|
||||
if (note is not JsonObject || Value(note, "type") is not ("Note" or "Article" or "Page" or "Question"))
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
@@ -255,7 +261,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
async Task<InboxResult> Delete(JsonNode delete, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var objectUri = Id(delete["object"]);
|
||||
if (string.IsNullOrEmpty(objectUri))
|
||||
if (!Origin.Same(objectUri, actor.ActorURI))
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
|
||||
if (objectUri == actor.ActorURI)
|
||||
@@ -278,15 +284,15 @@ namespace PrivaPub.Federation.Inbox
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
}
|
||||
|
||||
async Task<InboxResult> Update(JsonNode update, ForeignAvatar actor, LocalActor signAs, CancellationToken token)
|
||||
async Task<InboxResult> Update(JsonNode update, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var inner = update["object"];
|
||||
if (Id(inner) == actor.ActorURI)
|
||||
{
|
||||
await _remoteActors.GetActor(actor.ActorURI, signAs, refresh: true, token);
|
||||
await _remoteActors.GetActor(actor.ActorURI, refresh: true, token);
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
}
|
||||
if (inner is not JsonObject || Id(inner["attributedTo"]) != actor.ActorURI)
|
||||
if (inner is not JsonObject || Id(inner["attributedTo"]) != actor.ActorURI || !Origin.Same(Id(inner), actor.ActorURI))
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
|
||||
var objectUri = Id(inner);
|
||||
|
||||
Reference in new issue
Block a user