Add basic HTTP signature validation (implementation works but needs to be integrated into the rest of the thing)
This commit is contained in:
parent
371c135360
commit
3bea6254b3
2 changed files with 102 additions and 0 deletions
43
Iceshrimp.Backend/Controllers/SignatureTestController.cs
Normal file
43
Iceshrimp.Backend/Controllers/SignatureTestController.cs
Normal file
|
@ -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<IActionResult> 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string> 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue