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="
+ }
+]
|