Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
176 lines
6.1 KiB
C#
176 lines
6.1 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
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 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<CryptoSigningKey> GetSigningKeys()
|
|
{
|
|
var list = new List<CryptoSigningKey>();
|
|
foreach (var registration in _registrations.Values)
|
|
{
|
|
var material = _kmsClient.ExportAsync(registration.KeyId, registration.VersionId).GetAwaiter().GetResult();
|
|
var metadata = new Dictionary<string, string?>(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<byte>();
|
|
}
|
|
|
|
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);
|