From 3bea6254b34ced83b2ce0354921bd79ee89d8651 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Fri, 5 Jan 2024 20:05:14 +0100 Subject: [PATCH] Add basic HTTP signature validation (implementation works but needs to be integrated into the rest of the thing) --- .../Controllers/SignatureTestController.cs | 43 ++++++++++++++ .../Federation/Cryptography/HttpSignature.cs | 59 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 Iceshrimp.Backend/Controllers/SignatureTestController.cs create mode 100644 Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs diff --git a/Iceshrimp.Backend/Controllers/SignatureTestController.cs b/Iceshrimp.Backend/Controllers/SignatureTestController.cs new file mode 100644 index 00000000..359f4ee1 --- /dev/null +++ b/Iceshrimp.Backend/Controllers/SignatureTestController.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Mime; +using Iceshrimp.Backend.Controllers.Schemas; +using Iceshrimp.Backend.Core.Database; +using Iceshrimp.Backend.Core.Database.Tables; +using Iceshrimp.Backend.Core.Federation.Cryptography; +using Iceshrimp.Backend.Core.Helpers; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Iceshrimp.Backend.Controllers; + +[ApiController] +[Produces("application/json")] +[Route("/inbox")] +public class SignatureTestController : Controller { + [HttpPost] + [Consumes(MediaTypeNames.Application.Json)] + public async Task Inbox() { + var sig = new HttpSignature(Request, ["(request-target)", "digest", "host", "date"]); + + //TODO: fetch key from db (duh) + + const string key = """ + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtCFuaufkSCpsDZ2twSrH + GAFcJTGQ7ZspaFekVM7gBP1GQ/jfjwO3qT9fMgbsCuQXNTIw0U9zlsTIPB91yNPw + w5UpbqQ3dnpnYnwXg1BsqfX7EOLR1Dlnw6dk+5yeginJsNno15SRQ7CDqbEXj7Nc + lhNOGgU+LaXHhN59Paye3sfsvUHu4fmTp/rALWGPl/Rvx7RVRcR76CcTfTaHPYdb + OQAtqPJBfWgHpPLAUjRypzZoN/ExMgiCbFuxI7UFNNXxU3te8GNZaaob8bSwyUB6 + Xuq7Rw+Me3eYiDxrYHQ99ZytsgoHBNVrVh/X7wIl0AlpjyWeGug3uIUjXR0twuGj + wwIDAQAB + -----END PUBLIC KEY----- + """; + + return Ok(new ErrorResponse { + StatusCode = 200, + Error = "null", + Message = sig.Verify(key).ToString() + }); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs new file mode 100644 index 00000000..d48968fa --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs @@ -0,0 +1,59 @@ +using System.Data; +using System.Security.Cryptography; +using System.Text; + +namespace Iceshrimp.Backend.Core.Federation.Cryptography; + +public class HttpSignature { + private readonly string _keyId; + private readonly string _algo; + private readonly byte[] _signature; + private readonly byte[] _signatureData; + + public HttpSignature(HttpRequest request, IEnumerable requiredHeaders) { + if (!request.Headers.TryGetValue("signature", out var sigHeader)) + throw new ConstraintException("Signature string is missing the signature header"); + + //TODO: does this break for requests without a body? + var digest = SHA256.HashDataAsync(request.BodyReader.AsStream()).AsTask(); + + var sig = sigHeader.ToString().Split(",") + .Select(s => s.Split('=')) + .ToDictionary(p => p[0], p => (p[1] + new string('=', p.Length - 2)).Trim('"')); + + var signatureBase64 = sig["signature"] ?? throw new ConstraintException("Signature string is missing the signature field"); + + _keyId = sig["keyId"] ?? throw new ConstraintException("Signature string is missing the keyId field"); + _algo = sig["algorithm"] ?? throw new ConstraintException("Signature string is missing the algorithm field"); + _signature = Convert.FromBase64String(signatureBase64); + + var headers = sig["headers"].Split(" ") ?? + throw new ConstraintException("Signature data is missing the headers field"); + + var sb = new StringBuilder(); + if (!requiredHeaders.All(headers.Contains)) + throw new ConstraintException("Request is missing required headers"); + + //TODO: handle additional params, see https://github.com/Chocobozzz/node-http-signature/blob/master/lib/parser.js#L294-L310 + foreach (var header in headers) { + sb.Append($"{header}: "); + sb.AppendLine(header switch { + "(request-target)" => $"{request.Method.ToLower()} {request.Path}", + _ => request.Headers[header] + }); + } + + _signatureData = Encoding.UTF8.GetBytes(sb.ToString(0, sb.Length - 1)); // remove trailing newline + + //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.Result) != request.Headers["digest"].ToString().Remove(0, 8)) + throw new ConstraintException("Request digest mismatch"); + } + + public bool Verify(string key) { + var rsa = RSA.Create(); + rsa.ImportFromPem(key); + return rsa.VerifyData(_signatureData, _signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } +} \ No newline at end of file