74 lines
3 KiB
C#
74 lines
3 KiB
C#
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<RequestSigningService>();
|
|
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}", data.Host, data.RequestPath);
|
|
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)
|
|
{
|
|
throw new FoxchatError.IncomingFederationError("Request was made in the future");
|
|
}
|
|
else if ((now - Duration.FromMinutes(1)) > time)
|
|
{
|
|
throw new FoxchatError.IncomingFederationError("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 ?? "";
|
|
|
|
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();
|
|
}
|