about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-07-16 19:00:05 +0200
committerRory& <root@rory.gay>2025-07-16 19:00:05 +0200
commitc2613aab129c8d1d5aba3b7ed02609059a826c84 (patch)
treeaeb7a3610cae4c15f013ef7181b379f3b53f5534
parentFix dependency path for federationtest (diff)
downloadLibMatrix-c2613aab129c8d1d5aba3b7ed02609059a826c84.tar.xz
Federation tuff
m---------ArcaneLibs0
-rw-r--r--LibMatrix.Federation/AuthenticatedFederationClient.cs26
-rw-r--r--LibMatrix.Federation/Extensions/Ed25519Extensions.cs2
-rw-r--r--LibMatrix.Federation/Extensions/ObjectExtensions.cs31
-rw-r--r--LibMatrix.Federation/Extensions/SignedObjectExtensions.cs37
-rw-r--r--LibMatrix.Federation/Extensions/XMatrixAuthorizationSchemeExtensions.cs20
-rw-r--r--LibMatrix.Federation/XMatrixAuthorizationScheme.cs1
-rw-r--r--LibMatrix/Abstractions/VersionedKeyId.cs24
-rw-r--r--LibMatrix/Abstractions/VersionedPublicKey.cs16
-rw-r--r--LibMatrix/Homeservers/FederationClient.cs79
-rw-r--r--LibMatrix/LibMatrix.csproj4
-rw-r--r--LibMatrix/Responses/Federation/ServerKeysResponse.cs55
-rw-r--r--LibMatrix/Responses/Federation/ServerVersionResponse.cs16
-rw-r--r--LibMatrix/Responses/Federation/SignedObject.cs (renamed from LibMatrix.Federation/Utilities/JsonSigning.cs)48
-rw-r--r--LibMatrix/Services/HomeserverProviderService.cs1
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs79
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs (renamed from Utilities/LibMatrix.FederationTest/Controllers/FederationKeysController.cs)7
-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/WellKnownController.cs (renamed from Utilities/LibMatrix.FederationTest/Controllers/WellKnownController.cs)2
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/TestController.cs6
-rw-r--r--Utilities/LibMatrix.FederationTest/Program.cs14
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs25
-rw-r--r--Utilities/LibMatrix.FederationTest/Utilities/Ed25519Utils.cs2
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs1
24 files changed, 350 insertions, 149 deletions
diff --git a/ArcaneLibs b/ArcaneLibs
-Subproject 7cbf235cceb82dc711622314c49edfc7e2614dc
+Subproject 549f13d0be4a5a386cd027242acce752b0c674d
diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs

index 6f8d44b..15dc88f 100644 --- a/LibMatrix.Federation/AuthenticatedFederationClient.cs +++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -1,20 +1,24 @@ -using LibMatrix.Homeservers; +using LibMatrix.Abstractions; +using LibMatrix.Federation.Extensions; +using LibMatrix.Homeservers; namespace LibMatrix.Federation; -public class AuthenticatedFederationClient : FederationClient { +public class AuthenticatedFederationClient(string federationEndpoint, AuthenticatedFederationClient.AuthenticatedFederationConfiguration config, string? proxy = null) : FederationClient(federationEndpoint, proxy) { public class AuthenticatedFederationConfiguration { - - } - public AuthenticatedFederationClient(string federationEndpoint, AuthenticatedFederationConfiguration config, string? proxy = null) : base(federationEndpoint, proxy) - { - + public required VersionedHomeserverPrivateKey PrivateKey { get; set; } + public required string OriginServerName { get; set; } } - // public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) { - // var response = await GetAsync<UserDeviceListResponse>($"/_matrix/federation/v1/user/devices/{userId}", accessToken); - // return response; - // } + public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) { + var response = await HttpClient.SendAsync(new XMatrixAuthorizationScheme.XMatrixRequestSignature() { + OriginServerName = config.OriginServerName, + DestinationServerName = userId.Split(':', 2)[1], + Method = "GET", + Uri = $"/_matrix/federation/v1/user/devices/{userId}", + }.ToSignedHttpRequestMessage(config.PrivateKey)); + return response; + } } \ No newline at end of file diff --git a/LibMatrix.Federation/Extensions/Ed25519Extensions.cs b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
index 69baf58..e5a9e5d 100644 --- a/LibMatrix.Federation/Extensions/Ed25519Extensions.cs +++ b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
@@ -1,3 +1,4 @@ +using LibMatrix.Abstractions; using LibMatrix.FederationTest.Utilities; using Org.BouncyCastle.Crypto.Parameters; @@ -5,4 +6,5 @@ namespace LibMatrix.Federation.Extensions; public static class Ed25519Extensions { public static string ToUnpaddedBase64(this Ed25519PublicKeyParameters key) => UnpaddedBase64.Encode(key.GetEncoded()); + public static Ed25519PrivateKeyParameters GetPrivateEd25519Key(this VersionedHomeserverPrivateKey key) => new(UnpaddedBase64.Decode(key.PrivateKey), 0); } \ No newline at end of file diff --git a/LibMatrix.Federation/Extensions/ObjectExtensions.cs b/LibMatrix.Federation/Extensions/ObjectExtensions.cs new file mode 100644
index 0000000..d20385d --- /dev/null +++ b/LibMatrix.Federation/Extensions/ObjectExtensions.cs
@@ -0,0 +1,31 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using LibMatrix.Abstractions; +using LibMatrix.Extensions; +using LibMatrix.FederationTest.Utilities; +using LibMatrix.Responses.Federation; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math.EC.Rfc8032; + +namespace LibMatrix.Federation.Extensions; +public static class ObjectExtensions { + public static SignedObject<T> Sign<T>(this T content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { + SignedObject<T> signedObject = new() { + Signatures = [], + Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(content)) ?? new JsonObject(), + }; + + var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(signedObject.Content); + var signature = new byte[Ed25519.SignatureSize]; + key.Sign(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signature, 0); + + if (!signedObject.Signatures.ContainsKey(serverName)) + signedObject.Signatures[serverName] = new Dictionary<string, string>(); + + signedObject.Signatures[serverName][keyName] = UnpaddedBase64.Encode(signature); + return signedObject; + } + + public static SignedObject<T> Sign<T>(this T content, VersionedHomeserverPrivateKey privateKey) + => Sign(content, privateKey.ServerName, privateKey.KeyId, privateKey.GetPrivateEd25519Key()); +} \ No newline at end of file diff --git a/LibMatrix.Federation/Extensions/SignedObjectExtensions.cs b/LibMatrix.Federation/Extensions/SignedObjectExtensions.cs new file mode 100644
index 0000000..eb1376e --- /dev/null +++ b/LibMatrix.Federation/Extensions/SignedObjectExtensions.cs
@@ -0,0 +1,37 @@ +using LibMatrix.Extensions; +using LibMatrix.FederationTest.Utilities; +using LibMatrix.Responses.Federation; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math.EC.Rfc8032; + +namespace LibMatrix.Federation.Extensions; +public static class SignedObjectExtensions { + public static SignedObject<T> Sign<T>(this SignedObject<T> content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { + var signResult = content.Content.Sign(serverName, keyName, key); + var signedObject = new SignedObject<T> { + Signatures = content.Signatures, + Content = signResult.Content + }; + + if (!signedObject.Signatures.ContainsKey(serverName)) + signedObject.Signatures[serverName] = new Dictionary<string, string>(); + + signedObject.Signatures[serverName][keyName] = signResult.Signatures[serverName][keyName]; + return signedObject; + } + + public static bool ValidateSignature<T>(this SignedObject<T> content, string serverName, string keyName, Ed25519PublicKeyParameters key) { + if (!content.Signatures.TryGetValue(serverName, out var serverSignatures)) + return false; + + if (!serverSignatures.TryGetValue(keyName, out var signatureBase64)) + return false; + + var signature = UnpaddedBase64.Decode(signatureBase64); + if (signature.Length != Ed25519.SignatureSize) + return false; + + var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(content.Content); + return Ed25519.Verify(signature, 0, key.GetEncoded(), 0, contentBytes, 0, contentBytes.Length); + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/Extensions/XMatrixAuthorizationSchemeExtensions.cs b/LibMatrix.Federation/Extensions/XMatrixAuthorizationSchemeExtensions.cs new file mode 100644
index 0000000..792264a --- /dev/null +++ b/LibMatrix.Federation/Extensions/XMatrixAuthorizationSchemeExtensions.cs
@@ -0,0 +1,20 @@ +using System.Net.Http.Json; +using LibMatrix.Abstractions; + +namespace LibMatrix.Federation.Extensions; + +public static class XMatrixAuthorizationSchemeExtensions { + public static HttpRequestMessage ToSignedHttpRequestMessage(this XMatrixAuthorizationScheme.XMatrixRequestSignature requestSignature, VersionedHomeserverPrivateKey privateKey) { + var signature = requestSignature.Sign(privateKey); + var requestMessage = new HttpRequestMessage { + Method = new HttpMethod(requestSignature.Method), + RequestUri = new Uri(requestSignature.Uri, UriKind.Relative) + }; + + if (requestSignature.Content != null) { + requestMessage.Content = JsonContent.Create(requestSignature.Content); + } + + return requestMessage; + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
index fc402b7..45899b8 100644 --- a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs +++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -1,4 +1,3 @@ -using System.Net; using System.Net.Http.Headers; using System.Text.Json.Nodes; using System.Text.Json.Serialization; diff --git a/LibMatrix/Abstractions/VersionedKeyId.cs b/LibMatrix/Abstractions/VersionedKeyId.cs new file mode 100644
index 0000000..0a6d651 --- /dev/null +++ b/LibMatrix/Abstractions/VersionedKeyId.cs
@@ -0,0 +1,24 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace LibMatrix.Abstractions; + +[DebuggerDisplay("{Algorithm}:{KeyId}")] +[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] +public class VersionedKeyId { + public required string Algorithm { get; set; } + public required string KeyId { get; set; } + + public static implicit operator VersionedKeyId(string key) { + var parts = key.Split(':', 2); + if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key)); + return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] }; + } + + public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}"; + public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId); + public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId); + + public override bool Equals(object? obj) => obj is VersionedKeyId other && Algorithm == other.Algorithm && KeyId == other.KeyId; + public override int GetHashCode() => HashCode.Combine(Algorithm, KeyId); +} \ No newline at end of file diff --git a/LibMatrix/Abstractions/VersionedPublicKey.cs b/LibMatrix/Abstractions/VersionedPublicKey.cs new file mode 100644
index 0000000..ad6747d --- /dev/null +++ b/LibMatrix/Abstractions/VersionedPublicKey.cs
@@ -0,0 +1,16 @@ +namespace LibMatrix.Abstractions; + +public class VersionedPublicKey { + public required VersionedKeyId KeyId { get; set; } + public required string PublicKey { get; set; } +} + +public class VersionedPrivateKey : VersionedPublicKey { + public required string PrivateKey { get; set; } +} +public class VersionedHomeserverPublicKey : VersionedPublicKey { + public required string ServerName { get; set; } +} +public class VersionedHomeserverPrivateKey : VersionedPrivateKey { + public required string ServerName { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index a2cb12d..9760e20 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -1,7 +1,6 @@ -using System.Text.Json.Serialization; using LibMatrix.Extensions; +using LibMatrix.Responses.Federation; using LibMatrix.Services; -using Microsoft.VisualBasic.CompilerServices; namespace LibMatrix.Homeservers; @@ -18,81 +17,7 @@ public class FederationClient { public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } public async Task<ServerVersionResponse> GetServerVersionAsync() => await HttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version"); - public async Task<ServerKeysResponse> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<ServerKeysResponse>("/_matrix/key/v2/server"); + public async Task<SignedObject<ServerKeysResponse>> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<SignedObject<ServerKeysResponse>>("/_matrix/key/v2/server"); } -public class ServerKeysResponse { - [JsonPropertyName("server_name")] - public string ServerName { get; set; } - [JsonPropertyName("valid_until_ts")] - public ulong ValidUntilTs { get; set; } - - [JsonIgnore] - public DateTime ValidUntil { - get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime; - set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); - } - - [JsonPropertyName("verify_keys")] - public Dictionary<string, CurrentVerifyKey> VerifyKeys { get; set; } = new(); - - [JsonIgnore] - public Dictionary<VersionedKeyId, CurrentVerifyKey> VerifyKeysById { - get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); - set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); - } - - [JsonPropertyName("old_verify_keys")] - public Dictionary<string, ExpiredVerifyKey> OldVerifyKeys { get; set; } = new(); - - [JsonIgnore] - public Dictionary<VersionedKeyId, ExpiredVerifyKey> OldVerifyKeysById { - get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); - set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); - } - - public class VersionedKeyId { - public required string Algorithm { get; set; } - public required string KeyId { get; set; } - - public static implicit operator VersionedKeyId(string key) { - var parts = key.Split(':', 2); - if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key)); - return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] }; - } - - public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}"; - public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId); - public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId); - } - - public class CurrentVerifyKey { - [JsonPropertyName("key")] - public string Key { get; set; } - } - - public class ExpiredVerifyKey : CurrentVerifyKey { - [JsonPropertyName("expired_ts")] - public ulong ExpiredTs { get; set; } - - [JsonIgnore] - public DateTime Expired { - get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime; - set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); - } - } -} - -public class ServerVersionResponse { - [JsonPropertyName("server")] - public required ServerInfo Server { get; set; } - - public class ServerInfo { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("version")] - public string Version { get; set; } - } -} \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index 62bb48f..23d4eb8 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj
@@ -18,8 +18,8 @@ </ItemGroup> <ItemGroup> - <!-- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250313-104848" Condition="'$(Configuration)' == 'Release'" />--> - <!-- <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(Configuration)' == 'Debug'"/>--> + <!-- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20250313-104848" Condition="'$(Configuration)' == 'Release'" />--> + <!-- <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(Configuration)' == 'Debug'"/>--> <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/> </ItemGroup> diff --git a/LibMatrix/Responses/Federation/ServerKeysResponse.cs b/LibMatrix/Responses/Federation/ServerKeysResponse.cs new file mode 100644
index 0000000..cb62e34 --- /dev/null +++ b/LibMatrix/Responses/Federation/ServerKeysResponse.cs
@@ -0,0 +1,55 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; +using LibMatrix.Abstractions; + +namespace LibMatrix.Responses.Federation; + +public class ServerKeysResponse { + [JsonPropertyName("server_name")] + public string ServerName { get; set; } + + [JsonPropertyName("valid_until_ts")] + public ulong ValidUntilTs { get; set; } + + [JsonIgnore] + public DateTime ValidUntil { + get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime; + set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + + [JsonPropertyName("verify_keys")] + public Dictionary<string, CurrentVerifyKey> VerifyKeys { get; set; } = new(); + + [JsonIgnore] + public Dictionary<VersionedKeyId, CurrentVerifyKey> VerifyKeysById { + get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); + set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); + } + + [JsonPropertyName("old_verify_keys")] + public Dictionary<string, ExpiredVerifyKey> OldVerifyKeys { get; set; } = new(); + + [JsonIgnore] + public Dictionary<VersionedKeyId, ExpiredVerifyKey> OldVerifyKeysById { + get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); + set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); + } + + [DebuggerDisplay("{Key}")] + public class CurrentVerifyKey { + [JsonPropertyName("key")] + public string Key { get; set; } + } + + [DebuggerDisplay("{Key} (expired {Expired})")] + public class ExpiredVerifyKey : CurrentVerifyKey { + [JsonPropertyName("expired_ts")] + public ulong ExpiredTs { get; set; } + + [JsonIgnore] + public DateTime Expired { + get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime; + set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + } +} diff --git a/LibMatrix/Responses/Federation/ServerVersionResponse.cs b/LibMatrix/Responses/Federation/ServerVersionResponse.cs new file mode 100644
index 0000000..b09bdd0 --- /dev/null +++ b/LibMatrix/Responses/Federation/ServerVersionResponse.cs
@@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses.Federation; + +public class ServerVersionResponse { + [JsonPropertyName("server")] + public required ServerInfo Server { get; set; } + + public class ServerInfo { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/Utilities/JsonSigning.cs b/LibMatrix/Responses/Federation/SignedObject.cs
index c727cde..3f6ffd6 100644 --- a/LibMatrix.Federation/Utilities/JsonSigning.cs +++ b/LibMatrix/Responses/Federation/SignedObject.cs
@@ -1,17 +1,11 @@ -using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; -using LibMatrix.Extensions; -using LibMatrix.FederationTest.Utilities; +using LibMatrix.Abstractions; using LibMatrix.Homeservers; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Math.EC.Rfc8032; -namespace LibMatrix.Federation.Utilities; - -public static class JsonSigning { } +namespace LibMatrix.Responses.Federation; [JsonConverter(typeof(SignedObjectConverterFactory))] public class SignedObject<T> { @@ -19,8 +13,8 @@ public class SignedObject<T> { public Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new(); [JsonIgnore] - public Dictionary<string, Dictionary<ServerKeysResponse.VersionedKeyId, string>> VerifyKeysById { - get => Signatures.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (ServerKeysResponse.VersionedKeyId)key.Key, key => key.Value)); + public Dictionary<string, Dictionary<VersionedKeyId, string>> SignaturesById { + get => Signatures.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value)); set => Signatures = value.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (string)key.Key, key => key.Value)); } @@ -34,7 +28,6 @@ public class SignedObject<T> { } } -// Content needs to be merged at toplevel public class SignedObjectConverter<T> : JsonConverter<SignedObject<T>> { public override SignedObject<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var jsonObject = JsonSerializer.Deserialize<JsonObject>(ref reader, options); @@ -72,37 +65,4 @@ internal class SignedObjectConverterFactory : JsonConverterFactory { var converter = (JsonConverter)Activator.CreateInstance(typeof(SignedObjectConverter<>).MakeGenericType(wrappedType))!; return converter; } -} - -public static class ObjectExtensions { - public static SignedObject<T> Sign<T>(this SignedObject<T> content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { - var signResult = Sign(content.Content, serverName, keyName, key); - var signedObject = new SignedObject<T> { - Signatures = content.Signatures, - Content = signResult.Content - }; - - if (!signedObject.Signatures.ContainsKey(serverName)) - signedObject.Signatures[serverName] = new Dictionary<string, string>(); - - signedObject.Signatures[serverName][keyName] = signResult.Signatures[serverName][keyName]; - return signedObject; - } - - public static SignedObject<T> Sign<T>(this T content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { - SignedObject<T> signedObject = new() { - Signatures = [], - Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(content)) ?? new JsonObject(), - }; - - var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(signedObject.Content); - var signature = new byte[Ed25519.SignatureSize]; - key.Sign(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signature, 0); - - if (!signedObject.Signatures.ContainsKey(serverName)) - signedObject.Signatures[serverName] = new Dictionary<string, string>(); - - signedObject.Signatures[serverName][keyName] = UnpaddedBase64.Encode(signature); - return signedObject; - } } \ No newline at end of file diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index 36bc828..c984c34 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -2,6 +2,7 @@ using System.Net.Http.Json; using ArcaneLibs.Collections; using LibMatrix.Homeservers; using LibMatrix.Responses; +using LibMatrix.Responses.Federation; using Microsoft.Extensions.Logging; namespace LibMatrix.Services; diff --git a/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs b/Utilities/LibMatrix.FederationTest/Controllers/RemoteServerPingController.cs new file mode 100644
index 0000000..8d3a5ea --- /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 = , + OriginServerName = null + }); + 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/FederationKeysController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/FederationKeysController.cs
index 33d0b99..6516415 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/")] @@ -32,7 +33,7 @@ public class FederationKeysController(FederationTestConfiguration config, Federa } } } - }.Sign(config.ServerName, new ServerKeysResponse.VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }, keys.privateKey); + }.Sign(config.ServerName, new VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }, keys.privateKey); } _serverKeyCacheLock.Release(); 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/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..9c0981d 100644 --- a/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs +++ b/Utilities/LibMatrix.FederationTest/Controllers/TestController.cs
@@ -1,8 +1,8 @@ using System.Text.Json.Nodes; -using ArcaneLibs.Extensions; +using LibMatrix.Abstractions; 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; @@ -21,7 +21,7 @@ public class TestController(FederationTestConfiguration config, FederationKeySto BaseAddress = new Uri("https://matrix.rory.gay") }; - var keyId = new ServerKeysResponse.VersionedKeyId() { + var keyId = new VersionedKeyId() { Algorithm = "ed25519", KeyId = "0" }; diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs
index adc809f..18d3421 100644 --- a/Utilities/LibMatrix.FederationTest/Program.cs +++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -1,10 +1,17 @@ +using System.Text.Json.Serialization; using LibMatrix.FederationTest.Services; +using LibMatrix.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllers(); +builder.Services.AddControllers() + .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 => { @@ -14,10 +21,10 @@ builder.Services.AddHttpLogging(options => { options.RequestHeaders.Add("X-Forwarded-Port"); }); +builder.Services.AddRoryLibMatrixServices(); builder.Services.AddSingleton<FederationTestConfiguration>(); builder.Services.AddSingleton<FederationKeyStore>(); - var app = builder.Build(); // Configure the HTTP request pipeline. @@ -25,10 +32,9 @@ if (true || app.Environment.IsDevelopment()) { app.MapOpenApi(); } -app.UseAuthorization(); +// app.UseAuthorization(); app.MapControllers(); // 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..e916703 100644 --- a/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs +++ b/Utilities/LibMatrix.FederationTest/Services/FederationKeyStore.cs
@@ -1,3 +1,5 @@ +using System.Text.Json; +using LibMatrix.Abstractions; using LibMatrix.FederationTest.Utilities; using Org.BouncyCastle.Crypto.Parameters; @@ -9,7 +11,28 @@ 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 VersionedHomeserverPrivateKey { + PrivateKey = keyPair.privateKey.GetEncoded().ToUnpaddedBase64(), + }; + File.WriteAllText(privateKeyPath, privateKey.ToJson()); + } + + return JsonSerializer.Deserialize<PrivateKeyCollection>() + } + + private (Ed25519PrivateKeyParameters privateKey, Ed25519PublicKeyParameters publicKey) InternalGetSigningKey() { if (currentKeyPair != default) { return currentKeyPair; } 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); } diff --git a/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs b/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
index 93e4b4f..495f9e3 100644 --- a/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs +++ b/Utilities/LibMatrix.HomeserverEmulator/Controllers/VersionsController.cs
@@ -1,6 +1,7 @@ using System.Text.Json.Serialization; using LibMatrix.Homeservers; using LibMatrix.Responses; +using LibMatrix.Responses.Federation; using Microsoft.AspNetCore.Mvc; namespace LibMatrix.HomeserverEmulator.Controllers;