diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
new file mode 100644
index 0000000..b892dbb
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
@@ -0,0 +1,63 @@
+using System.Text.Json;
+using ArcaneLibs.Extensions;
+using LibMatrix.Abstractions;
+using LibMatrix.Federation.Extensions;
+using LibMatrix.FederationTest.Utilities;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace LibMatrix.FederationTest.Services;
+
+public class FederationKeyStore(FederationTestConfiguration config) {
+ static FederationKeyStore() {
+ Console.WriteLine("INFO | FederationKeyStore initialized.");
+ }
+
+ private static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) currentKeyPair = default;
+
+ public class PrivateKeyCollection {
+ public required VersionedHomeserverPrivateKey CurrentSigningKey { get; set; }
+ }
+
+ public PrivateKeyCollection GetCurrentSigningKey() {
+ if (!Directory.Exists(config.KeyStorePath)) Directory.CreateDirectory(config.KeyStorePath);
+ var privateKeyPath = Path.Combine(config.KeyStorePath, "private-keys.json");
+
+ if (!File.Exists(privateKeyPath)) {
+ var keyPair = InternalGetSigningKey();
+ var privateKey = new PrivateKeyCollection() {
+ CurrentSigningKey = new VersionedHomeserverPrivateKey {
+ ServerName = config.ServerName,
+ KeyId = new() {
+ Algorithm = "ed25519",
+ KeyId = "0"
+ },
+ PrivateKey = keyPair.privateKey.ToUnpaddedBase64(),
+ PublicKey = keyPair.publicKey.ToUnpaddedBase64(),
+ }
+ };
+ File.WriteAllText(privateKeyPath, privateKey.ToJson());
+ }
+
+ return JsonSerializer.Deserialize<PrivateKeyCollection>(File.ReadAllText(privateKeyPath))!;
+ }
+
+ private (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) InternalGetSigningKey() {
+ if (currentKeyPair != default) {
+ return currentKeyPair;
+ }
+
+ if (!Directory.Exists(config.KeyStorePath)) Directory.CreateDirectory(config.KeyStorePath);
+
+ var privateKeyPath = Path.Combine(config.KeyStorePath, "signing.key");
+ if (!File.Exists(privateKeyPath)) {
+ var keyPair = Ed25519Utils.GenerateKeyPair();
+ File.WriteAllBytes(privateKeyPath, keyPair.privateKey.GetEncoded());
+ return keyPair;
+ }
+
+ var privateKeyBytes = File.ReadAllBytes(privateKeyPath);
+ var privateKey = Ed25519Utils.LoadPrivateKeyFromEncoded(privateKeyBytes);
+ var publicKey = privateKey.GeneratePublicKey();
+ return currentKeyPair = (privateKey, publicKey);
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
new file mode 100644
index 0000000..353ddf5
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationTestConfiguration.cs
@@ -0,0 +1,10 @@
+namespace LibMatrix.FederationTest.Services;
+
+public class FederationTestConfiguration {
+ public FederationTestConfiguration(IConfiguration configurationSection) {
+ configurationSection.GetRequiredSection("FederationTest").Bind(this);
+ }
+
+ public string ServerName { get; set; } = "localhost";
+ public string KeyStorePath { get; set; } = "./.keys";
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs b/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs
new file mode 100644
index 0000000..58274eb
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs
@@ -0,0 +1,58 @@
+using System.Net.Http.Headers;
+using System.Text.Json.Nodes;
+using LibMatrix.Extensions;
+using LibMatrix.Federation;
+using LibMatrix.FederationTest.Utilities;
+using LibMatrix.Responses.Federation;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Http.Features;
+using Org.BouncyCastle.Math.EC.Rfc8032;
+
+namespace LibMatrix.FederationTest.Services;
+
+public class ServerAuthService(HomeserverProviderService hsProvider, IHttpContextAccessor httpContextAccessor) {
+ private static Dictionary<string, SignedObject<ServerKeysResponse>> _serverKeysCache = new();
+
+ public async Task AssertValidAuthentication(XMatrixAuthorizationScheme.XMatrixAuthorizationHeader authHeader) {
+ var httpContext = httpContextAccessor.HttpContext!;
+ var hs = await hsProvider.GetFederationClient(authHeader.Origin, "");
+ var serverKeys = (_serverKeysCache.TryGetValue(authHeader.Origin, out var sk) && sk.TypedContent.ValidUntil > DateTimeOffset.UtcNow)
+ ? sk
+ : _serverKeysCache[authHeader.Origin] = await hs.GetServerKeysAsync();
+ var publicKeyBase64 = serverKeys.TypedContent.VerifyKeys[authHeader.Key].Key;
+ var publicKey = Ed25519Utils.LoadPublicKeyFromEncoded(publicKeyBase64);
+ var requestAuthenticationData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() {
+ Method = httpContext.Request.Method,
+ Uri = httpContext.Features.Get<IHttpRequestFeature>()!.RawTarget,
+ OriginServerName = authHeader.Origin,
+ DestinationServerName = authHeader.Destination,
+ Content = httpContext.Request.HasJsonContentType() ? await httpContext.Request.ReadFromJsonAsync<JsonObject?>() : null
+ };
+ var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(requestAuthenticationData);
+ var signatureBytes = UnpaddedBase64.Decode(authHeader.Signature);
+
+ Console.WriteLine($"Validating X-Matrix authorized request\n" +
+ $" - From: {requestAuthenticationData.OriginServerName}, To: {requestAuthenticationData.DestinationServerName}\n" +
+ $" - Key: {authHeader.Key} ({publicKeyBase64})\n" +
+ $" - Signature: {authHeader.Signature}\n" +
+ $" - Request: {requestAuthenticationData.Method} {requestAuthenticationData.Uri}\n" +
+ $" - Has request body: {requestAuthenticationData.Content is not null}\n" +
+ // $" - Canonicalized request body (or null if missing): {(requestAuthenticationData.Content is null ? "(null)" : CanonicalJsonSerializer.Serialize(requestAuthenticationData.Content))}\n" +
+ $" - Canonicalized message to verify: {System.Text.Encoding.UTF8.GetString(contentBytes)}");
+
+ if (!publicKey.Verify(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signatureBytes, 0)) {
+ throw new UnauthorizedAccessException("Invalid signature in X-Matrix authorization header.");
+ }
+
+ Console.WriteLine("INFO | Valid X-Matrix authorization header.");
+ }
+
+ public async Task AssertValidAuthentication() {
+ await AssertValidAuthentication(
+ XMatrixAuthorizationScheme.XMatrixAuthorizationHeader.FromHeaderValue(
+ httpContextAccessor.HttpContext!.Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")!
+ )
+ );
+ }
+}
\ No newline at end of file
|