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
46 lines
2.2 KiB
C#
46 lines
2.2 KiB
C#
namespace PrivaPub.Federation.Signing
|
|
{
|
|
// What signed a request: an RFC 9421 message signature when it carries Signature-Input, else a draft-cavage Signature;
|
|
// the inbox and signed fetches check either the same way.
|
|
public sealed class RequestSignature
|
|
{
|
|
readonly SignatureParameters _cavage;
|
|
readonly MessageSignature _message;
|
|
|
|
RequestSignature(SignatureParameters cavage, MessageSignature message)
|
|
{
|
|
_cavage = cavage;
|
|
_message = message;
|
|
}
|
|
|
|
public string KeyId => _message?.KeyId ?? _cavage.KeyId;
|
|
public string Algorithm => _message != default ? _message.Algorithm ?? "rsa-v1_5-sha256" : _cavage.Algorithm;
|
|
// how the ledger names it: "cavage:rsa-sha256", "rfc9421:rsa-v1_5-sha256"
|
|
public string Scheme => _message != default ? "rfc9421:" + Algorithm : "cavage:" + Algorithm;
|
|
public string[] Covered => _message?.Components ?? _cavage.Headers;
|
|
|
|
// the request's signature, or null when it has none it can read
|
|
public static RequestSignature Of(HttpRequest request)
|
|
{
|
|
if (MessageSignatures.Present(request))
|
|
{
|
|
var message = MessageSignatures.Parse(request.Headers["Signature-Input"].ToString(), request.Headers["Signature"].ToString());
|
|
return message == default ? default : new RequestSignature(default, message);
|
|
}
|
|
var cavage = HttpSignatures.Parse(request.Headers["Signature"].ToString());
|
|
return cavage == default ? default : new RequestSignature(cavage, default);
|
|
}
|
|
|
|
// what is wrong with the request as signed (unsigned target or digest, a stale date...), or null
|
|
public string Problem(HttpRequest request, byte[] body) =>
|
|
_message != default ? MessageSignatures.CheckRequest(request, _message, body) : HttpSignatures.CheckRequest(request, _cavage, body);
|
|
|
|
// the string the signature is over: the signing string, or RFC 9421's signature base against our public address
|
|
public string Signed(HttpRequest request, string baseAddress) =>
|
|
_message != default ? MessageSignatures.Base(request, _message, baseAddress) : HttpSignatures.SigningString(request, _cavage);
|
|
|
|
public bool VerifiedBy(string publicKeyPem, string signed) =>
|
|
_message != default ? MessageSignatures.Verify(publicKeyPem, _message, signed) : HttpSignatures.Verify(publicKeyPem, signed, _cavage.Signature);
|
|
}
|
|
}
|