From 6975119b7d21cafdd0620d35b9542fb5d47ef392 Mon Sep 17 00:00:00 2001 From: Rory& Date: Fri, 20 Jun 2025 04:50:00 +0200 Subject: Basic federation, move some response classes to the right namespace --- .../Extensions/Ed25519Extensions.cs | 8 ++ LibMatrix.Federation/Utilities/JsonSigning.cs | 108 +++++++++++++++++++++ LibMatrix.Federation/Utilities/UnpaddedBase64.cs | 17 ++++ LibMatrix.Federation/XMatrixAuthorizationScheme.cs | 9 +- 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 LibMatrix.Federation/Extensions/Ed25519Extensions.cs create mode 100644 LibMatrix.Federation/Utilities/JsonSigning.cs create mode 100644 LibMatrix.Federation/Utilities/UnpaddedBase64.cs (limited to 'LibMatrix.Federation') diff --git a/LibMatrix.Federation/Extensions/Ed25519Extensions.cs b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs new file mode 100644 index 0000000..69baf58 --- /dev/null +++ b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs @@ -0,0 +1,8 @@ +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()); +} \ No newline at end of file diff --git a/LibMatrix.Federation/Utilities/JsonSigning.cs b/LibMatrix.Federation/Utilities/JsonSigning.cs new file mode 100644 index 0000000..c727cde --- /dev/null +++ b/LibMatrix.Federation/Utilities/JsonSigning.cs @@ -0,0 +1,108 @@ +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 { + [JsonPropertyName("signatures")] + public Dictionary> Signatures { get; set; } = new(); + + [JsonIgnore] + public Dictionary> 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() ?? throw new JsonException("Failed to deserialize TypedContent from Content."); + set => Content = JsonSerializer.Deserialize(JsonSerializer.Serialize(value)) ?? new JsonObject(); + } +} + +// Content needs to be merged at toplevel +public class SignedObjectConverter : JsonConverter> { + public override SignedObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var jsonObject = JsonSerializer.Deserialize(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 { + Content = jsonObject, + Signatures = signatures.Deserialize>>() + ?? throw new JsonException("Failed to deserialize 'signatures' property into Dictionary>.") + }; + + return signedObject; + } + + public override void Write(Utf8JsonWriter writer, SignedObject 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 Sign(this SignedObject content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { + var signResult = Sign(content.Content, serverName, keyName, key); + var signedObject = new SignedObject { + Signatures = content.Signatures, + Content = signResult.Content + }; + + if (!signedObject.Signatures.ContainsKey(serverName)) + signedObject.Signatures[serverName] = new Dictionary(); + + signedObject.Signatures[serverName][keyName] = signResult.Signatures[serverName][keyName]; + return signedObject; + } + + public static SignedObject Sign(this T content, string serverName, string keyName, Ed25519PrivateKeyParameters key) { + SignedObject signedObject = new() { + Signatures = [], + Content = JsonSerializer.Deserialize(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(); + + signedObject.Signatures[serverName][keyName] = UnpaddedBase64.Encode(signature); + return signedObject; + } +} \ No newline at end of file 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 index cb349c9..5025434 100644 --- a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs +++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs @@ -40,8 +40,15 @@ public class XMatrixAuthorizationScheme { { Console.WriteLine(headerValues.ToJson()); } + + return new() { + Destination = "", + Key = "", + Origin = "", + Signature = "" + }; } - public string ToHeaderValue() { } + public string ToHeaderValue() => $"{Scheme} origin=\"{Origin}\", destination=\"{Destination}\", key=\"{Key}\", sig=\"{Signature}\""; } } \ No newline at end of file -- cgit 1.5.1