Add unit test for linked data signatures
This commit is contained in:
parent
2c9e40f63e
commit
9d70c68dbd
8 changed files with 89 additions and 47 deletions
|
@ -32,7 +32,7 @@ public class ActivityPubController(ILogger<ActivityPubController> logger, Databa
|
||||||
var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id);
|
var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
if (user == null) return NotFound();
|
if (user == null) return NotFound();
|
||||||
var rendered = await userRenderer.Render(user);
|
var rendered = await userRenderer.Render(user);
|
||||||
var compacted = LDHelpers.Compact(rendered);
|
var compacted = LdHelpers.Compact(rendered);
|
||||||
return Ok(compacted);
|
return Ok(compacted);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,7 +18,7 @@ public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc)
|
||||||
var input = await response.Content.ReadAsStringAsync();
|
var input = await response.Content.ReadAsStringAsync();
|
||||||
var json = JsonConvert.DeserializeObject<JObject?>(input, JsonSerializerSettings);
|
var json = JsonConvert.DeserializeObject<JObject?>(input, JsonSerializerSettings);
|
||||||
|
|
||||||
var res = LDHelpers.Expand(json);
|
var res = LdHelpers.Expand(json);
|
||||||
if (res == null) throw new Exception("Failed to expand JSON-LD object");
|
if (res == null) throw new Exception("Failed to expand JSON-LD object");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ using VDS.RDF.Writing.Formatting;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
||||||
|
|
||||||
public static class LDHelpers {
|
public static class LdHelpers {
|
||||||
private static readonly Dictionary<string, RemoteDocument> ContextCache = new() {
|
private static readonly Dictionary<string, RemoteDocument> ContextCache = new() {
|
||||||
{
|
{
|
||||||
"https://www.w3.org/ns/activitystreams", new RemoteDocument {
|
"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 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 JObject? Compact(JToken? json) => JsonLdProcessor.Compact(json, DefaultContext, Options);
|
||||||
public static JArray? Expand(JToken? json) => JsonLdProcessor.Expand(json, Options);
|
public static JArray? Expand(JToken? json) => JsonLdProcessor.Expand(json, Options);
|
||||||
public static string Canonicalize(JArray json) => JsonLdProcessor.Canonicalize(json);
|
public static string Canonicalize(JArray json) => JsonLdProcessor.Canonicalize(json);
|
||||||
|
|
|
@ -11,28 +11,33 @@ using VC = Iceshrimp.Backend.Core.Federation.ActivityStreams.Types.ValueObjectCo
|
||||||
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||||
|
|
||||||
public static class LdSignature {
|
public static class LdSignature {
|
||||||
public static async Task<bool> Verify(JArray activity, string key) {
|
public static Task<bool> Verify(JArray activity, string key) {
|
||||||
foreach (var act in activity) {
|
if (activity.ToArray() is not [JObject obj]) throw new Exception("Invalid activity");
|
||||||
var options = act["https://w3id.org/security#signature"];
|
return Verify(obj, key);
|
||||||
if (options?.ToObject<SignatureOptions[]>() 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 async Task<JToken> Sign(JToken data, string key, string? creator) {
|
public static async Task<bool> Verify(JObject activity, string key) {
|
||||||
|
var options = activity["https://w3id.org/security#signature"];
|
||||||
|
if (options?.ToObject<SignatureOptions[]>() 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<JObject> 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<JObject> Sign(JObject activity, string key, string? creator) {
|
||||||
var options = new SignatureOptions {
|
var options = new SignatureOptions {
|
||||||
Created = DateTime.Now,
|
Created = DateTime.Now,
|
||||||
Creator = creator,
|
Creator = creator,
|
||||||
|
@ -41,24 +46,23 @@ public static class LdSignature {
|
||||||
Domain = null,
|
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");
|
if (signatureData == null) throw new NullReferenceException("Signature data must not be null");
|
||||||
|
|
||||||
var rsa = RSA.Create();
|
var rsa = RSA.Create();
|
||||||
rsa.ImportFromPem(key);
|
rsa.ImportFromPem(key);
|
||||||
var signatureBytes = rsa.SignData(signatureData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
var signatureBytes = rsa.SignData(signatureData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||||
var signature = Convert.ToBase64String(signatureBytes);
|
var signature = Convert.ToBase64String(signatureBytes);
|
||||||
|
|
||||||
options.Signature = signature;
|
options.Signature = signature;
|
||||||
|
|
||||||
if (data is not JObject obj) throw new Exception();
|
activity.Add("https://w3id.org/security#signature", JToken.FromObject(options));
|
||||||
obj.Add("https://w3id.org/security#signature", JToken.FromObject(options));
|
|
||||||
|
|
||||||
return obj;
|
return LdHelpers.Expand(activity)?[0] as JObject ?? throw new Exception("Failed to expand signed activity");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<byte[]?> GetSignatureData(JToken data, SignatureOptions options) =>
|
private static Task<byte[]?> GetSignatureData(JToken data, SignatureOptions options) =>
|
||||||
GetSignatureData(data, LDHelpers.Expand(JObject.FromObject(options))!);
|
GetSignatureData(data, LdHelpers.Expand(JObject.FromObject(options))!);
|
||||||
|
|
||||||
private static async Task<byte[]?> GetSignatureData(JToken data, JToken options) {
|
private static async Task<byte[]?> GetSignatureData(JToken data, JToken options) {
|
||||||
if (data is not JObject inputData) return null;
|
if (data is not JObject inputData) return null;
|
||||||
|
@ -71,8 +75,8 @@ public static class LdSignature {
|
||||||
|
|
||||||
inputData.Remove("https://w3id.org/security#signature");
|
inputData.Remove("https://w3id.org/security#signature");
|
||||||
|
|
||||||
var canonicalData = LDHelpers.Canonicalize(inputData);
|
var canonicalData = LdHelpers.Canonicalize(inputData);
|
||||||
var canonicalOptions = LDHelpers.Canonicalize(inputOptions);
|
var canonicalOptions = LdHelpers.Canonicalize(inputOptions);
|
||||||
|
|
||||||
var dataHash = await DigestHelpers.Sha256Digest(canonicalData);
|
var dataHash = await DigestHelpers.Sha256Digest(canonicalData);
|
||||||
var optionsHash = await DigestHelpers.Sha256Digest(canonicalOptions);
|
var optionsHash = await DigestHelpers.Sha256Digest(canonicalOptions);
|
||||||
|
@ -81,7 +85,7 @@ public static class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SignatureOptions {
|
private class SignatureOptions {
|
||||||
[J("@type")] public required List<string> Type { get; set; }
|
[J("@type")] public required List<string> Type { get; set; }
|
||||||
|
|
||||||
[J("https://w3id.org/security#signatureValue")]
|
[J("https://w3id.org/security#signatureValue")]
|
||||||
[JC(typeof(VC))]
|
[JC(typeof(VC))]
|
||||||
|
|
34
Iceshrimp.Tests/Cryptography/LdSignatureTests.cs
Normal file
34
Iceshrimp.Tests/Cryptography/LdSignatureTests.cs
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
namespace Iceshrimp.Tests.Cryptography;
|
|
||||||
|
|
||||||
[TestClass]
|
|
||||||
public class LdSignatures {
|
|
||||||
[TestMethod]
|
|
||||||
public void TestMethod1() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -10,10 +10,19 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.4"/>
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.4"/>
|
<PackageReference Include="MSTest.TestAdapter" Version="3.0.4" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
<PackageReference Include="MSTest.TestFramework" Version="3.0.4" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Iceshrimp.Backend\Iceshrimp.Backend.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="configuration.ini" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
3
Iceshrimp.Tests/configuration.ini
Normal file
3
Iceshrimp.Tests/configuration.ini
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[Instance]
|
||||||
|
WebDomain=shrimp.example.org
|
||||||
|
AccountDomain=example.org
|
Loading…
Add table
Reference in a new issue