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

@@ -25,23 +25,28 @@ namespace PrivaPub.Federation.Signing
readonly IRemoteActorService _remoteActors;
readonly DbEntities _dbEntities;
public SignedFetchAuthorizer(IRemoteActorService remoteActors, DbEntities dbEntities)
readonly ILocalActorService _localActors;
public SignedFetchAuthorizer(IRemoteActorService remoteActors, DbEntities dbEntities, ILocalActorService localActors = default)
{
_remoteActors = remoteActors;
_dbEntities = dbEntities;
_localActors = localActors;
}
// the actor whose signature (draft-cavage, or RFC 9421 when there is a base address to check its target against)
// verifies on the request
public async Task<ForeignAvatar> Requester(HttpRequest request, CancellationToken token)
{
var parameters = HttpSignatures.Parse(request.Headers["Signature"].ToString());
if (parameters == default || HttpSignatures.CheckRequest(request, parameters, body: default) != default)
var signature = RequestSignature.Of(request);
if (signature == default || signature.Problem(request, body: default) != default)
return default;
var signingString = HttpSignatures.SigningString(request, parameters);
var actor = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: false, token);
if (actor != default && HttpSignatures.Verify(actor.PublicKey, signingString, parameters.Signature))
var signed = signature.Signed(request, _localActors?.BaseAddress ?? $"{request.Scheme}://{request.Host}");
var actor = await _remoteActors.GetActorByKeyId(signature.KeyId, refresh: false, token);
if (actor != default && signature.VerifiedBy(actor.PublicKey, signed))
return actor;
actor = await _remoteActors.GetActorByKeyId(parameters.KeyId, refresh: true, token);
return actor != default && HttpSignatures.Verify(actor.PublicKey, signingString, parameters.Signature) ? actor : default;
actor = await _remoteActors.GetActorByKeyId(signature.KeyId, refresh: true, token);
return actor != default && signature.VerifiedBy(actor.PublicKey, signed) ? actor : default;
}
public async Task<bool> MayRead(PostEntity post, ForeignAvatar requester, CancellationToken token)