This commit is contained in:
master
2025-10-29 19:24:20 +02:00
parent 86f606a115
commit fac626db8d
41 changed files with 2134 additions and 168 deletions

View File

@@ -0,0 +1,120 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Kms;
/// <summary>
/// Crypto provider that delegates signing operations to a KMS backend.
/// </summary>
public sealed class KmsCryptoProvider : ICryptoProvider
{
private readonly IKmsClient _kmsClient;
private readonly ConcurrentDictionary<string, KmsSigningRegistration> _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 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<CryptoSigningKey> GetSigningKeys()
{
var list = new List<CryptoSigningKey>();
foreach (var registration in _registrations.Values)
{
var material = _kmsClient.ExportAsync(registration.KeyId, registration.VersionId).GetAwaiter().GetResult();
var parameters = new ECParameters
{
Curve = ECCurve.NamedCurves.nistP256,
D = material.D,
Q = new ECPoint
{
X = material.Qx,
Y = material.Qy,
},
};
var metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
[KmsMetadataKeys.Version] = material.VersionId
};
list.Add(new CryptoSigningKey(
new CryptoKeyReference(material.KeyId, Name),
material.Algorithm,
in parameters,
material.CreatedAt,
metadata: metadata));
}
return list;
}
internal static class KmsMetadataKeys
{
public const string Version = "kms.version";
}
}
internal sealed record KmsSigningRegistration(string KeyId, string VersionId, string Algorithm);