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:
thepraandClaude Opus 5.5 committed 2026-10-05 06:48:22 +02:00
1 parent 26dac40720
commit c5a69d240a
10 files changed
+423 -30

No files matched your search

+17 -16
View File
@@ -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);