diff --git a/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs b/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs
new file mode 100644
index 0000000..ce0e119
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs
@@ -0,0 +1,79 @@
+using LibMatrix.Federation;
+using LibMatrix.Federation.Extensions;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.FederationTest.Utilities;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers;
+
+[ApiController]
+public class RemoteServerPingController(FederationTestConfiguration config, FederationKeyStore keyStore, HomeserverResolverService hsResolver) : ControllerBase {
+ [HttpGet]
+ [Route("/ping/{serverName}")]
+ public async Task<object> PingRemoteServer(string serverName) {
+ Dictionary<string, object> responseMessage = [];
+ var hsResolveResult = await hsResolver.ResolveHomeserverFromWellKnown(serverName, enableClient: false);
+ responseMessage["resolveResult"] = hsResolveResult;
+
+ if (!string.IsNullOrWhiteSpace(hsResolveResult.Server)) {
+ try {
+ var ownKey = keyStore.GetCurrentSigningKey();
+ var hs = new AuthenticatedFederationClient(hsResolveResult.Server, new() {
+ PrivateKey = ownKey.CurrentSigningKey,
+ OriginServerName = config.ServerName
+ });
+ var keys = await hs.GetServerKeysAsync();
+ responseMessage["version"] = await hs.GetServerVersionAsync();
+ responseMessage["keys"] = keys;
+
+ responseMessage["keysAreValid"] = keys.SignaturesById[serverName].ToDictionary(
+ sig => (string)sig.Key,
+ sig => keys.ValidateSignature(serverName, sig.Key, Ed25519Utils.LoadPublicKeyFromEncoded(keys.TypedContent.VerifyKeysById[sig.Key].Key))
+ );
+ }
+ catch (Exception ex) {
+ responseMessage["error"] = new {
+ error = "Failed to connect to remote server",
+ message = ex.Message,
+ st = ex.StackTrace,
+ };
+ return responseMessage;
+ }
+ }
+
+ return responseMessage;
+ }
+
+ [HttpPost]
+ [Route("/ping/")]
+ public async IAsyncEnumerable<KeyValuePair<string, object>> PingRemoteServers([FromBody] List<string>? serverNames) {
+ Dictionary<string, object> responseMessage = [];
+
+ if (serverNames == null || !serverNames.Any()) {
+ responseMessage["error"] = "No server names provided";
+ yield return responseMessage.First();
+ yield break;
+ }
+
+ var results = serverNames!.Select(s => (s, PingRemoteServer(s))).ToList();
+ foreach (var result in results) {
+ var (serverName, pingResult) = result;
+ try {
+ responseMessage[serverName] = await pingResult;
+ if (results.Where(x => !x.Item2.IsCompleted).Select(x => x.s).ToList() is { } servers and not { Count: 0 })
+ Console.WriteLine($"INFO | Waiting for servers: {string.Join(", ", servers)}");
+ }
+ catch (Exception ex) {
+ responseMessage[serverName] = new {
+ error = "Failed to ping remote server",
+ message = ex.Message,
+ st = ex.StackTrace,
+ };
+ }
+
+ yield return new KeyValuePair<string, object>(serverName, responseMessage[serverName]);
+ // await Response.Body.FlushAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs
new file mode 100644
index 0000000..707a149
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs
@@ -0,0 +1,51 @@
+using System.Net.Http.Headers;
+using LibMatrix.Federation;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route("_matrix/federation/")]
+public class DirectoryController(ServerAuthService serverAuth) : ControllerBase {
+ [HttpGet("v1/publicRooms")]
+ [HttpPost("v1/publicRooms")]
+ public async Task<IActionResult> GetPublicRooms() {
+ if (Request.Headers.ContainsKey("Authorization")) {
+ Console.WriteLine("INFO | Authorization header found.");
+ await serverAuth.AssertValidAuthentication();
+ }
+ else Console.WriteLine("INFO | Room directory request without auth");
+
+ var rooms = new List<PublicRoomDirectoryResult.PublicRoomListItem> {
+ new() {
+ GuestCanJoin = false,
+ RoomId = "!tuiLEoMqNOQezxILzt:rory.gay",
+ NumJoinedMembers = Random.Shared.Next(),
+ WorldReadable = false,
+ CanonicalAlias = "#libmatrix:rory.gay",
+ Name = "Rory&::LibMatrix",
+ Topic = $"A .NET {Environment.Version.Major} library for interacting with Matrix"
+ }
+ };
+ return Ok(new PublicRoomDirectoryResult() {
+ Chunk = rooms,
+ TotalRoomCountEstimate = rooms.Count
+ });
+ }
+
+ [HttpGet("v1/query/profile")]
+ public async Task<IActionResult> GetProfile([FromQuery(Name = "user_id")] string userId) {
+ if (Request.Headers.ContainsKey("Authorization")) {
+ Console.WriteLine("INFO | Authorization header found.");
+ await serverAuth.AssertValidAuthentication();
+ }
+ else Console.WriteLine("INFO | Profile request without auth");
+
+ return Ok(new {
+ avatar_url = "mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg",
+ displayname = "Rory&::LibMatrix.FederationTest"
+ });
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs
index 33d0b99..d96bef5 100644
--- a/Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs
@@ -1,10 +1,11 @@
+using LibMatrix.Abstractions;
using LibMatrix.Federation.Extensions;
-using LibMatrix.Federation.Utilities;
using LibMatrix.FederationTest.Services;
using LibMatrix.Homeservers;
+using LibMatrix.Responses.Federation;
using Microsoft.AspNetCore.Mvc;
-namespace LibMatrix.FederationTest.Controllers;
+namespace LibMatrix.FederationTest.Controllers.Spec;
[ApiController]
[Route("_matrix/key/v2/")]
@@ -22,18 +23,19 @@ public class FederationKeysController(FederationTestConfiguration config, Federa
if (_cachedServerKeysResponse == null || _cachedServerKeysResponse.TypedContent.ValidUntil < DateTime.Now + TimeSpan.FromSeconds(30)) {
var keys = keyStore.GetCurrentSigningKey();
_cachedServerKeysResponse = new ServerKeysResponse() {
- ValidUntil = DateTime.Now + TimeSpan.FromMinutes(1),
+ ValidUntil = DateTime.Now + TimeSpan.FromMinutes(5),
ServerName = config.ServerName,
OldVerifyKeys = [],
VerifyKeysById = new() {
{
- new() { Algorithm = "ed25519", KeyId = "0" }, new ServerKeysResponse.CurrentVerifyKey() {
- Key = keys.publicKey.ToUnpaddedBase64(),
+ keys.CurrentSigningKey.KeyId, new ServerKeysResponse.CurrentVerifyKey() {
+ Key = keys.CurrentSigningKey.PublicKey //.ToUnpaddedBase64(),
}
}
}
- }.Sign(config.ServerName, new ServerKeysResponse.VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }, keys.privateKey);
+ }.Sign(keys.CurrentSigningKey);
}
+
_serverKeyCacheLock.Release();
return _cachedServerKeysResponse;
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs
index 2c3aaa3..d146cfd 100644
--- a/Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs
@@ -1,7 +1,8 @@
using LibMatrix.Homeservers;
+using LibMatrix.Responses.Federation;
using Microsoft.AspNetCore.Mvc;
-namespace LibMatrix.FederationTest.Controllers;
+namespace LibMatrix.FederationTest.Controllers.Spec;
[ApiController]
[Route("_matrix/federation/v1/")]
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs
new file mode 100644
index 0000000..7c561ad
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs
@@ -0,0 +1,41 @@
+using System.Net.Http.Headers;
+using LibMatrix.Federation;
+using LibMatrix.Federation.FederationTypes;
+using LibMatrix.FederationTest.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route("_matrix/federation/")]
+public class MembershipsController(ServerAuthService sas) : ControllerBase {
+ [HttpGet("v1/make_join/{roomId}/{userId}")]
+ [HttpPut("v1/send_join/{roomId}/{eventId}")]
+ [HttpPut("v2/send_join/{roomId}/{eventId}")]
+ [HttpGet("v1/make_knock/{roomId}/{userId}")]
+ [HttpPut("v1/send_knock/{roomId}/{eventId}")]
+ [HttpGet("v1/make_leave/{roomId}/{eventId}")]
+ [HttpPut("v1/send_leave/{roomId}/{eventId}")]
+ [HttpPut("v2/send_leave/{roomId}/{eventId}")]
+ public async Task<IActionResult> JoinKnockMemberships() {
+ await sas.AssertValidAuthentication();
+ return NotFound(new MatrixException() {
+ ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND,
+ Error = "Rory&::LibMatrix.FederationTest does not support membership events."
+ }.GetAsObject());
+ }
+
+ // [HttpPut("v1/invite/{roomId}/{eventId}")]
+ [HttpPut("v2/invite/{roomId}/{eventId}")]
+ public async Task<IActionResult> InviteHandler([FromBody] RoomInvite invite) {
+ await sas.AssertValidAuthentication();
+
+ Console.WriteLine($"Received invite event from {invite.Event.Sender} for room {invite.Event.RoomId} (version {invite.RoomVersion})\n" +
+ $"{invite.InviteRoomState.Count} invite room state events.");
+
+ return NotFound(new MatrixException() {
+ ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND,
+ Error = "Rory&::LibMatrix.FederationTest does not support membership events."
+ }.GetAsObject());
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs
index 28fca8d..b91868c 100644
--- a/Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs
@@ -1,7 +1,7 @@
using LibMatrix.Services.WellKnownResolver.WellKnownResolvers;
using Microsoft.AspNetCore.Mvc;
-namespace LibMatrix.FederationTest.Controllers;
+namespace LibMatrix.FederationTest.Controllers.Spec;
[ApiController]
[Route(".well-known/")]
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
index 4a6bc87..900c8a0 100644
--- a/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
+++ b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
@@ -1,10 +1,8 @@
using System.Text.Json.Nodes;
-using ArcaneLibs.Extensions;
using LibMatrix.Extensions;
using LibMatrix.Federation;
-using LibMatrix.Federation.Utilities;
+using LibMatrix.Federation.Extensions;
using LibMatrix.FederationTest.Services;
-using LibMatrix.Homeservers;
using Microsoft.AspNetCore.Mvc;
namespace LibMatrix.FederationTest.Controllers;
@@ -21,32 +19,33 @@ public class TestController(FederationTestConfiguration config, FederationKeySto
BaseAddress = new Uri("https://matrix.rory.gay")
};
- var keyId = new ServerKeysResponse.VersionedKeyId() {
- Algorithm = "ed25519",
- KeyId = "0"
- };
+ var currentKey = keyStore.GetCurrentSigningKey().CurrentSigningKey;
var signatureData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() {
- Method = "GET",
- Uri = "/_matrix/federation/v1/user/devices/@emma:rory.gay",
- OriginServerName = config.ServerName,
- DestinationServerName = "rory.gay"
- }
- .Sign(config.ServerName, keyId, keyStore.GetCurrentSigningKey().privateKey);
-
- var signature = signatureData.Signatures[config.ServerName][keyId];
- var headerValue = new XMatrixAuthorizationScheme.XMatrixAuthorizationHeader() {
- Origin = config.ServerName,
- Destination = "rory.gay",
- Key = keyId,
- Signature = signature
- }.ToHeaderValue();
+ OriginServerName = config.ServerName,
+ Method = "GET",
+ DestinationServerName = "rory.gay",
+ Uri = "/_matrix/federation/v1/user/devices/@emma:rory.gay",
+ };
+ // .Sign(currentKey);
+ //
+ // var signature = signatureData.Signatures[config.ServerName][currentKey.KeyId];
+ // var headerValue = new XMatrixAuthorizationScheme.XMatrixAuthorizationHeader() {
+ // Origin = config.ServerName,
+ // Key = currentKey.KeyId,
+ // Destination = "rory.gay",
+ // Signature = signature
+ // }.ToHeaderValue();
- var req = new HttpRequestMessage(HttpMethod.Get, "/_matrix/federation/v1/user/devices/@emma:rory.gay");
- req.Headers.Add("Authorization", headerValue);
+ // var req = new HttpRequestMessage(HttpMethod.Get, "/_matrix/federation/v1/user/devices/@emma:rory.gay");
+ // req.Headers.Add("Authorization", headerValue);
+ var req = signatureData.ToSignedHttpRequestMessage(currentKey);
var response = await hc.SendAsync(req);
var content = await response.Content.ReadFromJsonAsync<JsonObject>();
return content!;
}
+
+ // [HttpGet("/testMakeJoin")]
+ // public async Task<JsonObject> GetTestMakeJoin() { }
}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/FedTest.http b/Utilities/LibMatrix.FederationTest/FedTest.http
new file mode 100644
index 0000000..26b1cd0
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/FedTest.http
@@ -0,0 +1,13 @@
+POST https://libmatrix-fed-test.rory.gay/ping
+Accept: application/json
+Content-Type: application/json
+
+[
+ "matrix.org",
+ "rory.gay",
+ "element.io",
+ "4d2.org",
+ "mozilla.org",
+ "fedora.im",
+ "opensuse.org"
+]
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
index 9b34f77..58e336e 100644
--- a/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
+++ b/Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj
@@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5"/>
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
- <ProjectReference Include="..\LibMatrix\LibMatrix.Federation\LibMatrix.Federation.csproj" />
+ <ProjectReference Include="..\..\LibMatrix.Federation\LibMatrix.Federation.csproj"/>
</ItemGroup>
</Project>
diff --git a/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml
new file mode 100644
index 0000000..283c13e
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml
@@ -0,0 +1,19 @@
+@page "/"
+@model LibMatrix.FederationTest.Pages.IndexPage
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <title>LibMatrix.FederationTest</title>
+ </head>
+ <body>
+ <div>
+ If you're seeing this, LibMatrix.FederationTest is running!
+ </div>
+ </body>
+</html>
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs
new file mode 100644
index 0000000..0d372b0
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace LibMatrix.FederationTest.Pages;
+
+public class IndexPage : PageModel {
+ public void OnGet() {
+
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs
index adc809f..3e9cb80 100644
--- a/Utilities/LibMatrix.FederationTest/Program.cs
+++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -1,10 +1,38 @@
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using LibMatrix.Extensions;
+using LibMatrix.Federation;
using LibMatrix.FederationTest.Services;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
-builder.Services.AddControllers();
+builder.Services.AddControllers()
+ .ConfigureApiBehaviorOptions(options => {
+ options.InvalidModelStateResponseFactory = context => {
+ var problemDetails = new ValidationProblemDetails(context.ModelState) {
+ Status = StatusCodes.Status400BadRequest,
+ Title = "One or more validation errors occurred.",
+ Detail = "See the errors property for more details.",
+ Instance = context.HttpContext.Request.Path
+ };
+
+ Console.WriteLine("Model validation failed: " + problemDetails.ToJson());
+
+ return new BadRequestObjectResult(problemDetails) {
+ ContentTypes = { "application/problem+json", "application/problem+xml" }
+ };
+ };
+ })
+ .AddJsonOptions(options => {
+ options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
+ options.JsonSerializerOptions.WriteIndented = true;
+ // options.JsonSerializerOptions.DefaultBufferSize = ;
+ }).AddMvcOptions(o => { o.SuppressOutputFormatterBuffering = true; });
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddHttpLogging(options => {
@@ -13,22 +41,24 @@ builder.Services.AddHttpLogging(options => {
options.RequestHeaders.Add("X-Forwarded-Host");
options.RequestHeaders.Add("X-Forwarded-Port");
});
+builder.Services.AddRazorPages();
+builder.Services.AddHttpContextAccessor();
+builder.Services.AddRoryLibMatrixServices();
builder.Services.AddSingleton<FederationTestConfiguration>();
builder.Services.AddSingleton<FederationKeyStore>();
-
+builder.Services.AddScoped<ServerAuthService>();
var app = builder.Build();
-
// Configure the HTTP request pipeline.
if (true || app.Environment.IsDevelopment()) {
app.MapOpenApi();
}
-app.UseAuthorization();
+// app.UseAuthorization();
app.MapControllers();
+app.MapRazorPages();
// app.UseHttpLogging();
-
app.Run();
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
index f24d14e..b892dbb 100644
--- a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
+++ b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
@@ -1,3 +1,7 @@
+using System.Text.Json;
+using ArcaneLibs.Extensions;
+using LibMatrix.Abstractions;
+using LibMatrix.Federation.Extensions;
using LibMatrix.FederationTest.Utilities;
using Org.BouncyCastle.Crypto.Parameters;
@@ -9,13 +13,41 @@ public class FederationKeyStore(FederationTestConfiguration config) {
}
private static (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) currentKeyPair = default;
- public (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) GetCurrentSigningKey() {
+
+ 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);
-
+
+ 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();
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
diff --git a/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
index bb57d51..7714fee 100644
--- a/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
+++ b/Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs
@@ -18,7 +18,7 @@ public class Ed25519Utils {
}
public static Ed25519PublicKeyParameters LoadPublicKeyFromEncoded(string key) {
- var keyBytes = Convert.FromBase64String(key);
+ var keyBytes = UnpaddedBase64.Decode(key);
return new Ed25519PublicKeyParameters(keyBytes, 0);
}
|