diff --git a/FEDERATION.md b/FEDERATION.md index 75f2bd4..62afea7 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -8,6 +8,7 @@ PrivaPub is an ActivityPub server written in C#. This document follows - [ActivityPub](https://www.w3.org/TR/activitypub/) (server-to-server) - [WebFinger](https://webfinger.net/) - [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures), `rsa-sha256` / `hs2019` with RSA keys + (`hs2019` hashed with SHA-256, or SHA-512 as some sign it; a `SHA-256=` or `SHA-512=` digest) - [HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421) (RFC 9421) and Content-Digest (RFC 9530), verified on deliveries and signed fetches: `rsa-v1_5-sha256` and `rsa-pss-sha512` with RSA keys - [NodeInfo](https://nodeinfo.diaspora.software/) 2.0 and 2.1 diff --git a/PrivaPub.Tests/Federation/HttpSignaturesTests.cs b/PrivaPub.Tests/Federation/HttpSignaturesTests.cs index 55b769d..6579a91 100644 --- a/PrivaPub.Tests/Federation/HttpSignaturesTests.cs +++ b/PrivaPub.Tests/Federation/HttpSignaturesTests.cs @@ -101,6 +101,33 @@ namespace PrivaPub.Tests.Federation Assert.Equal("the signature has expired", Check(MastodonRequest(extra: $",expires=\"{Now.AddMinutes(-20).ToUnixTimeSeconds()}\""))); + // hs2019 leaves the hash to the key: some sign RSA with SHA-512, which rsa-sha256 does not allow; and a SHA-512 digest + [Fact] + public void Accepts_hs2019_hashed_with_sha512_and_a_sha512_digest() + { + var context = new DefaultHttpContext(); + context.Request.Method = "POST"; + context.Request.Host = new HostString(Host); + context.Request.Path = "/peasants/bob/mouth"; + context.Features.Get().RawTarget = "/peasants/bob/mouth"; + var date = Now.ToString("r", CultureInfo.InvariantCulture); + var digest = "SHA-512=" + Convert.ToBase64String(SHA512.HashData(Body)); + context.Request.Headers["Date"] = date; + context.Request.Headers["Digest"] = digest; + var signingString = $"(request-target): post /peasants/bob/mouth\nhost: {Host}\ndate: {date}\ndigest: {digest}"; + var signature = Convert.ToBase64String(_key.SignData(Encoding.UTF8.GetBytes(signingString), HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1)); + string Signed(string algorithm) => $"keyId=\"{KeyId}\",algorithm=\"{algorithm}\",headers=\"(request-target) host date digest\",signature=\"{signature}\""; + + context.Request.Headers["Signature"] = Signed("hs2019"); + var parameters = HttpSignatures.Parse(context.Request.Headers["Signature"].ToString()); + Assert.Null(HttpSignatures.CheckRequest(context.Request, parameters, Body, Now)); + Assert.True(HttpSignatures.Verify(PublicKey, HttpSignatures.SigningString(context.Request, parameters), parameters.Signature, parameters.Algorithm)); + + context.Request.Headers["Signature"] = Signed("rsa-sha256"); + parameters = HttpSignatures.Parse(context.Request.Headers["Signature"].ToString()); + Assert.False(HttpSignatures.Verify(PublicKey, HttpSignatures.SigningString(context.Request, parameters), parameters.Signature, parameters.Algorithm)); + } + [Fact] public void Signs_with_the_raw_request_target() { diff --git a/PrivaPub/Federation/Signing/HttpSignatures.cs b/PrivaPub/Federation/Signing/HttpSignatures.cs index 1442f02..95cca1f 100644 --- a/PrivaPub/Federation/Signing/HttpSignatures.cs +++ b/PrivaPub/Federation/Signing/HttpSignatures.cs @@ -152,9 +152,11 @@ namespace PrivaPub.Federation.Signing { if (!parameters.Headers.Contains("digest")) return "the digest is not signed"; - var expectedHash = Convert.ToBase64String(SHA256.HashData(body)); + var sha256 = Convert.ToBase64String(SHA256.HashData(body)); + var sha512 = Convert.ToBase64String(SHA512.HashData(body)); var matches = request.Headers["Digest"].ToString().Split(',').Select(d => d.Trim()) - .Any(d => d.StartsWith("SHA-256=", StringComparison.OrdinalIgnoreCase) && d[8..] == expectedHash); + .Any(d => d.StartsWith("SHA-256=", StringComparison.OrdinalIgnoreCase) && d[8..] == sha256 + || d.StartsWith("SHA-512=", StringComparison.OrdinalIgnoreCase) && d[8..] == sha512); if (!matches) return "the digest does not match the body"; } @@ -213,7 +215,8 @@ namespace PrivaPub.Federation.Signing return string.IsNullOrEmpty(raw) || raw[0] != '/' ? $"{request.PathBase}{request.Path}{request.QueryString}" : raw; } - public static bool Verify(string publicKeyPem, string signingString, byte[] signature) + // hs2019 leaves the hash to the key: RSA with SHA-256, as nearly everyone signs, else with SHA-512, as some do + public static bool Verify(string publicKeyPem, string signingString, byte[] signature, string algorithm = "rsa-sha256") { if (string.IsNullOrEmpty(publicKeyPem) || signingString == null) return false; @@ -221,8 +224,9 @@ namespace PrivaPub.Federation.Signing { using var rsa = RSA.Create(); rsa.ImportFromPem(publicKeyPem); - return rsa.VerifyData(Encoding.UTF8.GetBytes(signingString), signature, - HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var data = Encoding.UTF8.GetBytes(signingString); + return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) + || algorithm == "hs2019" && rsa.VerifyData(data, signature, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1); } catch (CryptographicException) { diff --git a/PrivaPub/Federation/Signing/RequestSignature.cs b/PrivaPub/Federation/Signing/RequestSignature.cs index 6612f31..a072110 100644 --- a/PrivaPub/Federation/Signing/RequestSignature.cs +++ b/PrivaPub/Federation/Signing/RequestSignature.cs @@ -40,6 +40,6 @@ namespace PrivaPub.Federation.Signing _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); + _message != default ? MessageSignatures.Verify(publicKeyPem, _message, signed) : HttpSignatures.Verify(publicKeyPem, signed, _cavage.Signature, _cavage.Algorithm); } } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 37d8752..81f23ad 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -692,7 +692,7 @@ it, raw where it doesn't. Mitra refuses a proof by a key it has not read and does not read the actor again; a forwarded activity with a proof its actor's key verifies is taken without reading it again; Mastodon accepts PrivaPub's, so Activity-Relay's forwards reach it); - - `hs2019` with SHA-512. + - `hs2019` with SHA-512: **done 2026-10-06** (and `SHA-512=` digests). - **Discovery:** - a relay client for both relay styles: **done 2026-10-05/06** (`Federation:Relays`; forwarded posts read again from their origin, announces unwrapped; personas' public posts sent to them, owner decision; checked live against