From ae7a499e6cbefc9a99e655ab63371fb50b854c23 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Fri, 19 Jan 2024 23:58:19 +0100 Subject: [PATCH] Basic (working!) roundtrip LD signature implementation --- .../Controllers/AuthController.cs | 2 +- .../Federation/ActivityStreams/LDHelpers.cs | 9 +- .../Federation/Cryptography/DigestHelpers.cs | 12 ++ .../Federation/Cryptography/LdSignature.cs | 107 ++++++++++++++++++ .../Core/Helpers/CryptographyHelpers.cs | 11 ++ Iceshrimp.Backend/Core/Helpers/IdHelpers.cs | 4 - 6 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 Iceshrimp.Backend/Core/Federation/Cryptography/DigestHelpers.cs create mode 100644 Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs create mode 100644 Iceshrimp.Backend/Core/Helpers/CryptographyHelpers.cs diff --git a/Iceshrimp.Backend/Controllers/AuthController.cs b/Iceshrimp.Backend/Controllers/AuthController.cs index abf72c57..c5c69163 100644 --- a/Iceshrimp.Backend/Controllers/AuthController.cs +++ b/Iceshrimp.Backend/Controllers/AuthController.cs @@ -40,7 +40,7 @@ public class AuthController(DatabaseContext db) : Controller { UserId = user.Id, Active = !profile.TwoFactorEnabled, CreatedAt = new DateTime(), - Token = IdHelpers.GenerateRandomString(32) + Token = CryptographyHelpers.GenerateRandomString(32) }); var session = res.Entity; diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs index bafd151a..64feba14 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/LDHelpers.cs @@ -52,8 +52,9 @@ public static class LDHelpers { return result; } - public static JObject? Compact(object obj) => Compact(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); + public static JObject? Compact(object obj) => Compact(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); + public static string Canonicalize(JObject json) => JsonLdProcessor.Canonicalize([json]); } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/DigestHelpers.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/DigestHelpers.cs new file mode 100644 index 00000000..dd8831d9 --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/DigestHelpers.cs @@ -0,0 +1,12 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Iceshrimp.Backend.Core.Federation.Cryptography; + +public static class DigestHelpers { + public static async Task Sha256Digest(string input) { + var bytes = Encoding.UTF8.GetBytes(input); + var data = await SHA256.HashDataAsync(new MemoryStream(bytes)); + return Convert.ToHexString(data).ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs new file mode 100644 index 00000000..cdf5dbbf --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs @@ -0,0 +1,107 @@ +using System.Security.Cryptography; +using System.Text; +using Iceshrimp.Backend.Core.Federation.ActivityStreams; +using Iceshrimp.Backend.Core.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using J = Newtonsoft.Json.JsonPropertyAttribute; +using JC = Newtonsoft.Json.JsonConverterAttribute; +using VC = Iceshrimp.Backend.Core.Federation.ActivityStreams.Types.ValueObjectConverter; + +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 async Task Sign(JToken data, string key, string? creator) { + var options = new SignatureOptions { + Created = DateTime.Now, + Creator = creator, + Nonce = CryptographyHelpers.GenerateRandomHexString(16), + Type = ["_:RsaSignature2017"], + Domain = null, + }; + + var signatureData = await GetSignatureData(data, 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); + + options.Signature = signature; + + if (data is not JObject obj) throw new Exception(); + obj.Add("https://w3id.org/security#signature", JToken.FromObject(options)); + + return obj; + } + + private static Task GetSignatureData(JToken data, SignatureOptions 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; + if (options is not JArray { Count: 1 } inputOptionsArray) return null; + if (inputOptionsArray[0] is not JObject inputOptions) return null; + + inputOptions.Remove("@id"); + inputOptions.Remove("@type"); + inputOptions.Remove("https://w3id.org/security#signatureValue"); + + inputData.Remove("https://w3id.org/security#signature"); + + var canonicalData = LDHelpers.Canonicalize(inputData); + var canonicalOptions = LDHelpers.Canonicalize(inputOptions); + + var dataHash = await DigestHelpers.Sha256Digest(canonicalData); + var optionsHash = await DigestHelpers.Sha256Digest(canonicalOptions); + + return Encoding.UTF8.GetBytes(optionsHash + dataHash); + } + + private class SignatureOptions { + [J("@type")] public required List Type { get; set; } + + [J("https://w3id.org/security#signatureValue")] + [JC(typeof(VC))] + public string? Signature { get; set; } + + [J("http://purl.org/dc/terms/creator", NullValueHandling = NullValueHandling.Ignore)] + [JC(typeof(VC))] + public string? Creator { get; set; } + + [J("https://w3id.org/security#nonce", NullValueHandling = NullValueHandling.Ignore)] + [JC(typeof(VC))] + public string? Nonce { get; set; } + + [J("https://w3id.org/security#domain", NullValueHandling = NullValueHandling.Ignore)] + [JC(typeof(VC))] + public string? Domain { get; set; } + + [J("https://w3id.org/security#created", NullValueHandling = NullValueHandling.Ignore)] + [JC(typeof(VC))] + //FIXME: is this valid? it should output datetime in ISO format + public DateTime? Created { get; set; } + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/CryptographyHelpers.cs b/Iceshrimp.Backend/Core/Helpers/CryptographyHelpers.cs new file mode 100644 index 00000000..ea728223 --- /dev/null +++ b/Iceshrimp.Backend/Core/Helpers/CryptographyHelpers.cs @@ -0,0 +1,11 @@ +using System.Security.Cryptography; + +namespace Iceshrimp.Backend.Core.Helpers; + +public static class CryptographyHelpers { + public static string GenerateRandomString(int length) => + Convert.ToBase64String(RandomNumberGenerator.GetBytes(length)); + + public static string GenerateRandomHexString(int length) => + Convert.ToHexString(RandomNumberGenerator.GetBytes(length)).ToLowerInvariant(); +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/IdHelpers.cs b/Iceshrimp.Backend/Core/Helpers/IdHelpers.cs index 752e521a..6e3e863a 100644 --- a/Iceshrimp.Backend/Core/Helpers/IdHelpers.cs +++ b/Iceshrimp.Backend/Core/Helpers/IdHelpers.cs @@ -13,8 +13,4 @@ public static class IdHelpers { var timestamp = time.ToBase36().PadLeft(8, '0'); return timestamp + cuid; } - - public static string GenerateRandomString(int length) { - return Convert.ToBase64String(RandomNumberGenerator.GetBytes(length)); - } } \ No newline at end of file