Refactor http signature validation

This commit is contained in:
Laura Hausmann 2024-01-23 02:59:00 +01:00
parent 3fba5550d5
commit 5f088ba66f
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
2 changed files with 25 additions and 21 deletions

View file

@ -7,39 +7,44 @@ using Microsoft.Extensions.Primitives;
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
public static class HttpSignature {
public static async Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
IEnumerable<string> requiredHeaders, string key) {
public static Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
IEnumerable<string> requiredHeaders, string key) {
if (!requiredHeaders.All(signature.Headers.Contains))
throw new ConstraintException("Request is missing required headers");
//TODO: verify date header exists and is set to something the last 12 hours
var signingString = GenerateSigningString(signature.Headers, request.Method,
request.Path,
request.Headers);
//TODO: does this break for requests without a body?
var digest = await SHA256.HashDataAsync(request.BodyReader.AsStream());
//TODO: this definitely breaks if there's no body
//TODO: check for the SHA256= prefix instead of blindly removing the first 8 chars
if (Convert.ToBase64String(digest) != request.Headers["digest"].ToString().Remove(0, 8))
throw new ConstraintException("Request digest mismatch");
var rsa = RSA.Create();
rsa.ImportFromPem(key);
return rsa.VerifyData(Encoding.UTF8.GetBytes(signingString), signature.Signature,
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return VerifySignature(key, signingString, signature, request.Headers, request.Body);
}
//TODO: make this share code with the the regular Verify function
public static bool VerifySign(this HttpRequestMessage request, string key) {
public static Task<bool> Verify(this HttpRequestMessage request, string key) {
var signatureHeader = request.Headers.GetValues("Signature").First();
var signature = Parse(signatureHeader);
var signingString = GenerateSigningString(signature.Headers, request.Method.Method,
request.RequestUri!.AbsolutePath,
request.Headers.ToHeaderDictionary());
return VerifySignature(key, signingString, signature, request.Headers.ToHeaderDictionary(),
request.Content?.ReadAsStream());
}
private static async Task<bool> VerifySignature(string key, string signingString, HttpSignatureHeader signature,
IHeaderDictionary headers, Stream? body) {
if (!headers.TryGetValue("date", out var date)) throw new Exception("Date header is missing");
if (DateTime.Now - DateTime.Parse(date!) > TimeSpan.FromHours(12)) throw new Exception("Signature too old");
//TODO: does this break for requests without a body?
if (body != null) {
var digest = await SHA256.HashDataAsync(body);
//TODO: check for the SHA256= prefix instead of blindly removing the first 8 chars
if (Convert.ToBase64String(digest) != headers["digest"].ToString().Remove(0, 8))
throw new ConstraintException("Request digest mismatch");
}
var rsa = RSA.Create();
rsa.ImportFromPem(key);
return rsa.VerifyData(Encoding.UTF8.GetBytes(signingString), signature.Signature,

View file

@ -42,11 +42,10 @@ public class HttpRequestService(IOptions<Config.InstanceSection> options) {
return GenerateRequest(url, HttpMethod.Post, body, contentType);
}
public HttpRequestMessage GetSigned(string url, IEnumerable<string>? accept, User user, UserKeypair keypair) {
var msg = Get(url, accept).Sign(["(request-target)", "date", "host", "accept"], keypair.PrivateKey,
public HttpRequestMessage GetSigned(string url, IEnumerable<string>? accept, User user,
UserKeypair keypair) {
return Get(url, accept).Sign(["(request-target)", "date", "host", "accept"], keypair.PrivateKey,
$"https://{options.Value.WebDomain}/users/{user.Id}#main-key");
if (!msg.VerifySign(keypair.PublicKey)) throw new Exception("Failed to sign request");
return msg;
}
public async Task<HttpRequestMessage> PostSigned(string url, string body, string contentType, User user,