about summary refs log tree commit diff
path: root/LibMatrix.Federation
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix.Federation')
-rw-r--r--LibMatrix.Federation/AuthenticatedFederationClient.cs25
-rw-r--r--LibMatrix.Federation/Extensions/Ed25519Extensions.cs3
-rw-r--r--LibMatrix.Federation/Extensions/ObjectExtensions.cs31
-rw-r--r--LibMatrix.Federation/Extensions/SignedObjectExtensions.cs37
-rw-r--r--LibMatrix.Federation/Extensions/XMatrixAuthorizationSchemeExtensions.cs29
-rw-r--r--LibMatrix.Federation/LibMatrix.Federation.csproj16
-rw-r--r--LibMatrix.Federation/Utilities/JsonSigning.cs108
-rw-r--r--LibMatrix.Federation/XMatrixAuthorizationScheme.cs11
-rw-r--r--LibMatrix.Federation/deps.json22
9 files changed, 158 insertions, 124 deletions
diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs

index 6f8d44b..95af72f 100644 --- a/LibMatrix.Federation/AuthenticatedFederationClient.cs +++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -1,20 +1,23 @@ -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 required VersionedHomeserverPrivateKey PrivateKey { get; set; } + public required string OriginServerName { get; set; } } - public AuthenticatedFederationClient(string federationEndpoint, AuthenticatedFederationConfiguration config, string? proxy = null) : base(federationEndpoint, proxy) - { - - } - + // public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) { - // var response = await GetAsync<UserDeviceListResponse>($"/_matrix/federation/v1/user/devices/{userId}", accessToken); + // 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..ada6a3d 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,6 @@ namespace LibMatrix.Federation.Extensions; public static class Ed25519Extensions { public static string ToUnpaddedBase64(this Ed25519PublicKeyParameters key) => UnpaddedBase64.Encode(key.GetEncoded()); + public static string ToUnpaddedBase64(this Ed25519PrivateKeyParameters 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..b520b1c --- /dev/null +++ b/LibMatrix.Federation/Extensions/XMatrixAuthorizationSchemeExtensions.cs
@@ -0,0 +1,29 @@ +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) + }; + + var headerValue = new XMatrixAuthorizationScheme.XMatrixAuthorizationHeader() { + Origin = requestSignature.OriginServerName, + Key = privateKey.KeyId, + Destination = requestSignature.DestinationServerName, + Signature = signature.Signatures[requestSignature.OriginServerName][privateKey.KeyId] + }.ToHeaderValue(); + requestMessage.Headers.Add("Authorization", headerValue); + + if (requestSignature.Content != null) { + requestMessage.Content = JsonContent.Create(requestSignature.Content); + } + + return requestMessage; + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/LibMatrix.Federation.csproj b/LibMatrix.Federation/LibMatrix.Federation.csproj
index 78086bb..2a9a0d8 100644 --- a/LibMatrix.Federation/LibMatrix.Federation.csproj +++ b/LibMatrix.Federation/LibMatrix.Federation.csproj
@@ -1,19 +1,27 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> + <TargetFramework>net10.0</TargetFramework> <LangVersion>preview</LangVersion> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> + <PackageId>RoryLibMatrix.Federation</PackageId> + <PackageLicenseExpression>AGPL-3.0-only</PackageLicenseExpression> + <PackageReadmeFile>README.md</PackageReadmeFile> </PropertyGroup> <ItemGroup> - <ProjectReference Include="..\LibMatrix\LibMatrix.csproj" /> + <None Include="../README.md" Pack="true" PackagePath="\"/> </ItemGroup> <ItemGroup> - <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" /> - <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.0-preview.5.25277.114" /> + <ProjectReference Include="..\LibMatrix\LibMatrix.csproj"/> + <PackageReference Include="RoryLibMatrix" Version="*-*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2"/> + <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.0"/> </ItemGroup> </Project> diff --git a/LibMatrix.Federation/Utilities/JsonSigning.cs b/LibMatrix.Federation/Utilities/JsonSigning.cs deleted file mode 100644
index c727cde..0000000 --- a/LibMatrix.Federation/Utilities/JsonSigning.cs +++ /dev/null
@@ -1,108 +0,0 @@ -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.Homeservers; -using Org.BouncyCastle.Crypto.Parameters; -using Org.BouncyCastle.Math.EC.Rfc8032; - -namespace LibMatrix.Federation.Utilities; - -public static class JsonSigning { } - -[JsonConverter(typeof(SignedObjectConverterFactory))] -public class SignedObject<T> { - [JsonPropertyName("signatures")] - 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)); - set => Signatures = value.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (string)key.Key, key => key.Value)); - } - - [JsonExtensionData] - public required JsonObject Content { get; set; } - - [JsonIgnore] - public T TypedContent { - get => Content.Deserialize<T>() ?? throw new JsonException("Failed to deserialize TypedContent from Content."); - set => Content = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value)) ?? new JsonObject(); - } -} - -// 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); - if (jsonObject == null) { - throw new JsonException("Failed to deserialize SignedObject, JSON object is null."); - } - - var signatures = jsonObject["signatures"] ?? throw new JsonException("Failed to find 'signatures' property in JSON object."); - jsonObject.Remove("signatures"); - - var signedObject = new SignedObject<T> { - Content = jsonObject, - Signatures = signatures.Deserialize<Dictionary<string, Dictionary<string, string>>>() - ?? throw new JsonException("Failed to deserialize 'signatures' property into Dictionary<string, Dictionary<string, string>>.") - }; - - return signedObject; - } - - public override void Write(Utf8JsonWriter writer, SignedObject<T> value, JsonSerializerOptions options) { - var targetObj = value.Content.DeepClone(); - targetObj["signatures"] = value.Signatures.ToJsonNode(); - JsonSerializer.Serialize(writer, targetObj, options); - } -} - -internal class SignedObjectConverterFactory : JsonConverterFactory { - public override bool CanConvert(Type typeToConvert) { - if (!typeToConvert.IsGenericType) return false; - return typeToConvert.GetGenericTypeDefinition() == typeof(SignedObject<>); - } - - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - var wrappedType = typeToConvert.GetGenericArguments()[0]; - 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.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
index fc402b7..392cd93 100644 --- a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs +++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -1,8 +1,9 @@ -using System.Net; using System.Net.Http.Headers; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using ArcaneLibs.Extensions; +using LibMatrix.Abstractions; +using LibMatrix.Responses.Federation; using Microsoft.Extensions.Primitives; namespace LibMatrix.Federation; @@ -49,6 +50,14 @@ public class XMatrixAuthorizationScheme { }; } + public static XMatrixAuthorizationHeader FromSignedObject(SignedObject<XMatrixRequestSignature> signedObj, VersionedHomeserverPrivateKey currentKey) => + new() { + Origin = signedObj.TypedContent.OriginServerName, + Destination = signedObj.TypedContent.DestinationServerName, + Signature = signedObj.Signatures[signedObj.TypedContent.OriginServerName][currentKey.KeyId], + Key = currentKey.KeyId + }; + public string ToHeaderValue() => $"{Scheme} origin=\"{Origin}\", destination=\"{Destination}\", key=\"{Key}\", sig=\"{Signature}\""; } diff --git a/LibMatrix.Federation/deps.json b/LibMatrix.Federation/deps.json new file mode 100644
index 0000000..6c90cd1 --- /dev/null +++ b/LibMatrix.Federation/deps.json
@@ -0,0 +1,22 @@ +[ + { + "pname": "BouncyCastle.Cryptography", + "version": "2.6.2", + "hash": "sha256-Yjk2+x/RcVeccGOQOQcRKCiYzyx1mlFnhS5auCII+Ms=" + }, + { + "pname": "Microsoft.Extensions.DependencyInjection.Abstractions", + "version": "10.0.0", + "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps=" + }, + { + "pname": "Microsoft.Extensions.Logging.Abstractions", + "version": "10.0.0", + "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk=" + }, + { + "pname": "Microsoft.Extensions.Primitives", + "version": "10.0.0", + "hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I=" + } +]