using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Tokens; using StellaOps.Cryptography; using Xunit; namespace StellaOps.Cryptography.Tests; public class CryptoProviderRegistryTests { [Fact] public void ResolveOrThrow_RespectsPreferredProviderOrder() { var providerA = new FakeCryptoProvider("providerA") .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256) .WithSigner(SignatureAlgorithms.Es256, "key-a"); var providerB = new FakeCryptoProvider("providerB") .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256) .WithSigner(SignatureAlgorithms.Es256, "key-b"); var registry = new CryptoProviderRegistry(new[] { providerA, providerB }, new[] { "providerB" }); var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256); Assert.Same(providerB, resolved); } [Fact] public void ResolveSigner_UsesPreferredProviderHint() { var providerA = new FakeCryptoProvider("providerA") .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256) .WithSigner(SignatureAlgorithms.Es256, "key-a"); var providerB = new FakeCryptoProvider("providerB") .WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256) .WithSigner(SignatureAlgorithms.Es256, "key-b"); var registry = new CryptoProviderRegistry(new[] { providerA, providerB }, Array.Empty()); var hintResolution = registry.ResolveSigner( CryptoCapability.Signing, SignatureAlgorithms.Es256, new CryptoKeyReference("key-b"), preferredProvider: "providerB"); Assert.Equal("providerB", hintResolution.ProviderName); Assert.Equal("key-b", hintResolution.Signer.KeyId); var fallbackResolution = registry.ResolveSigner( CryptoCapability.Signing, SignatureAlgorithms.Es256, new CryptoKeyReference("key-a")); Assert.Equal("providerA", fallbackResolution.ProviderName); Assert.Equal("key-a", fallbackResolution.Signer.KeyId); } private sealed class FakeCryptoProvider : ICryptoProvider { private readonly Dictionary signers = new(StringComparer.Ordinal); private readonly HashSet<(CryptoCapability Capability, string Algorithm)> supported; public FakeCryptoProvider(string name) { Name = name; supported = new HashSet<(CryptoCapability, string)>(new CapabilityAlgorithmComparer()); } public string Name { get; } public FakeCryptoProvider WithSupport(CryptoCapability capability, string algorithm) { supported.Add((capability, algorithm)); return this; } public FakeCryptoProvider WithSigner(string algorithm, string keyId) { WithSupport(CryptoCapability.Signing, algorithm); var signer = new FakeSigner(Name, keyId, algorithm); signers[keyId] = signer; return this; } public bool Supports(CryptoCapability capability, string algorithmId) => supported.Contains((capability, algorithmId)); public IPasswordHasher GetPasswordHasher(string algorithmId) => throw new NotSupportedException(); public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference) { if (!signers.TryGetValue(keyReference.KeyId, out var signer)) { throw new KeyNotFoundException(); } if (!string.Equals(signer.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("Signer algorithm mismatch."); } return signer; } public void UpsertSigningKey(CryptoSigningKey signingKey) => signers[signingKey.Reference.KeyId] = new FakeSigner(Name, signingKey.Reference.KeyId, signingKey.AlgorithmId); public bool RemoveSigningKey(string keyId) => signers.Remove(keyId); public IReadOnlyCollection GetSigningKeys() => Array.Empty(); private sealed class CapabilityAlgorithmComparer : IEqualityComparer<(CryptoCapability Capability, string Algorithm)> { public bool Equals((CryptoCapability Capability, string Algorithm) x, (CryptoCapability Capability, string Algorithm) y) => x.Capability == y.Capability && string.Equals(x.Algorithm, y.Algorithm, StringComparison.OrdinalIgnoreCase); public int GetHashCode((CryptoCapability Capability, string Algorithm) obj) => HashCode.Combine(obj.Capability, obj.Algorithm.ToUpperInvariant()); } } private sealed class FakeSigner : ICryptoSigner { public FakeSigner(string provider, string keyId, string algorithmId) { Provider = provider; KeyId = keyId; AlgorithmId = algorithmId; } public string Provider { get; } public string KeyId { get; } public string AlgorithmId { get; } public ValueTask SignAsync(ReadOnlyMemory data, CancellationToken cancellationToken = default) => ValueTask.FromResult(Array.Empty()); public ValueTask VerifyAsync(ReadOnlyMemory data, ReadOnlyMemory signature, CancellationToken cancellationToken = default) => ValueTask.FromResult(true); public JsonWebKey ExportPublicJsonWebKey() => new() { Kid = KeyId, Alg = AlgorithmId, Kty = JsonWebAlgorithmsKeyTypes.Octet, Use = JsonWebKeyUseNames.Sig }; } }