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.cs23
-rw-r--r--LibMatrix.Federation/Extensions/Ed25519Extensions.cs11
-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/FederationTypes/FederationBackfillResponse.cs14
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationEvent.cs30
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs34
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationTransaction.cs26
-rw-r--r--LibMatrix.Federation/FederationTypes/RoomInvite.cs14
-rw-r--r--LibMatrix.Federation/LibMatrix.Federation.csproj27
-rw-r--r--LibMatrix.Federation/Utilities/UnpaddedBase64.cs17
-rw-r--r--LibMatrix.Federation/XMatrixAuthorizationScheme.cs91
-rw-r--r--LibMatrix.Federation/deps.json22
14 files changed, 406 insertions, 0 deletions
diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs
new file mode 100644

index 0000000..95af72f --- /dev/null +++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -0,0 +1,23 @@ +using LibMatrix.Abstractions; +using LibMatrix.Federation.Extensions; +using LibMatrix.Homeservers; + +namespace LibMatrix.Federation; + +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 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 new file mode 100644
index 0000000..ada6a3d --- /dev/null +++ b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
@@ -0,0 +1,11 @@ +using LibMatrix.Abstractions; +using LibMatrix.FederationTest.Utilities; +using Org.BouncyCastle.Crypto.Parameters; + +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/FederationTypes/FederationBackfillResponse.cs b/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs new file mode 100644
index 0000000..0fe72bd --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class FederationBackfillResponse { + [JsonPropertyName("origin")] + public required string Origin { get; set; } + + [JsonPropertyName("origin_server_ts")] + public required long OriginServerTs { get; set; } + + [JsonPropertyName("pdus")] + public required List<SignedFederationEvent> Pdus { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationEvent.cs b/LibMatrix.Federation/FederationTypes/FederationEvent.cs new file mode 100644
index 0000000..05bdcc9 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationEvent.cs
@@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class FederationEvent : MatrixEventResponse { + [JsonPropertyName("auth_events")] + public required List<string> AuthEvents { get; set; } = []; + + [JsonPropertyName("prev_events")] + public required List<string> PrevEvents { get; set; } = []; + + [JsonPropertyName("depth")] + public required int Depth { get; set; } +} + +public class SignedFederationEvent : FederationEvent { + [JsonPropertyName("signatures")] + public required Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new(); + + [JsonPropertyName("hashes")] + public required Dictionary<string, string> Hashes { get; set; } = new(); +} + +public class FederationEphemeralEvent { + [JsonPropertyName("edu_type")] + public required string Type { get; set; } + + [JsonPropertyName("content")] + public required Dictionary<string, object> Content { get; set; } = new(); +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs b/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs new file mode 100644
index 0000000..f43dd49 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs
@@ -0,0 +1,34 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class FederationGetMissingEventsRequest { + /// <summary> + /// Latest event IDs we already have (aka earliest to return) + /// </summary> + [JsonPropertyName("earliest_events")] + public required List<string> EarliestEvents { get; set; } + + /// <summary> + /// Events we want to get events before + /// </summary> + [JsonPropertyName("latest_events")] + public required List<string> LatestEvents { get; set; } + + /// <summary> + /// 10 by default + /// </summary> + [JsonPropertyName("limit")] + public int Limit { get; set; } + + /// <summary> + /// 0 by default + /// </summary> + [JsonPropertyName("min_depth")] + public long MinDepth { get; set; } +} + +public class FederationGetMissingEventsResponse { + [JsonPropertyName("events")] + public required List<SignedFederationEvent> Events { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/FederationTransaction.cs b/LibMatrix.Federation/FederationTypes/FederationTransaction.cs new file mode 100644
index 0000000..0581a08 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/FederationTransaction.cs
@@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +/// <summary> +/// This only covers v12 rooms for now? +/// </summary> +public class FederationTransaction { + /// <summary> + /// Up to 100 EDUs per transaction + /// </summary> + [JsonPropertyName("edus")] + public List<FederationEvent>? EphemeralEvents { get; set; } + + [JsonPropertyName("origin")] + public required string Origin { get; set; } + + [JsonPropertyName("origin_server_ts")] + public required long OriginServerTs { get; set; } + + /// <summary> + /// Up to 50 PDUs per transaction + /// </summary> + [JsonPropertyName("pdus")] + public List<SignedFederationEvent>? PersistentEvents { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/FederationTypes/RoomInvite.cs b/LibMatrix.Federation/FederationTypes/RoomInvite.cs new file mode 100644
index 0000000..dc550f3 --- /dev/null +++ b/LibMatrix.Federation/FederationTypes/RoomInvite.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Federation.FederationTypes; + +public class RoomInvite { + [JsonPropertyName("event")] + public required SignedFederationEvent Event { get; set; } + + [JsonPropertyName("invite_room_state")] + public required List<MatrixEventResponse> InviteRoomState { get; set; } = []; + + [JsonPropertyName("room_version")] + public required string RoomVersion { get; set; } +} \ No newline at end of file diff --git a/LibMatrix.Federation/LibMatrix.Federation.csproj b/LibMatrix.Federation/LibMatrix.Federation.csproj new file mode 100644
index 0000000..2a9a0d8 --- /dev/null +++ b/LibMatrix.Federation/LibMatrix.Federation.csproj
@@ -0,0 +1,27 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <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> + <None Include="../README.md" Pack="true" PackagePath="\"/> + </ItemGroup> + + <ItemGroup> + <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/UnpaddedBase64.cs b/LibMatrix.Federation/Utilities/UnpaddedBase64.cs new file mode 100644
index 0000000..06f84b2 --- /dev/null +++ b/LibMatrix.Federation/Utilities/UnpaddedBase64.cs
@@ -0,0 +1,17 @@ +namespace LibMatrix.FederationTest.Utilities; + +public static class UnpaddedBase64 { + public static string Encode(byte[] data) { + return Convert.ToBase64String(data).TrimEnd('='); + } + + public static byte[] Decode(string base64) { + string paddedBase64 = base64; + switch (paddedBase64.Length % 4) { + case 2: paddedBase64 += "=="; break; + case 3: paddedBase64 += "="; break; + } + + return Convert.FromBase64String(paddedBase64); + } +} \ No newline at end of file diff --git a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs new file mode 100644
index 0000000..c6be906 --- /dev/null +++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -0,0 +1,91 @@ +using System.Net.Http.Headers; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; +using LibMatrix.Abstractions; +using LibMatrix.Extensions; +using LibMatrix.Responses.Federation; +using Microsoft.Extensions.Primitives; + +namespace LibMatrix.Federation; + +public class XMatrixAuthorizationScheme { + public class XMatrixAuthorizationHeader { + public const string Scheme = "X-Matrix"; + + [JsonPropertyName("origin")] + public required string Origin { get; set; } + + [JsonPropertyName("destination")] + public required string Destination { get; set; } + + [JsonPropertyName("key")] + public required string Key { get; set; } + + [JsonPropertyName("sig")] + public required string Signature { get; set; } + + public static XMatrixAuthorizationHeader FromHeaderValue(AuthenticationHeaderValue header) { + if (header.Scheme != Scheme) + throw new LibMatrixException() { + Error = $"Expected authentication scheme of {Scheme}, got {header.Scheme}", + ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED + }; + + if (string.IsNullOrWhiteSpace(header.Parameter)) + throw new LibMatrixException() { + Error = $"Expected authentication header to have a value.", + ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED + }; + + var headerValues = new Dictionary<string, string>(); + var parts = header.Parameter.Split(','); + foreach (var part in parts) { + var kv = part.Split('=', 2); + if (kv.Length != 2) + continue; + var key = kv[0].Trim(); + var value = kv[1].Trim().Trim('"'); + headerValues[key] = value; + } + + Console.WriteLine("X-Matrix parts: " + headerValues.ToJson(unsafeContent: true)); + + var xma = new XMatrixAuthorizationHeader() { + Destination = headerValues["destination"], + Key = headerValues["key"], + Origin = headerValues["origin"], + Signature = headerValues["sig"] + }; + Console.WriteLine("Parsed X-Matrix Auth Header: " + xma.ToJson()); + return xma; + } + + 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}\""; + } + + public class XMatrixRequestSignature { + [JsonPropertyName("method")] + public required string Method { get; set; } + + [JsonPropertyName("uri")] + public required string Uri { get; set; } + + [JsonPropertyName("origin")] + public required string OriginServerName { get; set; } + + [JsonPropertyName("destination")] + public required string DestinationServerName { get; set; } + + [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonObject? Content { get; set; } + } +} \ No newline at end of file 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=" + } +]