hs2019 signatures hashed with SHA-512, and SHA-512 digests
A draft-cavage hs2019 signature leaves the hash to the key: with an RSA key it is tried with SHA-256, as nearly everyone signs, then with SHA-512, as some do; rsa-sha256 stays SHA-256 only. A Digest of SHA-512= is checked as SHA-256= is. 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
0310bbe0f9
commit
e708f1ad2e
5 files changed
+39
-7
No files matched your search
@@ -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
|
||||
|
||||
@@ -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<IHttpRequestFeature>().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()
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user