From 9d70c68dbd824f10aede5111fdd021619b852703 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 20 Jan 2024 20:39:29 +0100 Subject: [PATCH] Add unit test for linked data signatures --- .../Controllers/ActivityPubController.cs | 2 +- .../ActivityPub/ActivityPubService.cs | 2 +- .../Federation/ActivityStreams/LDHelpers.cs | 3 +- .../Federation/Cryptography/LdSignature.cs | 66 ++++++++++--------- .../Cryptography/LdSignatureTests.cs | 34 ++++++++++ Iceshrimp.Tests/Cryptography/LdSignatures.cs | 9 --- Iceshrimp.Tests/Iceshrimp.Tests.csproj | 17 +++-- Iceshrimp.Tests/configuration.ini | 3 + 8 files changed, 89 insertions(+), 47 deletions(-) create mode 100644 Iceshrimp.Tests/Cryptography/LdSignatureTests.cs delete mode 100644 Iceshrimp.Tests/Cryptography/LdSignatures.cs create mode 100644 Iceshrimp.Tests/configuration.ini diff --git a/Iceshrimp.Backend/Controllers/ActivityPubController.cs b/Iceshrimp.Backend/Controllers/ActivityPubController.cs index c8dd201f..e990eda7 100644 --- a/Iceshrimp.Backend/Controllers/ActivityPubController.cs +++ b/Iceshrimp.Backend/Controllers/ActivityPubController.cs @@ -32,7 +32,7 @@ public class ActivityPubController(ILogger logger, Databa var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id); if (user == null) return NotFound(); var rendered = await userRenderer.Render(user); - var compacted = LDHelpers.Compact(rendered); + var compacted = LdHelpers.Compact(rendered); return Ok(compacted); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs index 72c3258b..a95db114 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs @@ -18,7 +18,7 @@ public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc) var input = await response.Content.ReadAsStringAsync(); var json = JsonConvert.DeserializeObject(input, JsonSerializerSettings); - var res = LDHelpers.Expand(json); + var res = LdHelpers.Expand(json); if (res == null) throw new Exception("Failed to expand JSON-LD object"); return res; } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs index 64feba14..06e92178 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs @@ -10,7 +10,7 @@ using VDS.RDF.Writing.Formatting; namespace Iceshrimp.Backend.Core.Federation.ActivityStreams; -public static class LDHelpers { +public static class LdHelpers { private static readonly Dictionary ContextCache = new() { { "https://www.w3.org/ns/activitystreams", new RemoteDocument { @@ -53,6 +53,7 @@ public static class LDHelpers { } public static JObject? Compact(object obj) => Compact(JToken.FromObject(obj)); + public static JArray? Expand(object obj) => Expand(JToken.FromObject(obj)); public static JObject? Compact(JToken? json) => JsonLdProcessor.Compact(json, DefaultContext, Options); public static JArray? Expand(JToken? json) => JsonLdProcessor.Expand(json, Options); public static string Canonicalize(JArray json) => JsonLdProcessor.Canonicalize(json); diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs index cdf5dbbf..3fe8e0dd 100644 --- a/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs @@ -11,28 +11,33 @@ using VC = Iceshrimp.Backend.Core.Federation.ActivityStreams.Types.ValueObjectCo namespace Iceshrimp.Backend.Core.Federation.Cryptography; public static class LdSignature { - public static async Task Verify(JArray activity, string key) { - foreach (var act in activity) { - var options = act["https://w3id.org/security#signature"]; - if (options?.ToObject() is not { Length: 1 } signatures) return false; - var signature = signatures[0]; - if (signature.Type is not ["_:RsaSignature2017"]) return false; - if (signature.Signature is null) return false; - - var signatureData = await GetSignatureData(act, options); - if (signatureData is null) return false; - - var rsa = RSA.Create(); - rsa.ImportFromPem(key); - var verify = rsa.VerifyData(signatureData, Convert.FromBase64String(signature.Signature), - HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - if (!verify) return false; - } - - return true; + public static Task Verify(JArray activity, string key) { + if (activity.ToArray() is not [JObject obj]) throw new Exception("Invalid activity"); + return Verify(obj, key); } - public static async Task Sign(JToken data, string key, string? creator) { + public static async Task Verify(JObject activity, string key) { + var options = activity["https://w3id.org/security#signature"]; + if (options?.ToObject() is not { Length: 1 } signatures) return false; + var signature = signatures[0]; + if (signature.Type is not ["_:RsaSignature2017"]) return false; + if (signature.Signature is null) return false; + + var signatureData = await GetSignatureData(activity, options); + if (signatureData is null) return false; + + var rsa = RSA.Create(); + rsa.ImportFromPem(key); + return rsa.VerifyData(signatureData, Convert.FromBase64String(signature.Signature), + HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + public static Task Sign(JArray activity, string key, string? creator) { + if (activity.ToArray() is not [JObject obj]) throw new Exception("Invalid activity"); + return Sign(obj, key, creator); + } + + public static async Task Sign(JObject activity, string key, string? creator) { var options = new SignatureOptions { Created = DateTime.Now, Creator = creator, @@ -41,24 +46,23 @@ public static class LdSignature { Domain = null, }; - var signatureData = await GetSignatureData(data, options); + var signatureData = await GetSignatureData(activity, options); if (signatureData == null) throw new NullReferenceException("Signature data must not be null"); var rsa = RSA.Create(); rsa.ImportFromPem(key); var signatureBytes = rsa.SignData(signatureData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - var signature = Convert.ToBase64String(signatureBytes); + var signature = Convert.ToBase64String(signatureBytes); options.Signature = signature; - - if (data is not JObject obj) throw new Exception(); - obj.Add("https://w3id.org/security#signature", JToken.FromObject(options)); - - return obj; + + activity.Add("https://w3id.org/security#signature", JToken.FromObject(options)); + + return LdHelpers.Expand(activity)?[0] as JObject ?? throw new Exception("Failed to expand signed activity"); } private static Task GetSignatureData(JToken data, SignatureOptions options) => - GetSignatureData(data, LDHelpers.Expand(JObject.FromObject(options))!); + GetSignatureData(data, LdHelpers.Expand(JObject.FromObject(options))!); private static async Task GetSignatureData(JToken data, JToken options) { if (data is not JObject inputData) return null; @@ -71,8 +75,8 @@ public static class LdSignature { inputData.Remove("https://w3id.org/security#signature"); - var canonicalData = LDHelpers.Canonicalize(inputData); - var canonicalOptions = LDHelpers.Canonicalize(inputOptions); + var canonicalData = LdHelpers.Canonicalize(inputData); + var canonicalOptions = LdHelpers.Canonicalize(inputOptions); var dataHash = await DigestHelpers.Sha256Digest(canonicalData); var optionsHash = await DigestHelpers.Sha256Digest(canonicalOptions); @@ -81,7 +85,7 @@ public static class LdSignature { } private class SignatureOptions { - [J("@type")] public required List Type { get; set; } + [J("@type")] public required List Type { get; set; } [J("https://w3id.org/security#signatureValue")] [JC(typeof(VC))] diff --git a/Iceshrimp.Tests/Cryptography/LdSignatureTests.cs b/Iceshrimp.Tests/Cryptography/LdSignatureTests.cs new file mode 100644 index 00000000..3f8d1317 --- /dev/null +++ b/Iceshrimp.Tests/Cryptography/LdSignatureTests.cs @@ -0,0 +1,34 @@ +using System.Security.Cryptography; +using FluentAssertions; +using Iceshrimp.Backend.Core.Federation.ActivityStreams; +using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; +using Iceshrimp.Backend.Core.Federation.Cryptography; +using Iceshrimp.Backend.Core.Helpers; + +namespace Iceshrimp.Tests.Cryptography; + +[TestClass] +public class LdSignatureTests { + [TestMethod] + public async Task RoundtripTest() { + var keypair = RSA.Create(); + + var actor = new ASActor { + Id = $"https://example.org/users/{IdHelpers.GenerateSlowflakeId()}", + Type = ["https://www.w3.org/ns/activitystreams#Person"], + Url = new ASLink($"https://example.org/@test"), + Username = "test", + DisplayName = "Test account", + IsCat = false, + IsDiscoverable = true, + IsLocked = true + }; + + var expanded = LdHelpers.Expand(actor); + + var signed = await LdSignature.Sign(expanded!, keypair.ExportRSAPrivateKeyPem(), actor.Id + "#main-key"); + var verify = await LdSignature.Verify(signed, keypair.ExportRSAPublicKeyPem()); + + verify.Should().BeTrue(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Tests/Cryptography/LdSignatures.cs b/Iceshrimp.Tests/Cryptography/LdSignatures.cs deleted file mode 100644 index 06330bbe..00000000 --- a/Iceshrimp.Tests/Cryptography/LdSignatures.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Iceshrimp.Tests.Cryptography; - -[TestClass] -public class LdSignatures { - [TestMethod] - public void TestMethod1() { - - } -} \ No newline at end of file diff --git a/Iceshrimp.Tests/Iceshrimp.Tests.csproj b/Iceshrimp.Tests/Iceshrimp.Tests.csproj index 9884fad1..7278a261 100644 --- a/Iceshrimp.Tests/Iceshrimp.Tests.csproj +++ b/Iceshrimp.Tests/Iceshrimp.Tests.csproj @@ -10,10 +10,19 @@ - - - - + + + + + + + + + + + + + diff --git a/Iceshrimp.Tests/configuration.ini b/Iceshrimp.Tests/configuration.ini new file mode 100644 index 00000000..3d635e05 --- /dev/null +++ b/Iceshrimp.Tests/configuration.ini @@ -0,0 +1,3 @@ +[Instance] +WebDomain=shrimp.example.org +AccountDomain=example.org \ No newline at end of file