about summary refs log tree commit diff
path: root/LibMatrix.Federation/Utilities/JsonSigning.cs
blob: c727cde33a952b451206e771001f16c7b40f285a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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<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;
    }
}