using System.Globalization; using System.Security.Cryptography; using System.Text; using Foxchat.Core.Database; using Microsoft.AspNetCore.WebUtilities; using NodaTime; using NodaTime.Text; using Serilog; namespace Foxchat.Core.Federation; public partial class RequestSigningService(ILogger logger, IClock clock, IDatabaseContext context, CoreConfig config) { private readonly ILogger _logger = logger.ForContext(); private readonly IClock _clock = clock; private readonly CoreConfig _config = config; private readonly RSA _rsa = context.GetInstanceKeysAsync().GetAwaiter().GetResult(); private readonly HttpClient _httpClient = new(); public string GenerateSignature(SignatureData data) { var plaintext = GeneratePlaintext(data); var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); var hash = SHA256.HashData(plaintextBytes); var formatter = new RSAPKCS1SignatureFormatter(_rsa); formatter.SetHashAlgorithm(nameof(SHA256)); var signature = formatter.CreateSignature(hash); _logger.Debug("Generated signature for {Host} {RequestPath}: {Signature}", data.Host, data.RequestPath, WebEncoders.Base64UrlEncode(signature)); return WebEncoders.Base64UrlEncode(signature); } public bool VerifySignature( string publicKey, string encodedSignature, string dateHeader, string host, string requestPath, int? contentLength, string? userId) { var rsa = RSA.Create(); rsa.ImportFromPem(publicKey); var now = _clock.GetCurrentInstant(); var time = ParseTime(dateHeader); if ((now + Duration.FromMinutes(1)) < time) { // TODO: replace this with specific exception type throw new Exception("Request was made in the future"); } else if ((now - Duration.FromMinutes(1)) > time) { // TODO: replace this with specific exception type throw new Exception("Request was made too long ago"); } var plaintext = GeneratePlaintext(new SignatureData(time, host, requestPath, contentLength, userId)); var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); var hash = SHA256.HashData(plaintextBytes); var signature = WebEncoders.Base64UrlDecode(encodedSignature); var deformatter = new RSAPKCS1SignatureDeformatter(rsa); deformatter.SetHashAlgorithm(nameof(SHA256)); return deformatter.VerifySignature(hash, signature); } private static string GeneratePlaintext(SignatureData data) { var time = FormatTime(data.Time); var contentLength = data.ContentLength != null ? data.ContentLength.ToString() : ""; var userId = data.UserId ?? ""; Log.Information("Plaintext string: {Plaintext}", $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}"); return $"{time}:{data.Host}:{data.RequestPath}:{contentLength}:{userId}"; } private static readonly InstantPattern _pattern = InstantPattern.Create("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.GetCultureInfo("en-US")); private static string FormatTime(Instant time) => _pattern.Format(time); private static Instant ParseTime(string header) => _pattern.Parse(header).GetValueOrThrow(); }