Basic (working!) roundtrip LD signature implementation
This commit is contained in:
parent
16f7f89802
commit
ae7a499e6c
6 changed files with 136 additions and 9 deletions
|
@ -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;
|
||||
|
|
|
@ -56,4 +56,5 @@ public static class LDHelpers {
|
|||
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]);
|
||||
}
|
|
@ -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<string> Sha256Digest(string input) {
|
||||
var bytes = Encoding.UTF8.GetBytes(input);
|
||||
var data = await SHA256.HashDataAsync(new MemoryStream(bytes));
|
||||
return Convert.ToHexString(data).ToLowerInvariant();
|
||||
}
|
||||
}
|
107
Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs
Normal file
107
Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs
Normal file
|
@ -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<bool> Verify(JArray activity, string key) {
|
||||
foreach (var act in activity) {
|
||||
var options = act["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(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) {
|
||||
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<byte[]?> GetSignatureData(JToken data, SignatureOptions options) =>
|
||||
GetSignatureData(data, LDHelpers.Expand(JObject.FromObject(options))!);
|
||||
|
||||
private static async Task<byte[]?> 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<string> 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; }
|
||||
}
|
||||
}
|
11
Iceshrimp.Backend/Core/Helpers/CryptographyHelpers.cs
Normal file
11
Iceshrimp.Backend/Core/Helpers/CryptographyHelpers.cs
Normal file
|
@ -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();
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue