using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using Microsoft.IdentityModel.Tokens; using StellaOps.Cryptography; namespace StellaOps.Cryptography.Kms; /// /// Crypto provider that delegates signing operations to a KMS backend. /// public sealed class KmsCryptoProvider : ICryptoProvider { private readonly IKmsClient _kmsClient; private readonly ConcurrentDictionary _registrations = new(StringComparer.OrdinalIgnoreCase); public KmsCryptoProvider(IKmsClient kmsClient) => _kmsClient = kmsClient ?? throw new ArgumentNullException(nameof(kmsClient)); public string Name => "kms"; public bool Supports(CryptoCapability capability, string algorithmId) { if (!string.Equals(algorithmId, KmsAlgorithms.Es256, StringComparison.OrdinalIgnoreCase)) { return false; } return capability is CryptoCapability.Signing or CryptoCapability.Verification; } public IPasswordHasher GetPasswordHasher(string algorithmId) => throw new InvalidOperationException($"Provider '{Name}' does not support password hashing."); public ICryptoHasher GetHasher(string algorithmId) => throw new InvalidOperationException($"Provider '{Name}' does not support content hashing."); public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference) { ArgumentNullException.ThrowIfNull(keyReference); if (!Supports(CryptoCapability.Signing, algorithmId)) { throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'."); } if (!_registrations.TryGetValue(keyReference.KeyId, out var registration)) { throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'."); } return new KmsSigner(_kmsClient, registration); } public void UpsertSigningKey(CryptoSigningKey signingKey) { ArgumentNullException.ThrowIfNull(signingKey); if (!string.Equals(signingKey.AlgorithmId, KmsAlgorithms.Es256, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException($"Provider '{Name}' only supports {KmsAlgorithms.Es256} signing keys."); } if (signingKey.Metadata is null || !signingKey.Metadata.TryGetValue(KmsMetadataKeys.Version, out var versionId) || string.IsNullOrWhiteSpace(versionId)) { throw new InvalidOperationException("KMS signing keys must include metadata entry 'kms.version'."); } var registration = new KmsSigningRegistration(signingKey.Reference.KeyId, versionId!, signingKey.AlgorithmId); _registrations.AddOrUpdate(signingKey.Reference.KeyId, registration, (_, _) => registration); } public bool RemoveSigningKey(string keyId) { if (string.IsNullOrWhiteSpace(keyId)) { return false; } return _registrations.TryRemove(keyId, out _); } public IReadOnlyCollection GetSigningKeys() { var list = new List(); foreach (var registration in _registrations.Values) { var material = _kmsClient.ExportAsync(registration.KeyId, registration.VersionId).GetAwaiter().GetResult(); var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase) { [KmsMetadataKeys.Version] = material.VersionId }; var reference = new CryptoKeyReference(material.KeyId, Name); CryptoSigningKey signingKey; if (material.D.Length == 0) { // Remote KMS keys may withhold private scalars; represent them as raw keys using public coordinates. var privateHandle = Encoding.UTF8.GetBytes(string.IsNullOrWhiteSpace(material.VersionId) ? material.KeyId : material.VersionId); if (privateHandle.Length == 0) { privateHandle = material.Qx.Length > 0 ? material.Qx : material.Qy.Length > 0 ? material.Qy : throw new InvalidOperationException($"KMS key '{material.KeyId}' does not expose public coordinates."); } var publicKey = CombineCoordinates(material.Qx, material.Qy); signingKey = new CryptoSigningKey( reference, material.Algorithm, privateHandle, material.CreatedAt, metadata: metadata, publicKey: publicKey); } else { var parameters = new ECParameters { Curve = ECCurve.NamedCurves.nistP256, D = material.D, Q = new ECPoint { X = material.Qx, Y = material.Qy, }, }; signingKey = new CryptoSigningKey( reference, material.Algorithm, in parameters, material.CreatedAt, metadata: metadata); } list.Add(signingKey); } return list; } internal static class KmsMetadataKeys { public const string Version = "kms.version"; } private static byte[] CombineCoordinates(byte[] qx, byte[] qy) { if (qx.Length == 0 && qy.Length == 0) { return Array.Empty(); } var buffer = new byte[qx.Length + qy.Length]; if (qx.Length > 0) { Buffer.BlockCopy(qx, 0, buffer, 0, qx.Length); } if (qy.Length > 0) { Buffer.BlockCopy(qy, 0, buffer, qx.Length, qy.Length); } return buffer; } } internal sealed record KmsSigningRegistration(string KeyId, string VersionId, string Algorithm);