Files
git.stella-ops.org/src/StellaOps.Cryptography.Tests/CryptoProviderRegistryTests.cs
Vladimir Moushkov ea1106ce7c up
2025-10-15 10:03:56 +03:00

157 lines
5.8 KiB
C#

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<string>());
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<string, FakeSigner> 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<CryptoSigningKey> GetSigningKeys() => Array.Empty<CryptoSigningKey>();
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<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(Array.Empty<byte>());
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
=> ValueTask.FromResult(true);
public JsonWebKey ExportPublicJsonWebKey() => new()
{
Kid = KeyId,
Alg = AlgorithmId,
Kty = JsonWebAlgorithmsKeyTypes.Octet,
Use = JsonWebKeyUseNames.Sig
};
}
}