up
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class Argon2idPasswordHasherTests
|
||||
{
|
||||
private readonly Argon2idPasswordHasher hasher = new();
|
||||
|
||||
[Fact]
|
||||
public void Hash_ProducesPhcEncodedString()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
var encoded = hasher.Hash("s3cret", options);
|
||||
|
||||
Assert.StartsWith("$argon2id$", encoded, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_ReturnsTrue_ForCorrectPassword()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
var encoded = hasher.Hash("s3cret", options);
|
||||
|
||||
Assert.True(hasher.Verify("s3cret", encoded));
|
||||
Assert.False(hasher.Verify("wrong", encoded));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeedsRehash_ReturnsTrue_WhenParametersChange()
|
||||
{
|
||||
var options = new PasswordHashOptions();
|
||||
var encoded = hasher.Hash("s3cret", options);
|
||||
|
||||
var updated = options with { Iterations = options.Iterations + 1 };
|
||||
|
||||
Assert.True(hasher.NeedsRehash(encoded, updated));
|
||||
Assert.False(hasher.NeedsRehash(encoded, options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using StellaOps.Cryptography.Audit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests.Audit;
|
||||
|
||||
public class AuthEventRecordTests
|
||||
{
|
||||
[Fact]
|
||||
public void AuthEventRecord_InitializesCollections()
|
||||
{
|
||||
var record = new AuthEventRecord
|
||||
{
|
||||
EventType = "authority.test",
|
||||
Outcome = AuthEventOutcome.Success
|
||||
};
|
||||
|
||||
Assert.NotNull(record.Scopes);
|
||||
Assert.Empty(record.Scopes);
|
||||
Assert.NotNull(record.Properties);
|
||||
Assert.Empty(record.Properties);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifiedString_NormalizesWhitespace()
|
||||
{
|
||||
var value = ClassifiedString.Personal(" ");
|
||||
Assert.Null(value.Value);
|
||||
Assert.False(value.HasValue);
|
||||
Assert.Equal(AuthEventDataClassification.Personal, value.Classification);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Subject_DefaultsToEmptyCollections()
|
||||
{
|
||||
var subject = new AuthEventSubject();
|
||||
Assert.NotNull(subject.Attributes);
|
||||
Assert.Empty(subject.Attributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Record_AssignsTimestamp_WhenNotProvided()
|
||||
{
|
||||
var record = new AuthEventRecord
|
||||
{
|
||||
EventType = "authority.test",
|
||||
Outcome = AuthEventOutcome.Success
|
||||
};
|
||||
|
||||
Assert.NotEqual(default, record.OccurredAt);
|
||||
Assert.InRange(
|
||||
record.OccurredAt,
|
||||
DateTimeOffset.UtcNow.AddSeconds(-5),
|
||||
DateTimeOffset.UtcNow.AddSeconds(5));
|
||||
}
|
||||
}
|
||||
154
src/StellaOps.Cryptography.Tests/CryptoProviderRegistryTests.cs
Normal file
154
src/StellaOps.Cryptography.Tests/CryptoProviderRegistryTests.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
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 hintSigner = registry.ResolveSigner(
|
||||
CryptoCapability.Signing,
|
||||
SignatureAlgorithms.Es256,
|
||||
new CryptoKeyReference("key-b"),
|
||||
preferredProvider: "providerB");
|
||||
|
||||
Assert.Equal("key-b", hintSigner.KeyId);
|
||||
|
||||
var fallbackSigner = registry.ResolveSigner(
|
||||
CryptoCapability.Signing,
|
||||
SignatureAlgorithms.Es256,
|
||||
new CryptoKeyReference("key-a"));
|
||||
|
||||
Assert.Equal("key-a", fallbackSigner.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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class DefaultCryptoProviderSigningTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task UpsertSigningKey_AllowsSignAndVerifyEs256()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var parameters = ecdsa.ExportParameters(includePrivateParameters: true);
|
||||
|
||||
var signingKey = new CryptoSigningKey(
|
||||
new CryptoKeyReference("revocation-key"),
|
||||
SignatureAlgorithms.Es256,
|
||||
privateParameters: in parameters,
|
||||
createdAt: DateTimeOffset.UtcNow);
|
||||
|
||||
provider.UpsertSigningKey(signingKey);
|
||||
|
||||
var signer = provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference);
|
||||
|
||||
var payload = Encoding.UTF8.GetBytes("hello-world");
|
||||
var signature = await signer.SignAsync(payload);
|
||||
|
||||
Assert.NotNull(signature);
|
||||
Assert.True(signature.Length > 0);
|
||||
|
||||
var verified = await signer.VerifyAsync(payload, signature);
|
||||
Assert.True(verified);
|
||||
|
||||
var jwk = signer.ExportPublicJsonWebKey();
|
||||
Assert.Equal(signingKey.Reference.KeyId, jwk.Kid);
|
||||
Assert.Equal(SignatureAlgorithms.Es256, jwk.Alg);
|
||||
Assert.Equal(JsonWebAlgorithmsKeyTypes.EllipticCurve, jwk.Kty);
|
||||
Assert.Equal(JsonWebKeyUseNames.Sig, jwk.Use);
|
||||
Assert.Equal(JsonWebKeyECTypes.P256, jwk.Crv);
|
||||
Assert.False(string.IsNullOrWhiteSpace(jwk.X));
|
||||
Assert.False(string.IsNullOrWhiteSpace(jwk.Y));
|
||||
|
||||
var tampered = (byte[])signature.Clone();
|
||||
tampered[^1] ^= 0xFF;
|
||||
var tamperedResult = await signer.VerifyAsync(payload, tampered);
|
||||
Assert.False(tamperedResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveSigningKey_PreventsRetrieval()
|
||||
{
|
||||
var provider = new DefaultCryptoProvider();
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var parameters = ecdsa.ExportParameters(true);
|
||||
var signingKey = new CryptoSigningKey(new CryptoKeyReference("key-to-remove"), SignatureAlgorithms.Es256, in parameters, DateTimeOffset.UtcNow);
|
||||
|
||||
provider.UpsertSigningKey(signingKey);
|
||||
Assert.True(provider.RemoveSigningKey(signingKey.Reference.KeyId));
|
||||
|
||||
Assert.Throws<KeyNotFoundException>(() => provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class Pbkdf2PasswordHasherTests
|
||||
{
|
||||
private readonly Pbkdf2PasswordHasher hasher = new();
|
||||
|
||||
[Fact]
|
||||
public void Hash_ProducesLegacyFormat()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
{
|
||||
Algorithm = PasswordHashAlgorithm.Pbkdf2,
|
||||
Iterations = 210_000
|
||||
};
|
||||
|
||||
var encoded = hasher.Hash("s3cret", options);
|
||||
|
||||
Assert.StartsWith("PBKDF2.", encoded, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_Succeeds_ForCorrectPassword()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
{
|
||||
Algorithm = PasswordHashAlgorithm.Pbkdf2,
|
||||
Iterations = 210_000
|
||||
};
|
||||
|
||||
var encoded = hasher.Hash("s3cret", options);
|
||||
|
||||
Assert.True(hasher.Verify("s3cret", encoded));
|
||||
Assert.False(hasher.Verify("other", encoded));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NeedsRehash_DetectsIterationChange()
|
||||
{
|
||||
var options = new PasswordHashOptions
|
||||
{
|
||||
Algorithm = PasswordHashAlgorithm.Pbkdf2,
|
||||
Iterations = 100_000
|
||||
};
|
||||
|
||||
var encoded = hasher.Hash("s3cret", options);
|
||||
|
||||
var higher = options with { Iterations = 150_000 };
|
||||
|
||||
Assert.True(hasher.NeedsRehash(encoded, higher));
|
||||
Assert.False(hasher.NeedsRehash(encoded, options));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user