using Microsoft.IdentityModel.Tokens; using System; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using static StellaOps.Localization.T; namespace StellaOps.Cryptography; public sealed class EcdsaSigner : ICryptoSigner { private static readonly string[] DefaultKeyOps = { "sign", "verify" }; private readonly CryptoSigningKey signingKey; private EcdsaSigner(CryptoSigningKey signingKey) => this.signingKey = signingKey ?? throw new ArgumentNullException(nameof(signingKey)); public string KeyId => signingKey.Reference.KeyId; public string AlgorithmId => signingKey.AlgorithmId; public static ICryptoSigner Create(CryptoSigningKey signingKey) => new EcdsaSigner(signingKey); public static ICryptoSigner CreateVerifierFromPublicKey(string algorithmId, ReadOnlySpan publicKeyBytes) { using var ecdsa = ECDsa.Create(); ecdsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _); var publicParameters = ecdsa.ExportParameters(false); var verifierKey = new CryptoSigningKey( reference: new CryptoKeyReference("ephemeral-verifier"), algorithmId: algorithmId, publicParameters: publicParameters, verificationOnly: true, createdAt: DateTimeOffset.UtcNow, expiresAt: null, metadata: null); return new EcdsaSigner(verifierKey); } public ValueTask SignAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var ecdsa = ECDsa.Create(signingKey.PrivateParameters); var hashAlgorithm = ResolveHashAlgorithm(signingKey.AlgorithmId); var signature = ecdsa.SignData(data.Span, hashAlgorithm); return ValueTask.FromResult(signature); } public ValueTask VerifyAsync(ReadOnlyMemory data, ReadOnlyMemory signature, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); using var ecdsa = ECDsa.Create(signingKey.PublicParameters); var hashAlgorithm = ResolveHashAlgorithm(signingKey.AlgorithmId); var verified = ecdsa.VerifyData(data.Span, signature.Span, hashAlgorithm); return ValueTask.FromResult(verified); } public JsonWebKey ExportPublicJsonWebKey() { var jwk = new JsonWebKey { Kid = signingKey.Reference.KeyId, Alg = signingKey.AlgorithmId, Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve, Use = JsonWebKeyUseNames.Sig, Crv = ResolveCurve(signingKey.AlgorithmId) }; foreach (var op in DefaultKeyOps) { jwk.KeyOps.Add(op); } jwk.X = Base64UrlEncoder.Encode(signingKey.PublicParameters.Q.X ?? Array.Empty()); jwk.Y = Base64UrlEncoder.Encode(signingKey.PublicParameters.Q.Y ?? Array.Empty()); return jwk; } private static HashAlgorithmName ResolveHashAlgorithm(string algorithmId) => algorithmId switch { { } alg when string.Equals(alg, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase) => HashAlgorithmName.SHA256, { } alg when string.Equals(alg, SignatureAlgorithms.Es384, StringComparison.OrdinalIgnoreCase) => HashAlgorithmName.SHA384, { } alg when string.Equals(alg, SignatureAlgorithms.Es512, StringComparison.OrdinalIgnoreCase) => HashAlgorithmName.SHA512, _ => throw new InvalidOperationException(_t("crypto.ecdsa.algorithm_unsupported", algorithmId)) }; private static string ResolveCurve(string algorithmId) => algorithmId switch { { } alg when string.Equals(alg, SignatureAlgorithms.Es256, StringComparison.OrdinalIgnoreCase) => JsonWebKeyECTypes.P256, { } alg when string.Equals(alg, SignatureAlgorithms.Es384, StringComparison.OrdinalIgnoreCase) => JsonWebKeyECTypes.P384, { } alg when string.Equals(alg, SignatureAlgorithms.Es512, StringComparison.OrdinalIgnoreCase) => JsonWebKeyECTypes.P521, _ => throw new InvalidOperationException(_t("crypto.ecdsa.curve_unsupported", algorithmId)) }; }