using Microsoft.IdentityModel.Tokens; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; using StellaOps.Cryptography; using System.Collections.Concurrent; namespace StellaOps.Cryptography.Plugin.BouncyCastle; /// /// Ed25519 signing provider backed by BouncyCastle primitives. /// public sealed class BouncyCastleEd25519CryptoProvider : ICryptoProvider { private static readonly HashSet SupportedAlgorithms = new(StringComparer.OrdinalIgnoreCase) { SignatureAlgorithms.Ed25519, SignatureAlgorithms.EdDsa }; private static readonly string[] DefaultKeyOps = { "sign", "verify" }; private readonly ConcurrentDictionary signingKeys = new(StringComparer.Ordinal); public string Name => "bouncycastle.ed25519"; public bool Supports(CryptoCapability capability, string algorithmId) { if (string.IsNullOrWhiteSpace(algorithmId)) { return false; } return capability switch { CryptoCapability.Signing or CryptoCapability.Verification => SupportedAlgorithms.Contains(algorithmId), _ => false }; } public ICryptoHasher GetHasher(string algorithmId) => throw new NotSupportedException("BouncyCastle Ed25519 provider does not expose hashing capabilities."); public IPasswordHasher GetPasswordHasher(string algorithmId) => throw new NotSupportedException("BouncyCastle provider does not expose password hashing capabilities."); public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference) { ArgumentException.ThrowIfNullOrWhiteSpace(algorithmId); ArgumentNullException.ThrowIfNull(keyReference); if (!signingKeys.TryGetValue(keyReference.KeyId, out var entry)) { throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'."); } EnsureAlgorithmSupported(algorithmId); var normalized = NormalizeAlgorithm(algorithmId); if (!string.Equals(entry.Descriptor.AlgorithmId, normalized, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException( $"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.Descriptor.AlgorithmId}', not '{algorithmId}'."); } return new Ed25519SignerWrapper(entry); } public void UpsertSigningKey(CryptoSigningKey signingKey) { ArgumentNullException.ThrowIfNull(signingKey); EnsureAlgorithmSupported(signingKey.AlgorithmId); if (signingKey.Kind != CryptoSigningKeyKind.Raw) { throw new InvalidOperationException($"Provider '{Name}' requires raw Ed25519 private key material."); } var privateKey = NormalizePrivateKey(signingKey.PrivateKey); var publicKey = NormalizePublicKey(signingKey.PublicKey, privateKey); var privateKeyParameters = new Ed25519PrivateKeyParameters(privateKey, 0); var publicKeyParameters = new Ed25519PublicKeyParameters(publicKey, 0); var descriptor = new CryptoSigningKey( signingKey.Reference, NormalizeAlgorithm(signingKey.AlgorithmId), privateKey, signingKey.CreatedAt, signingKey.ExpiresAt, publicKey, signingKey.Metadata); signingKeys.AddOrUpdate( signingKey.Reference.KeyId, _ => new KeyEntry(descriptor, privateKeyParameters, publicKeyParameters), (_, _) => new KeyEntry(descriptor, privateKeyParameters, publicKeyParameters)); } public bool RemoveSigningKey(string keyId) { if (string.IsNullOrWhiteSpace(keyId)) { return false; } return signingKeys.TryRemove(keyId, out _); } public IReadOnlyCollection GetSigningKeys() => signingKeys.Values.Select(static entry => entry.Descriptor).ToArray(); private static void EnsureAlgorithmSupported(string algorithmId) { if (!SupportedAlgorithms.Contains(algorithmId)) { throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider 'bouncycastle.ed25519'."); } } private static string NormalizeAlgorithm(string algorithmId) => string.Equals(algorithmId, SignatureAlgorithms.EdDsa, StringComparison.OrdinalIgnoreCase) ? SignatureAlgorithms.Ed25519 : SignatureAlgorithms.Ed25519; private static byte[] NormalizePrivateKey(ReadOnlyMemory privateKey) { var span = privateKey.Span; return span.Length switch { 32 => span.ToArray(), 64 => span[..32].ToArray(), _ => throw new InvalidOperationException("Ed25519 private key must be 32 or 64 bytes.") }; } private static byte[] NormalizePublicKey(ReadOnlyMemory publicKey, byte[] privateKey) { if (publicKey.IsEmpty) { var privateParams = new Ed25519PrivateKeyParameters(privateKey, 0); return privateParams.GeneratePublicKey().GetEncoded(); } if (publicKey.Span.Length != 32) { throw new InvalidOperationException("Ed25519 public key must be 32 bytes."); } return publicKey.ToArray(); } private sealed record KeyEntry( CryptoSigningKey Descriptor, Ed25519PrivateKeyParameters PrivateKey, Ed25519PublicKeyParameters PublicKey); private sealed class Ed25519SignerWrapper : ICryptoSigner { private readonly KeyEntry entry; public Ed25519SignerWrapper(KeyEntry entry) { this.entry = entry ?? throw new ArgumentNullException(nameof(entry)); } public string KeyId => entry.Descriptor.Reference.KeyId; public string AlgorithmId => entry.Descriptor.AlgorithmId; public ValueTask SignAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var signer = new Ed25519Signer(); var buffer = data.ToArray(); signer.Init(true, entry.PrivateKey); signer.BlockUpdate(buffer, 0, buffer.Length); var signature = signer.GenerateSignature(); return ValueTask.FromResult(signature); } public ValueTask VerifyAsync(ReadOnlyMemory data, ReadOnlyMemory signature, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var verifier = new Ed25519Signer(); var buffer = data.ToArray(); verifier.Init(false, entry.PublicKey); verifier.BlockUpdate(buffer, 0, buffer.Length); var verified = verifier.VerifySignature(signature.ToArray()); return ValueTask.FromResult(verified); } public JsonWebKey ExportPublicJsonWebKey() { var jwk = new JsonWebKey { Kid = entry.Descriptor.Reference.KeyId, Alg = SignatureAlgorithms.EdDsa, Kty = "OKP", Use = JsonWebKeyUseNames.Sig, Crv = "Ed25519" }; foreach (var op in DefaultKeyOps) { jwk.KeyOps.Add(op); } jwk.X = Base64UrlEncoder.Encode(entry.PublicKey.GetEncoded()); return jwk; } } }