about summary refs log tree commit diff
path: root/Utilities/LibMatrix.FederationTest
diff options
context:
space:
mode:
Diffstat (limited to 'Utilities/LibMatrix.FederationTest')
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs79
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs51
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs (renamed from Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs)14
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationVersionController.cs (renamed from Utilities/LibMatrix.FederationTest/Controllers/FederationVersionController.cs)3
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs41
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/WellKnownController.cs (renamed from Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs)2
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/TestController.cs45
-rw-r--r--Utilities/LibMatrix.FederationTest/FedTest.http13
-rw-r--r--Utilities/LibMatrix.FederationTest/LibMatrix.FederationTest.csproj6
-rw-r--r--Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml19
-rw-r--r--Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs9
-rw-r--r--Utilities/LibMatrix.FederationTest/Program.cs40
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs40
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs58
-rw-r--r--Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs2
15 files changed, 378 insertions, 44 deletions
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); }