157 lines
5.8 KiB
C#
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
|
|
};
|
|
}
|
|
}
|