RFC 9421 signatures are verified, not refused
WordPress's ActivityPub plugin (and Ghost and Fedify) sign with RFC 9421 first and fall back to draft-cavage only after a refusal, so each first delivery cost two requests and a 401 in our statistics. Now a request carrying Signature-Input is verified as an HTTP message signature: its covered components (the method and our own public target, the body's Content-Digest), its created and expires, with the actor's RSA key under PKCS#1 v1.5 or PSS. Deliveries and signed fetches both take it; the ledger names the scheme (rfc9421:rsa-v1_5-sha256). What PrivaPub sends stays draft-cavage, which every server reads. Ed25519 waits for FEP-521a keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
26dac40720
commit
c5a69d240a
10 files changed
+423
-30
No files matched your search
@@ -66,7 +66,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
public InboxResult NoSuchRecipient(HttpRequest request)
|
||||
{
|
||||
var result = new InboxResult(StatusCodes.Status404NotFound, "no such local actor", Reason: "unknown-recipient");
|
||||
var keyId = HttpSignatures.Parse(request.Headers["Signature"].ToString())?.KeyId;
|
||||
var keyId = RequestSignature.Of(request)?.KeyId;
|
||||
Record(new Receipt { ClaimedHost = HostOf(keyId), Inbox = "personal", Bytes = request.ContentLength }, result, 0);
|
||||
return result;
|
||||
}
|
||||
@@ -141,32 +141,33 @@ namespace PrivaPub.Federation.Inbox
|
||||
if (string.IsNullOrEmpty(type) || string.IsNullOrEmpty(actorUri))
|
||||
return new(StatusCodes.Status400BadRequest, "type and actor are required", Reason: "missing-type-or-actor");
|
||||
|
||||
var parameters = HttpSignatures.Parse(request.Headers["Signature"].ToString());
|
||||
if (parameters == default)
|
||||
// draft-cavage's Signature, or RFC 9421's Signature-Input and Signature
|
||||
var signature = RequestSignature.Of(request);
|
||||
if (signature == default)
|
||||
return new(StatusCodes.Status401Unauthorized, "missing or unreadable Signature header", Reason: "no-signature");
|
||||
receipt.Signature = "cavage:" + parameters.Algorithm;
|
||||
receipt.ClaimedHost ??= HostOf(parameters.KeyId);
|
||||
if (_domainBlocks.IsSuspended(HostOf(parameters.KeyId)) || _domainBlocks.IsSuspended(HostOf(actorUri)))
|
||||
receipt.Signature = signature.Scheme;
|
||||
receipt.ClaimedHost ??= HostOf(signature.KeyId);
|
||||
if (_domainBlocks.IsSuspended(HostOf(signature.KeyId)) || _domainBlocks.IsSuspended(HostOf(actorUri)))
|
||||
{
|
||||
receipt.Blocked = true;
|
||||
receipt.ClaimedHost = _domainBlocks.IsSuspended(HostOf(actorUri)) ? HostOf(actorUri) : HostOf(parameters.KeyId);
|
||||
receipt.ClaimedHost = _domainBlocks.IsSuspended(HostOf(actorUri)) ? HostOf(actorUri) : HostOf(signature.KeyId);
|
||||
return new(StatusCodes.Status202Accepted, Reason: "suspended");
|
||||
}
|
||||
|
||||
var requestProblem = HttpSignatures.CheckRequest(request, parameters, body);
|
||||
var requestProblem = signature.Problem(request, body);
|
||||
if (requestProblem != default)
|
||||
return new(StatusCodes.Status401Unauthorized, requestProblem, Reason: HttpSignatures.ProblemCode(requestProblem));
|
||||
|
||||
var signingString = HttpSignatures.SigningString(request, parameters);
|
||||
var keyOwner = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: false, token);
|
||||
var signed = signature.Signed(request, _localActors.BaseAddress);
|
||||
var keyOwner = await _remoteActors.GetActorByKeyId(signature.KeyId, refresh: false, token);
|
||||
if (keyOwner == default && type == "Delete" && Id(activity["object"]) == actorUri)
|
||||
return new(StatusCodes.Status202Accepted, Reason: "self-delete-unknown-key");
|
||||
if (keyOwner == default || !HttpSignatures.Verify(keyOwner.PublicKey, signingString, parameters.Signature))
|
||||
if (keyOwner == default || !signature.VerifiedBy(keyOwner.PublicKey, signed))
|
||||
{
|
||||
keyOwner = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: true, token);
|
||||
if (keyOwner == default && _remoteActors.KeyTemporarilyUnavailable(parameters.KeyId))
|
||||
keyOwner = await _remoteActors.GetActorByKeyId(signature.KeyId, refresh: true, token);
|
||||
if (keyOwner == default && _remoteActors.KeyTemporarilyUnavailable(signature.KeyId))
|
||||
return new(StatusCodes.Status503ServiceUnavailable, "the signing key could not be fetched; try again later", KeyRetrySeconds, "key-unavailable");
|
||||
if (keyOwner == default || !HttpSignatures.Verify(keyOwner.PublicKey, signingString, parameters.Signature))
|
||||
if (keyOwner == default || !signature.VerifiedBy(keyOwner.PublicKey, signed))
|
||||
return new(StatusCodes.Status401Unauthorized, "the signature does not verify", Reason: "signature-invalid");
|
||||
}
|
||||
|
||||
@@ -179,8 +180,8 @@ namespace PrivaPub.Federation.Inbox
|
||||
return shapeProblem;
|
||||
|
||||
var activityId = Id(activity);
|
||||
var payload = new InboxPayload(actorUri, activity.ToJsonString(), recipient == default ? "shared" : "personal", parameters.KeyId,
|
||||
parameters.Algorithm, parameters.Headers, DateTime.UtcNow);
|
||||
var payload = new InboxPayload(actorUri, activity.ToJsonString(), recipient == default ? "shared" : "personal", signature.KeyId,
|
||||
signature.Algorithm, signature.Covered, DateTime.UtcNow);
|
||||
var queued = await _queue.Enqueue(JobKind.ProcessInbox, JsonSerializer.Serialize(payload),
|
||||
new Uri(actorUri).Host.ToLowerInvariant(), activityId == default ? default : "inbox|" + activityId, token);
|
||||
_logger.LogInformation("Inbox {Recipient}: {Type} from {Actor} queued", recipient?.Handle ?? "shared", type, actorUri);
|
||||
|
||||
Reference in new issue
Block a user