Refactor http signature validation
This commit is contained in:
parent
3fba5550d5
commit
5f088ba66f
2 changed files with 25 additions and 21 deletions
|
@ -7,39 +7,44 @@ using Microsoft.Extensions.Primitives;
|
||||||
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||||
|
|
||||||
public static class HttpSignature {
|
public static class HttpSignature {
|
||||||
public static async Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
|
public static Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
|
||||||
IEnumerable<string> requiredHeaders, string key) {
|
IEnumerable<string> requiredHeaders, string key) {
|
||||||
if (!requiredHeaders.All(signature.Headers.Contains))
|
if (!requiredHeaders.All(signature.Headers.Contains))
|
||||||
throw new ConstraintException("Request is missing required headers");
|
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,
|
var signingString = GenerateSigningString(signature.Headers, request.Method,
|
||||||
request.Path,
|
request.Path,
|
||||||
request.Headers);
|
request.Headers);
|
||||||
|
|
||||||
//TODO: does this break for requests without a body?
|
return VerifySignature(key, signingString, signature, request.Headers, request.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: make this share code with the the regular Verify function
|
//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 signatureHeader = request.Headers.GetValues("Signature").First();
|
||||||
var signature = Parse(signatureHeader);
|
var signature = Parse(signatureHeader);
|
||||||
var signingString = GenerateSigningString(signature.Headers, request.Method.Method,
|
var signingString = GenerateSigningString(signature.Headers, request.Method.Method,
|
||||||
request.RequestUri!.AbsolutePath,
|
request.RequestUri!.AbsolutePath,
|
||||||
request.Headers.ToHeaderDictionary());
|
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();
|
var rsa = RSA.Create();
|
||||||
rsa.ImportFromPem(key);
|
rsa.ImportFromPem(key);
|
||||||
return rsa.VerifyData(Encoding.UTF8.GetBytes(signingString), signature.Signature,
|
return rsa.VerifyData(Encoding.UTF8.GetBytes(signingString), signature.Signature,
|
||||||
|
|
|
@ -42,11 +42,10 @@ public class HttpRequestService(IOptions<Config.InstanceSection> options) {
|
||||||
return GenerateRequest(url, HttpMethod.Post, body, contentType);
|
return GenerateRequest(url, HttpMethod.Post, body, contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpRequestMessage GetSigned(string url, IEnumerable<string>? accept, User user, UserKeypair keypair) {
|
public HttpRequestMessage GetSigned(string url, IEnumerable<string>? accept, User user,
|
||||||
var msg = Get(url, accept).Sign(["(request-target)", "date", "host", "accept"], keypair.PrivateKey,
|
UserKeypair keypair) {
|
||||||
|
return Get(url, accept).Sign(["(request-target)", "date", "host", "accept"], keypair.PrivateKey,
|
||||||
$"https://{options.Value.WebDomain}/users/{user.Id}#main-key");
|
$"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,
|
public async Task<HttpRequestMessage> PostSigned(string url, string body, string contentType, User user,
|
||||||
|
|
Loading…
Add table
Reference in a new issue