diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs
new file mode 100644
index 0000000..ee4bb25
--- /dev/null
+++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -0,0 +1,24 @@
+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..e5a9e5d
--- /dev/null
+++ b/LibMatrix.Federation/Extensions/Ed25519Extensions.cs
@@ -0,0 +1,10 @@
+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 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/LibMatrix.Federation.csproj b/LibMatrix.Federation/LibMatrix.Federation.csproj
new file mode 100644
index 0000000..af09d85
--- /dev/null
+++ b/LibMatrix.Federation/LibMatrix.Federation.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net9.0</TargetFramework>
+ <LangVersion>preview</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\LibMatrix\LibMatrix.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
+ <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.0-preview.5.25277.114" />
+ </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..45899b8
--- /dev/null
+++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -0,0 +1,70 @@
+using System.Net.Http.Headers;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+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 StringValues(header.Parameter);
+ foreach (var value in headerValues) {
+ Console.WriteLine(headerValues.ToJson());
+ }
+
+ return new() {
+ Destination = "",
+ Key = "",
+ Origin = "",
+ Signature = ""
+ };
+ }
+
+ 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.WhenWritingDefault)]
+ public JsonObject? Content { get; set; }
+ }
+}
\ No newline at end of file
|