stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -1,45 +1,44 @@
using System;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class Argon2idPasswordHasherTests
{
private readonly Argon2idPasswordHasher hasher = new();
private readonly Argon2idPasswordHasher _hasher = new();
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Hash_ProducesPhcEncodedString()
{
var options = new PasswordHashOptions();
var encoded = hasher.Hash("s3cret", options);
var encoded = _hasher.Hash("s3cret", options);
Assert.StartsWith("$argon2id$", encoded, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Verify_ReturnsTrue_ForCorrectPassword()
{
var options = new PasswordHashOptions();
var encoded = hasher.Hash("s3cret", options);
var encoded = _hasher.Hash("s3cret", options);
Assert.True(hasher.Verify("s3cret", encoded));
Assert.False(hasher.Verify("wrong", encoded));
Assert.True(_hasher.Verify("s3cret", encoded));
Assert.False(_hasher.Verify("wrong", encoded));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void NeedsRehash_ReturnsTrue_WhenParametersChange()
{
var options = new PasswordHashOptions();
var encoded = hasher.Hash("s3cret", options);
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));
Assert.True(_hasher.NeedsRehash(encoded, updated));
Assert.False(_hasher.NeedsRehash(encoded, options));
}
}

View File

@@ -1,5 +1,6 @@
using System;
using StellaOps.Cryptography.Audit;
using Xunit;
namespace StellaOps.Cryptography.Tests.Audit;
@@ -49,9 +50,7 @@ public class AuthEventRecordTests
};
Assert.NotEqual(default, record.OccurredAt);
Assert.InRange(
record.OccurredAt,
DateTimeOffset.UtcNow.AddSeconds(-5),
DateTimeOffset.UtcNow.AddSeconds(5));
Assert.Equal(TimeSpan.Zero, record.OccurredAt.Offset);
Assert.True(record.OccurredAt > DateTimeOffset.UnixEpoch);
}
}

View File

@@ -0,0 +1,27 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void GetHasher_ThrowsNotSupported()
{
Action act = () => _provider.GetHasher("SHA-256");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose hashing capabilities*");
_output.WriteLine("✓ GetHasher throws NotSupportedException");
}
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
Action act = () => _provider.GetPasswordHasher("argon2id");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose password hashing capabilities*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
}

View File

@@ -0,0 +1,36 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void Provider_Name_IsExpected()
{
_provider.Name.Should().Be("bouncycastle.ed25519");
_output.WriteLine($"✓ Provider name: {_provider.Name}");
}
[Fact]
public void Provider_CanBeResolvedFromDI()
{
var configuration = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddStellaOpsCrypto();
services.AddBouncyCastleEd25519Provider();
using var provider = services.BuildServiceProvider();
var cryptoProviders = provider.GetServices<ICryptoProvider>().ToList();
var bcProvider = cryptoProviders.OfType<BouncyCastleEd25519CryptoProvider>().FirstOrDefault();
bcProvider.Should().NotBeNull();
bcProvider!.Name.Should().Be("bouncycastle.ed25519");
_output.WriteLine("✓ BouncyCastle provider resolved from DI");
}
}

View File

@@ -0,0 +1,65 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void GetSigner_UnregisteredKey_ThrowsKeyNotFound()
{
var keyReference = new CryptoKeyReference("unregistered-key", _provider.Name);
Action act = () => _provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ GetSigner throws KeyNotFoundException for unregistered key");
}
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperation()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
Action act = () => provider.GetSigner(SignatureAlgorithms.Es256, keyReference);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ GetSigner throws InvalidOperationException for unsupported algorithm");
}
[Fact]
public void GetSigner_NullAlgorithm_ThrowsArgumentException()
{
var keyReference = new CryptoKeyReference("test-key", _provider.Name);
Action act = () => _provider.GetSigner(null!, keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ GetSigner throws ArgumentException for null algorithm");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
Action act = () => _provider.GetSigner(SignatureAlgorithms.Ed25519, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner throws ArgumentNullException for null key reference");
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void Supports_Signing_Ed25519_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Ed25519)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/Ed25519");
}
[Fact]
public void Supports_Signing_EdDsa_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.EdDsa)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/EdDSA");
}
[Theory]
[InlineData("ed25519")]
[InlineData("ED25519")]
[InlineData("Ed25519")]
[InlineData("eddsa")]
[InlineData("EDDSA")]
[InlineData("EdDSA")]
public void Supports_Signing_CaseInsensitive(string algorithm)
{
_provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Case-insensitive match: {algorithm}");
}
}

View File

@@ -0,0 +1,93 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void GetSigningKeys_InitiallyEmpty()
{
var provider = new BouncyCastleEd25519CryptoProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty collection initially");
}
[Fact]
public void UpsertSigningKey_AddsKey()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key-001";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().HaveCount(1);
provider.GetSigningKeys().Single().Reference.KeyId.Should().Be(keyId);
_output.WriteLine("✓ UpsertSigningKey adds key to collection");
}
[Fact]
public void UpsertSigningKey_UnsupportedAlgorithm_Throws()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Es256,
new byte[32],
createdAt: FixedNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ UpsertSigningKey rejects unsupported algorithm");
}
[Fact]
public void RemoveSigningKey_RemovesExistingKey()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "key-to-remove";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().HaveCount(1);
var result = provider.RemoveSigningKey(keyId);
result.Should().BeTrue();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ RemoveSigningKey removes existing key");
}
[Fact]
public void RemoveSigningKey_NonExistentKey_ReturnsFalse()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var result = provider.RemoveSigningKey("nonexistent");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false for non-existent key");
}
}

View File

@@ -0,0 +1,39 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void AlgorithmSummary_DocumentsSupportedAlgorithms()
{
var supportedForSigning = new[]
{
SignatureAlgorithms.Ed25519,
SignatureAlgorithms.EdDsa
};
foreach (var algo in supportedForSigning)
{
_provider.Supports(CryptoCapability.Signing, algo).Should().BeTrue();
_provider.Supports(CryptoCapability.Verification, algo).Should().BeTrue();
}
_output.WriteLine("=== BouncyCastle Ed25519 Provider Algorithm Summary ===");
_output.WriteLine("Supported for Signing:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Supported for Verification:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Unsupported Capabilities:");
_output.WriteLine(" - PasswordHashing");
_output.WriteLine(" - ContentHashing");
_output.WriteLine(" - SymmetricEncryption");
_output.WriteLine(" - KeyDerivation");
}
}

View File

@@ -0,0 +1,55 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Theory]
[InlineData(SignatureAlgorithms.Es256)]
[InlineData(SignatureAlgorithms.Es384)]
[InlineData(SignatureAlgorithms.Es512)]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.GostR3410_2012_512)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData(SignatureAlgorithms.Dilithium3)]
[InlineData(SignatureAlgorithms.Falcon512)]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("HS256")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
_provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse($"because {algorithm} is not supported by BouncyCastle Ed25519 provider");
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
[Fact]
public void Supports_Signing_NullAlgorithm_ReturnsFalse()
{
_provider.Supports(CryptoCapability.Signing, null!)
.Should().BeFalse();
_output.WriteLine("✓ Null algorithm returns false");
}
[Fact]
public void Supports_Signing_EmptyAlgorithm_ReturnsFalse()
{
_provider.Supports(CryptoCapability.Signing, string.Empty)
.Should().BeFalse();
_output.WriteLine("✓ Empty algorithm returns false");
}
[Fact]
public void Supports_Signing_WhitespaceAlgorithm_ReturnsFalse()
{
_provider.Supports(CryptoCapability.Signing, " ")
.Should().BeFalse();
_output.WriteLine("✓ Whitespace algorithm returns false");
}
}

View File

@@ -0,0 +1,42 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
_provider.Supports(CryptoCapability.PasswordHashing, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
_provider.Supports(CryptoCapability.SymmetricEncryption, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ SymmetricEncryption capability not supported");
}
[Fact]
public void Supports_KeyDerivation_ReturnsFalse()
{
_provider.Supports(CryptoCapability.KeyDerivation, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ KeyDerivation capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
_provider.Supports(CryptoCapability.ContentHashing, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
}

View File

@@ -0,0 +1,24 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleCapabilityDetectionTests
{
[Fact]
public void Supports_Verification_Ed25519_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.Ed25519)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/Ed25519");
}
[Fact]
public void Supports_Verification_EdDsa_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.EdDsa)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/EdDSA");
}
}

View File

@@ -4,16 +4,9 @@
// Task: SIGNER-5100-004 - Add capability detection tests for BouncyCastle plugin: enumerate supported algorithms
// Description: Capability detection and algorithm enumeration tests for BouncyCastle crypto plugin
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
/// <summary>
/// Capability detection tests for the BouncyCastle Ed25519 crypto provider.
/// Validates:
@@ -25,8 +18,9 @@ namespace StellaOps.Cryptography.Tests;
[Trait("Category", "CryptoPlugin")]
[Trait("Category", "BouncyCastle")]
[Trait("Category", "C1")]
public sealed class BouncyCastleCapabilityDetectionTests
public sealed partial class BouncyCastleCapabilityDetectionTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
private readonly BouncyCastleEd25519CryptoProvider _provider;
@@ -35,422 +29,4 @@ public sealed class BouncyCastleCapabilityDetectionTests
_output = output;
_provider = new BouncyCastleEd25519CryptoProvider();
}
#region Provider Identity Tests
[Fact]
public void Provider_Name_IsExpected()
{
_provider.Name.Should().Be("bouncycastle.ed25519");
_output.WriteLine($"✓ Provider name: {_provider.Name}");
}
[Fact]
public void Provider_CanBeResolvedFromDI()
{
// Arrange
var configuration = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddStellaOpsCrypto();
services.AddBouncyCastleEd25519Provider();
using var provider = services.BuildServiceProvider();
// Act
var cryptoProviders = provider.GetServices<ICryptoProvider>().ToList();
var bcProvider = cryptoProviders.OfType<BouncyCastleEd25519CryptoProvider>().FirstOrDefault();
// Assert
bcProvider.Should().NotBeNull();
bcProvider!.Name.Should().Be("bouncycastle.ed25519");
_output.WriteLine("✓ BouncyCastle provider resolved from DI");
}
#endregion
#region Signing Capability Tests
[Fact]
public void Supports_Signing_Ed25519_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Ed25519)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/Ed25519");
}
[Fact]
public void Supports_Signing_EdDsa_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.EdDsa)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/EdDSA");
}
[Theory]
[InlineData("ed25519")]
[InlineData("ED25519")]
[InlineData("Ed25519")]
[InlineData("eddsa")]
[InlineData("EDDSA")]
[InlineData("EdDSA")]
public void Supports_Signing_CaseInsensitive(string algorithm)
{
_provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Case-insensitive match: {algorithm}");
}
#endregion
#region Verification Capability Tests
[Fact]
public void Supports_Verification_Ed25519_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.Ed25519)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/Ed25519");
}
[Fact]
public void Supports_Verification_EdDsa_ReturnsTrue()
{
_provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.EdDsa)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/EdDSA");
}
#endregion
#region Unsupported Algorithm Tests
[Theory]
[InlineData(SignatureAlgorithms.Es256)]
[InlineData(SignatureAlgorithms.Es384)]
[InlineData(SignatureAlgorithms.Es512)]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.GostR3410_2012_512)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData(SignatureAlgorithms.Dilithium3)]
[InlineData(SignatureAlgorithms.Falcon512)]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("HS256")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
_provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse($"because {algorithm} is not supported by BouncyCastle Ed25519 provider");
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
[Fact]
public void Supports_Signing_NullAlgorithm_ReturnsFalse()
{
_provider.Supports(CryptoCapability.Signing, null!)
.Should().BeFalse();
_output.WriteLine("✓ Null algorithm returns false");
}
[Fact]
public void Supports_Signing_EmptyAlgorithm_ReturnsFalse()
{
_provider.Supports(CryptoCapability.Signing, string.Empty)
.Should().BeFalse();
_output.WriteLine("✓ Empty algorithm returns false");
}
[Fact]
public void Supports_Signing_WhitespaceAlgorithm_ReturnsFalse()
{
_provider.Supports(CryptoCapability.Signing, " ")
.Should().BeFalse();
_output.WriteLine("✓ Whitespace algorithm returns false");
}
#endregion
#region Unsupported Capability Tests
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
_provider.Supports(CryptoCapability.PasswordHashing, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
_provider.Supports(CryptoCapability.SymmetricEncryption, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ SymmetricEncryption capability not supported");
}
[Fact]
public void Supports_KeyDerivation_ReturnsFalse()
{
_provider.Supports(CryptoCapability.KeyDerivation, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ KeyDerivation capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
_provider.Supports(CryptoCapability.ContentHashing, SignatureAlgorithms.Ed25519)
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
#endregion
#region Hasher Capability Tests
[Fact]
public void GetHasher_ThrowsNotSupported()
{
Action act = () => _provider.GetHasher("SHA-256");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose hashing capabilities*");
_output.WriteLine("✓ GetHasher throws NotSupportedException");
}
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
Action act = () => _provider.GetPasswordHasher("argon2id");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose password hashing capabilities*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
#endregion
#region Signing Key Management Tests
[Fact]
public void GetSigningKeys_InitiallyEmpty()
{
var provider = new BouncyCastleEd25519CryptoProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty collection initially");
}
[Fact]
public void UpsertSigningKey_AddsKey()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key-001";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
// Act
provider.UpsertSigningKey(signingKey);
// Assert
provider.GetSigningKeys().Should().HaveCount(1);
provider.GetSigningKeys().Single().Reference.KeyId.Should().Be(keyId);
_output.WriteLine("✓ UpsertSigningKey adds key to collection");
}
[Fact]
public void UpsertSigningKey_UnsupportedAlgorithm_Throws()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Es256, // Not supported
new byte[32],
createdAt: DateTimeOffset.UtcNow);
// Act
Action act = () => provider.UpsertSigningKey(signingKey);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ UpsertSigningKey rejects unsupported algorithm");
}
[Fact]
public void RemoveSigningKey_RemovesExistingKey()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "key-to-remove";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().HaveCount(1);
// Act
var result = provider.RemoveSigningKey(keyId);
// Assert
result.Should().BeTrue();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ RemoveSigningKey removes existing key");
}
[Fact]
public void RemoveSigningKey_NonExistentKey_ReturnsFalse()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var result = provider.RemoveSigningKey("nonexistent");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false for non-existent key");
}
#endregion
#region Signer Retrieval Tests
[Fact]
public void GetSigner_UnregisteredKey_ThrowsKeyNotFound()
{
// Arrange
var keyReference = new CryptoKeyReference("unregistered-key", _provider.Name);
// Act
Action act = () => _provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
// Assert
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ GetSigner throws KeyNotFoundException for unregistered key");
}
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperation()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
// Act
Action act = () => provider.GetSigner(SignatureAlgorithms.Es256, keyReference);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ GetSigner throws InvalidOperationException for unsupported algorithm");
}
[Fact]
public void GetSigner_NullAlgorithm_ThrowsArgumentException()
{
var keyReference = new CryptoKeyReference("test-key", _provider.Name);
Action act = () => _provider.GetSigner(null!, keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ GetSigner throws ArgumentException for null algorithm");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
Action act = () => _provider.GetSigner(SignatureAlgorithms.Ed25519, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner throws ArgumentNullException for null key reference");
}
#endregion
#region Algorithm Summary
[Fact]
public void AlgorithmSummary_DocumentsSupportedAlgorithms()
{
// This test documents the supported algorithms for reference
var supportedForSigning = new[]
{
SignatureAlgorithms.Ed25519,
SignatureAlgorithms.EdDsa
};
foreach (var algo in supportedForSigning)
{
_provider.Supports(CryptoCapability.Signing, algo).Should().BeTrue();
_provider.Supports(CryptoCapability.Verification, algo).Should().BeTrue();
}
_output.WriteLine("=== BouncyCastle Ed25519 Provider Algorithm Summary ===");
_output.WriteLine("Supported for Signing:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Supported for Verification:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Unsupported Capabilities:");
_output.WriteLine(" - PasswordHashing");
_output.WriteLine(" - ContentHashing");
_output.WriteLine(" - SymmetricEncryption");
_output.WriteLine(" - KeyDerivation");
}
#endregion
}

View File

@@ -1,19 +1,18 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class BouncyCastleEd25519CryptoProviderTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SignAndVerify_WithBouncyCastleProvider_Succeeds()
[Fact]
public async Task SignAndVerify_WithBouncyCastleProvider_SucceedsAsync()
{
var configuration = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
@@ -34,7 +33,7 @@ public sealed class BouncyCastleEd25519CryptoProviderTests
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
createdAt: FixedNow);
bcProvider.UpsertSigningKey(signingKey);

View File

@@ -0,0 +1,67 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.Es256, keyReference);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ Unsupported algorithm → InvalidOperationException");
_output.WriteLine(" Error code mapping: CRYPTO_ALGORITHM_NOT_SUPPORTED");
}
[Theory]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("HS256")]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("UNKNOWN")]
public void GetSigner_VariousUnsupportedAlgorithms_ThrowsInvalidOperationException(string algorithm)
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
Action act = () => provider.GetSigner(algorithm, keyReference);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine($"✓ Algorithm '{algorithm}' → InvalidOperationException");
}
[Fact]
public void UpsertSigningKey_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Es256,
new byte[32],
createdAt: FixedNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ UpsertSigningKey with unsupported algorithm → InvalidOperationException");
}
}

View File

@@ -0,0 +1,95 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
[Fact]
public void GetSigner_NullAlgorithm_ThrowsArgumentException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
Action act = () => provider.GetSigner(null!, keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ Null algorithm → ArgumentException");
_output.WriteLine(" Error code mapping: CRYPTO_INVALID_ARGUMENT");
}
[Fact]
public void GetSigner_EmptyAlgorithm_ThrowsArgumentException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
Action act = () => provider.GetSigner(string.Empty, keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ Empty algorithm → ArgumentException");
}
[Fact]
public void GetSigner_WhitespaceAlgorithm_ThrowsArgumentException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
Action act = () => provider.GetSigner(" ", keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ Whitespace algorithm → ArgumentException");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ Null key reference → ArgumentNullException");
}
[Fact]
public void UpsertSigningKey_NullSigningKey_ThrowsArgumentNullException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
Action act = () => provider.UpsertSigningKey(null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ Null signing key → ArgumentNullException");
}
[Fact]
public void RemoveSigningKey_NullKeyId_ReturnsFalse()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var result = provider.RemoveSigningKey(null!);
result.Should().BeFalse();
_output.WriteLine("✓ Null key ID → returns false (graceful)");
}
[Fact]
public void RemoveSigningKey_EmptyKeyId_ReturnsFalse()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var result = provider.RemoveSigningKey(string.Empty);
result.Should().BeFalse();
_output.WriteLine("✓ Empty key ID → returns false (graceful)");
}
}

View File

@@ -0,0 +1,33 @@
using FluentAssertions;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
[Fact]
public void GetHasher_ThrowsNotSupportedException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
Action act = () => provider.GetHasher("SHA-256");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose hashing capabilities*");
_output.WriteLine("✓ GetHasher → NotSupportedException");
_output.WriteLine(" Error code mapping: CRYPTO_CAPABILITY_NOT_SUPPORTED");
}
[Fact]
public void GetPasswordHasher_ThrowsNotSupportedException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
Action act = () => provider.GetPasswordHasher("argon2id");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose password hashing capabilities*");
_output.WriteLine("✓ GetPasswordHasher → NotSupportedException");
}
}

View File

@@ -0,0 +1,17 @@
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
private static void RegisterKey(BouncyCastleEd25519CryptoProvider provider, string keyId)
{
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
}
}

View File

@@ -0,0 +1,68 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
[Theory]
[InlineData(16)]
[InlineData(31)]
[InlineData(33)]
[InlineData(65)]
[InlineData(128)]
public void UpsertSigningKey_InvalidPrivateKeyLength_ThrowsInvalidOperationException(int keyLength)
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
new byte[keyLength],
createdAt: FixedNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*32 or 64 bytes*");
_output.WriteLine($"✓ Private key length {keyLength} → InvalidOperationException");
_output.WriteLine(" Error code mapping: CRYPTO_INVALID_KEY_MATERIAL");
}
[Fact]
public void UpsertSigningKey_EmptyPrivateKey_ThrowsArgumentException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
Action act = () => _ = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
Array.Empty<byte>(),
createdAt: FixedNow);
act.Should().Throw<ArgumentException>()
.WithMessage("*Private key material must be provided*");
}
[Fact]
public void UpsertSigningKey_InvalidPublicKeyLength_ThrowsInvalidOperationException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
new byte[32],
createdAt: FixedNow,
publicKey: new byte[16]);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*32 bytes*");
_output.WriteLine("✓ Invalid public key length → InvalidOperationException");
}
}

View File

@@ -0,0 +1,59 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
[Fact]
public void GetSigner_KeyNotRegistered_ThrowsKeyNotFoundException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("nonexistent-key", provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ Key not found → KeyNotFoundException");
_output.WriteLine(" Error code mapping: CRYPTO_KEY_NOT_FOUND");
}
[Fact]
public void GetSigner_DifferentKeyId_ThrowsKeyNotFoundException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var registeredKeyId = "registered-key";
var requestedKeyId = "different-key";
RegisterKey(provider, registeredKeyId);
var keyReference = new CryptoKeyReference(requestedKeyId, provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<KeyNotFoundException>();
_output.WriteLine("✓ Different key ID → KeyNotFoundException");
}
[Fact]
public void GetSigner_AfterKeyRemoval_ThrowsKeyNotFoundException()
{
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "temporary-key";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signer = provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
signer.Should().NotBeNull();
provider.RemoveSigningKey(keyId);
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<KeyNotFoundException>();
_output.WriteLine("✓ Key removed → KeyNotFoundException");
}
}

View File

@@ -0,0 +1,34 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleErrorClassificationTests
{
[Fact]
public void ErrorCodeMappingSummary_DocumentsExpectedMappings()
{
var mappings = new Dictionary<Type, string>
{
{ typeof(KeyNotFoundException), "CRYPTO_KEY_NOT_FOUND" },
{ typeof(InvalidOperationException), "CRYPTO_INVALID_OPERATION" },
{ typeof(ArgumentNullException), "CRYPTO_INVALID_ARGUMENT" },
{ typeof(ArgumentException), "CRYPTO_INVALID_ARGUMENT" },
{ typeof(NotSupportedException), "CRYPTO_CAPABILITY_NOT_SUPPORTED" }
};
_output.WriteLine("=== BouncyCastle Error Code Mapping Reference ===");
foreach (var mapping in mappings)
{
_output.WriteLine($" {mapping.Key.Name} → {mapping.Value}");
}
_output.WriteLine("");
_output.WriteLine("Specific Error Codes:");
_output.WriteLine(" CRYPTO_KEY_NOT_FOUND - Key ID not registered with provider");
_output.WriteLine(" CRYPTO_ALGORITHM_NOT_SUPPORTED - Algorithm not supported by this provider");
_output.WriteLine(" CRYPTO_INVALID_KEY_MATERIAL - Key bytes have wrong length/format");
_output.WriteLine(" CRYPTO_INVALID_ARGUMENT - Null or invalid parameter");
_output.WriteLine(" CRYPTO_CAPABILITY_NOT_SUPPORTED - Provider doesn't support capability");
mappings.Should().NotBeEmpty();
}
}

View File

@@ -4,16 +4,8 @@
// Task: SIGNER-5100-006 - Add error classification tests for BouncyCastle: key not present → deterministic error code
// Description: Error classification tests for BouncyCastle Ed25519 crypto plugin
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
/// <summary>
/// Error classification tests for the BouncyCastle Ed25519 crypto provider.
/// Validates that specific error conditions produce deterministic exception types
@@ -23,426 +15,13 @@ namespace StellaOps.Cryptography.Tests;
[Trait("Category", "BouncyCastle")]
[Trait("Category", "Errors")]
[Trait("Category", "C1")]
public sealed class BouncyCastleErrorClassificationTests
public sealed partial class BouncyCastleErrorClassificationTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
public BouncyCastleErrorClassificationTests(ITestOutputHelper output)
{
_output = output;
}
#region Key Not Present Errors
[Fact]
public void GetSigner_KeyNotRegistered_ThrowsKeyNotFoundException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("nonexistent-key", provider.Name);
// Act
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
// Assert
var exception = act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine($"✓ Key not found → KeyNotFoundException");
_output.WriteLine($" Error code mapping: CRYPTO_KEY_NOT_FOUND");
}
[Fact]
public void GetSigner_DifferentKeyId_ThrowsKeyNotFoundException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var registeredKeyId = "registered-key";
var requestedKeyId = "different-key";
RegisterKey(provider, registeredKeyId);
var keyReference = new CryptoKeyReference(requestedKeyId, provider.Name);
// Act
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
// Assert
act.Should().Throw<KeyNotFoundException>();
_output.WriteLine("✓ Different key ID → KeyNotFoundException");
}
[Fact]
public void GetSigner_AfterKeyRemoval_ThrowsKeyNotFoundException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "temporary-key";
RegisterKey(provider, keyId);
// Verify key works initially
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signer = provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
signer.Should().NotBeNull();
// Remove the key
provider.RemoveSigningKey(keyId);
// Act
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
// Assert
act.Should().Throw<KeyNotFoundException>();
_output.WriteLine("✓ Key removed → KeyNotFoundException");
}
#endregion
#region Algorithm Not Supported Errors
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
// Act
Action act = () => provider.GetSigner(SignatureAlgorithms.Es256, keyReference);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ Unsupported algorithm → InvalidOperationException");
_output.WriteLine(" Error code mapping: CRYPTO_ALGORITHM_NOT_SUPPORTED");
}
[Theory]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("HS256")]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("UNKNOWN")]
public void GetSigner_VariousUnsupportedAlgorithms_ThrowsInvalidOperationException(string algorithm)
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyId = "test-key";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
// Act
Action act = () => provider.GetSigner(algorithm, keyReference);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine($"✓ Algorithm '{algorithm}' → InvalidOperationException");
}
[Fact]
public void UpsertSigningKey_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Es256, // Not supported
new byte[32],
createdAt: DateTimeOffset.UtcNow);
// Act
Action act = () => provider.UpsertSigningKey(signingKey);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ UpsertSigningKey with unsupported algorithm → InvalidOperationException");
}
#endregion
#region Invalid Key Material Errors
[Theory]
[InlineData(16)]
[InlineData(31)]
[InlineData(33)]
[InlineData(65)]
[InlineData(128)]
public void UpsertSigningKey_InvalidPrivateKeyLength_ThrowsInvalidOperationException(int keyLength)
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
new byte[keyLength],
createdAt: DateTimeOffset.UtcNow);
// Act
Action act = () => provider.UpsertSigningKey(signingKey);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*32 or 64 bytes*");
_output.WriteLine($"✓ Private key length {keyLength} → InvalidOperationException");
_output.WriteLine(" Error code mapping: CRYPTO_INVALID_KEY_MATERIAL");
}
[Fact]
public void UpsertSigningKey_EmptyPrivateKey_ThrowsArgumentException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
// Act
Action act = () => _ = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
Array.Empty<byte>(),
createdAt: DateTimeOffset.UtcNow);
// Assert
act.Should().Throw<ArgumentException>()
.WithMessage("*Private key material must be provided*");
}
[Fact]
public void UpsertSigningKey_InvalidPublicKeyLength_ThrowsInvalidOperationException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
new byte[32], // Valid private key
createdAt: DateTimeOffset.UtcNow,
publicKey: new byte[16]); // Invalid public key
// Act
Action act = () => provider.UpsertSigningKey(signingKey);
// Assert
act.Should().Throw<InvalidOperationException>()
.WithMessage("*32 bytes*");
_output.WriteLine("✓ Invalid public key length → InvalidOperationException");
}
#endregion
#region Argument Validation Errors
[Fact]
public void GetSigner_NullAlgorithm_ThrowsArgumentException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
// Act
Action act = () => provider.GetSigner(null!, keyReference);
// Assert
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ Null algorithm → ArgumentException");
_output.WriteLine(" Error code mapping: CRYPTO_INVALID_ARGUMENT");
}
[Fact]
public void GetSigner_EmptyAlgorithm_ThrowsArgumentException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
// Act
Action act = () => provider.GetSigner(string.Empty, keyReference);
// Assert
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ Empty algorithm → ArgumentException");
}
[Fact]
public void GetSigner_WhitespaceAlgorithm_ThrowsArgumentException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
// Act
Action act = () => provider.GetSigner(" ", keyReference);
// Assert
act.Should().Throw<ArgumentException>();
_output.WriteLine("✓ Whitespace algorithm → ArgumentException");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
// Act
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, null!);
// Assert
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ Null key reference → ArgumentNullException");
}
[Fact]
public void UpsertSigningKey_NullSigningKey_ThrowsArgumentNullException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
// Act
Action act = () => provider.UpsertSigningKey(null!);
// Assert
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ Null signing key → ArgumentNullException");
}
[Fact]
public void RemoveSigningKey_NullKeyId_ReturnsFalse()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
// Act
var result = provider.RemoveSigningKey(null!);
// Assert
result.Should().BeFalse();
_output.WriteLine("✓ Null key ID → returns false (graceful)");
}
[Fact]
public void RemoveSigningKey_EmptyKeyId_ReturnsFalse()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
// Act
var result = provider.RemoveSigningKey(string.Empty);
// Assert
result.Should().BeFalse();
_output.WriteLine("✓ Empty key ID → returns false (graceful)");
}
#endregion
#region Capability Not Supported Errors
[Fact]
public void GetHasher_ThrowsNotSupportedException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
// Act
Action act = () => provider.GetHasher("SHA-256");
// Assert
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose hashing capabilities*");
_output.WriteLine("✓ GetHasher → NotSupportedException");
_output.WriteLine(" Error code mapping: CRYPTO_CAPABILITY_NOT_SUPPORTED");
}
[Fact]
public void GetPasswordHasher_ThrowsNotSupportedException()
{
// Arrange
var provider = new BouncyCastleEd25519CryptoProvider();
// Act
Action act = () => provider.GetPasswordHasher("argon2id");
// Assert
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose password hashing capabilities*");
_output.WriteLine("✓ GetPasswordHasher → NotSupportedException");
}
#endregion
#region Error Code Mapping Summary
[Fact]
public void ErrorCodeMappingSummary_DocumentsExpectedMappings()
{
// This test documents the expected error code mappings for operators
var mappings = new Dictionary<Type, string>
{
{ typeof(KeyNotFoundException), "CRYPTO_KEY_NOT_FOUND" },
{ typeof(InvalidOperationException), "CRYPTO_INVALID_OPERATION" },
{ typeof(ArgumentNullException), "CRYPTO_INVALID_ARGUMENT" },
{ typeof(ArgumentException), "CRYPTO_INVALID_ARGUMENT" },
{ typeof(NotSupportedException), "CRYPTO_CAPABILITY_NOT_SUPPORTED" }
};
_output.WriteLine("=== BouncyCastle Error Code Mapping Reference ===");
foreach (var mapping in mappings)
{
_output.WriteLine($" {mapping.Key.Name} → {mapping.Value}");
}
_output.WriteLine("");
_output.WriteLine("Specific Error Codes:");
_output.WriteLine(" CRYPTO_KEY_NOT_FOUND - Key ID not registered with provider");
_output.WriteLine(" CRYPTO_ALGORITHM_NOT_SUPPORTED - Algorithm not supported by this provider");
_output.WriteLine(" CRYPTO_INVALID_KEY_MATERIAL - Key bytes have wrong length/format");
_output.WriteLine(" CRYPTO_INVALID_ARGUMENT - Null or invalid parameter");
_output.WriteLine(" CRYPTO_CAPABILITY_NOT_SUPPORTED - Provider doesn't support capability");
mappings.Should().NotBeEmpty();
}
#endregion
#region Helper Methods
private static void RegisterKey(BouncyCastleEd25519CryptoProvider provider, string keyId)
{
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
}
#endregion
}

View File

@@ -0,0 +1,33 @@
using System.Text;
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
[Fact]
public async Task Sign_EdDsaAlias_WorksAsync()
{
var provider = CreateProvider();
var keyId = "key-eddsa-alias";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
var signer = provider.GetSigner(SignatureAlgorithms.EdDsa, keyReference);
var message = Encoding.UTF8.GetBytes("Test with EdDSA alias");
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
_output.WriteLine("✓ EdDSA alias works correctly");
}
}

View File

@@ -0,0 +1,83 @@
using System.Text;
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
[Fact]
public async Task SignAndVerify_BasicMessage_SucceedsAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-basic");
var message = Encoding.UTF8.GetBytes("Hello, StellaOps!");
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
signature.Should().HaveCount(64, "Ed25519 signatures are 64 bytes");
_output.WriteLine("✓ Basic message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_EmptyMessage_SucceedsAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-empty");
var message = Array.Empty<byte>();
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
_output.WriteLine("✓ Empty message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_SingleByte_SucceedsAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-single");
var message = new byte[] { 0x42 };
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
_output.WriteLine("✓ Single byte message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_LargeMessage_SucceedsAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-large");
var message = CreateLargeMessage();
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
_output.WriteLine("✓ Large (1 MB) message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_BinaryData_SucceedsAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-binary");
var message = new byte[] { 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFE };
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
_output.WriteLine("✓ Binary data sign/verify roundtrip succeeded");
}
}

View File

@@ -0,0 +1,38 @@
using System.Text;
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
[Fact]
public async Task Sign_SameMessage_ProducesSameSignatureAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-determinism");
var message = Encoding.UTF8.GetBytes("Deterministic message");
var signature1 = await signer.SignAsync(message);
var signature2 = await signer.SignAsync(message);
signature1.Should().BeEquivalentTo(signature2);
_output.WriteLine("✓ Ed25519 signing is deterministic");
}
[Fact]
public async Task Sign_DifferentKeys_ProduceDifferentSignaturesAsync()
{
var provider = CreateProvider();
var signer1 = SetupSigner(provider, "key-diff-1", seed: 100);
var signer2 = SetupSigner(provider, "key-diff-2", seed: 200);
var message = Encoding.UTF8.GetBytes("Same message");
var signature1 = await signer1.SignAsync(message);
var signature2 = await signer2.SignAsync(message);
signature1.Should().NotBeEquivalentTo(signature2);
_output.WriteLine("✓ Different keys produce different signatures");
}
}

View File

@@ -0,0 +1,38 @@
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.BouncyCastle;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
private static BouncyCastleEd25519CryptoProvider CreateProvider()
=> new();
private static ICryptoSigner SetupSigner(
BouncyCastleEd25519CryptoProvider provider,
string keyId,
int? seed = null)
{
var privateKeyBytes = seed.HasValue
? Enumerable.Range(seed.Value, 32).Select(i => (byte)(i % 256)).ToArray()
: Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
return provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
}
private static byte[] CreateLargeMessage()
{
var message = new byte[1_000_000];
for (var i = 0; i < message.Length; i++)
{
message[i] = (byte)(i % 256);
}
return message;
}
}

View File

@@ -0,0 +1,31 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
[Fact]
public void ExportPublicJwk_HasCorrectProperties()
{
var provider = CreateProvider();
var keyId = "key-jwk-export";
var signer = SetupSigner(provider, keyId);
var jwk = signer.ExportPublicJsonWebKey();
jwk.Should().NotBeNull();
jwk.Kid.Should().Be(keyId);
jwk.Kty.Should().Be("OKP");
jwk.Crv.Should().Be("Ed25519");
jwk.Alg.Should().Be(SignatureAlgorithms.EdDsa);
jwk.Use.Should().Be("sig");
jwk.X.Should().NotBeNullOrEmpty("public key X coordinate required");
jwk.D.Should().BeNull("private key should not be exported");
_output.WriteLine("✓ JWK export has correct properties");
_output.WriteLine($" Kid: {jwk.Kid}");
_output.WriteLine($" Kty: {jwk.Kty}");
_output.WriteLine($" Crv: {jwk.Crv}");
_output.WriteLine($" Alg: {jwk.Alg}");
}
}

View File

@@ -0,0 +1,53 @@
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
[Fact]
public async Task RegistryResolution_SignAndVerify_SucceedsAsync()
{
var configuration = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddStellaOpsCrypto();
services.AddBouncyCastleEd25519Provider();
using var serviceProvider = services.BuildServiceProvider();
var registry = serviceProvider.GetRequiredService<ICryptoProviderRegistry>();
var bcProvider = serviceProvider.GetServices<ICryptoProvider>()
.OfType<BouncyCastleEd25519CryptoProvider>()
.Single();
var keyId = "registry-test-key";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, bcProvider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: FixedNow);
bcProvider.UpsertSigningKey(signingKey);
var resolution = registry.ResolveSigner(
CryptoCapability.Signing,
SignatureAlgorithms.Ed25519,
keyReference,
bcProvider.Name);
var message = Encoding.UTF8.GetBytes("Registry resolution test");
var signature = await resolution.Signer.SignAsync(message);
var verified = await resolution.Signer.VerifyAsync(message, signature);
verified.Should().BeTrue();
resolution.ProviderName.Should().Be(bcProvider.Name);
_output.WriteLine("✓ Registry resolution sign/verify succeeded");
}
}

View File

@@ -0,0 +1,74 @@
using System.Text;
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
[Fact]
public async Task Verify_TamperedMessage_ReturnsFalseAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-tamper-msg");
var message = Encoding.UTF8.GetBytes("Original message");
var tamperedMessage = Encoding.UTF8.GetBytes("Tampered message");
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(tamperedMessage, signature);
verified.Should().BeFalse();
_output.WriteLine("✓ Tampered message correctly rejected");
}
[Fact]
public async Task Verify_TamperedSignature_ReturnsFalseAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-tamper-sig");
var message = Encoding.UTF8.GetBytes("Test message");
var signature = await signer.SignAsync(message);
var tamperedSignature = signature.ToArray();
tamperedSignature[0] ^= 0xFF;
var verified = await signer.VerifyAsync(message, tamperedSignature);
verified.Should().BeFalse();
_output.WriteLine("✓ Tampered signature correctly rejected");
}
[Fact]
public async Task Verify_TruncatedSignature_ReturnsFalseAsync()
{
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-truncate");
var message = Encoding.UTF8.GetBytes("Test message");
var signature = await signer.SignAsync(message);
var truncatedSignature = signature.Take(32).ToArray();
var verified = await signer.VerifyAsync(message, truncatedSignature);
verified.Should().BeFalse();
_output.WriteLine("✓ Truncated signature correctly rejected");
}
[Fact]
public async Task Verify_WrongKey_ReturnsFalseAsync()
{
var provider = CreateProvider();
var signer1 = SetupSigner(provider, "key-1", seed: 1);
var signer2 = SetupSigner(provider, "key-2", seed: 2);
var message = Encoding.UTF8.GetBytes("Test message");
var signature = await signer1.SignAsync(message);
var verified = await signer2.VerifyAsync(message, signature);
verified.Should().BeFalse();
_output.WriteLine("✓ Wrong key correctly rejected");
}
}

View File

@@ -4,18 +4,8 @@
// Task: SIGNER-5100-005 - Add sign/verify roundtrip tests for BouncyCastle: sign with private key → verify with public key
// Description: Sign/verify roundtrip tests for BouncyCastle Ed25519 crypto plugin
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography.DependencyInjection;
using StellaOps.Cryptography.Plugin.BouncyCastle;
using System.Security.Cryptography;
using System.Text;
using Xunit;
namespace StellaOps.Cryptography.Tests;
/// <summary>
/// Sign/verify roundtrip tests for the BouncyCastle Ed25519 crypto provider.
/// Validates:
@@ -29,377 +19,13 @@ namespace StellaOps.Cryptography.Tests;
[Trait("Category", "BouncyCastle")]
[Trait("Category", "SignVerify")]
[Trait("Category", "C1")]
public sealed class BouncyCastleSignVerifyRoundtripTests
public sealed partial class BouncyCastleSignVerifyRoundtripTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
public BouncyCastleSignVerifyRoundtripTests(ITestOutputHelper output)
{
_output = output;
}
#region Basic Roundtrip Tests
[Fact]
public async Task SignAndVerify_BasicMessage_Succeeds()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-basic");
var message = Encoding.UTF8.GetBytes("Hello, StellaOps!");
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
signature.Should().HaveCount(64, "Ed25519 signatures are 64 bytes");
_output.WriteLine("✓ Basic message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_EmptyMessage_Succeeds()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-empty");
var message = Array.Empty<byte>();
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
_output.WriteLine("✓ Empty message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_SingleByte_Succeeds()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-single");
var message = new byte[] { 0x42 };
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
_output.WriteLine("✓ Single byte message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_LargeMessage_Succeeds()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-large");
var message = new byte[1_000_000]; // 1 MB
RandomNumberGenerator.Fill(message);
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
_output.WriteLine("✓ Large (1 MB) message sign/verify roundtrip succeeded");
}
[Fact]
public async Task SignAndVerify_BinaryData_Succeeds()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-binary");
var message = new byte[] { 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFE };
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
_output.WriteLine("✓ Binary data sign/verify roundtrip succeeded");
}
#endregion
#region Tampering Detection Tests
[Fact]
public async Task Verify_TamperedMessage_ReturnsFalse()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-tamper-msg");
var message = Encoding.UTF8.GetBytes("Original message");
var tamperedMessage = Encoding.UTF8.GetBytes("Tampered message");
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(tamperedMessage, signature);
// Assert
verified.Should().BeFalse();
_output.WriteLine("✓ Tampered message correctly rejected");
}
[Fact]
public async Task Verify_TamperedSignature_ReturnsFalse()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-tamper-sig");
var message = Encoding.UTF8.GetBytes("Test message");
// Act
var signature = await signer.SignAsync(message);
var tamperedSignature = signature.ToArray();
tamperedSignature[0] ^= 0xFF; // Flip bits in first byte
var verified = await signer.VerifyAsync(message, tamperedSignature);
// Assert
verified.Should().BeFalse();
_output.WriteLine("✓ Tampered signature correctly rejected");
}
[Fact]
public async Task Verify_TruncatedSignature_ReturnsFalse()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-truncate");
var message = Encoding.UTF8.GetBytes("Test message");
// Act
var signature = await signer.SignAsync(message);
var truncatedSignature = signature.Take(32).ToArray(); // Only half
var verified = await signer.VerifyAsync(message, truncatedSignature);
// Assert
verified.Should().BeFalse();
_output.WriteLine("✓ Truncated signature correctly rejected");
}
[Fact]
public async Task Verify_WrongKey_ReturnsFalse()
{
// Arrange
var provider = CreateProvider();
var signer1 = SetupSigner(provider, "key-1", seed: 1);
var signer2 = SetupSigner(provider, "key-2", seed: 2);
var message = Encoding.UTF8.GetBytes("Test message");
// Act - sign with key-1, verify with key-2
var signature = await signer1.SignAsync(message);
var verified = await signer2.VerifyAsync(message, signature);
// Assert
verified.Should().BeFalse();
_output.WriteLine("✓ Wrong key correctly rejected");
}
#endregion
#region Determinism Tests
[Fact]
public async Task Sign_SameMessage_ProducesSameSignature()
{
// Arrange
var provider = CreateProvider();
var signer = SetupSigner(provider, "key-determinism");
var message = Encoding.UTF8.GetBytes("Deterministic message");
// Act
var signature1 = await signer.SignAsync(message);
var signature2 = await signer.SignAsync(message);
// Assert - Ed25519 is deterministic
signature1.Should().BeEquivalentTo(signature2);
_output.WriteLine("✓ Ed25519 signing is deterministic");
}
[Fact]
public async Task Sign_DifferentKeys_ProduceDifferentSignatures()
{
// Arrange
var provider = CreateProvider();
var signer1 = SetupSigner(provider, "key-diff-1", seed: 100);
var signer2 = SetupSigner(provider, "key-diff-2", seed: 200);
var message = Encoding.UTF8.GetBytes("Same message");
// Act
var signature1 = await signer1.SignAsync(message);
var signature2 = await signer2.SignAsync(message);
// Assert
signature1.Should().NotBeEquivalentTo(signature2);
_output.WriteLine("✓ Different keys produce different signatures");
}
#endregion
#region JWK Export Tests
[Fact]
public async Task ExportPublicJwk_HasCorrectProperties()
{
// Arrange
var provider = CreateProvider();
var keyId = "key-jwk-export";
var signer = SetupSigner(provider, keyId);
// Act
var jwk = signer.ExportPublicJsonWebKey();
// Assert
jwk.Should().NotBeNull();
jwk.Kid.Should().Be(keyId);
jwk.Kty.Should().Be("OKP");
jwk.Crv.Should().Be("Ed25519");
jwk.Alg.Should().Be(SignatureAlgorithms.EdDsa);
jwk.Use.Should().Be("sig");
jwk.X.Should().NotBeNullOrEmpty("public key X coordinate required");
// Private key components should NOT be present
jwk.D.Should().BeNull("private key should not be exported");
_output.WriteLine("✓ JWK export has correct properties");
_output.WriteLine($" Kid: {jwk.Kid}");
_output.WriteLine($" Kty: {jwk.Kty}");
_output.WriteLine($" Crv: {jwk.Crv}");
_output.WriteLine($" Alg: {jwk.Alg}");
}
#endregion
#region Algorithm Alias Tests
[Fact]
public async Task Sign_EdDsaAlias_Works()
{
// Arrange
var provider = CreateProvider();
var keyId = "key-eddsa-alias";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
// Register with Ed25519
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
// Get signer using EdDSA alias
var signer = provider.GetSigner(SignatureAlgorithms.EdDsa, keyReference);
var message = Encoding.UTF8.GetBytes("Test with EdDSA alias");
// Act
var signature = await signer.SignAsync(message);
var verified = await signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
_output.WriteLine("✓ EdDSA alias works correctly");
}
#endregion
#region Registry Integration Tests
[Fact]
public async Task RegistryResolution_SignAndVerify_Succeeds()
{
// Arrange
var configuration = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddStellaOpsCrypto();
services.AddBouncyCastleEd25519Provider();
using var serviceProvider = services.BuildServiceProvider();
var registry = serviceProvider.GetRequiredService<ICryptoProviderRegistry>();
var bcProvider = serviceProvider.GetServices<ICryptoProvider>()
.OfType<BouncyCastleEd25519CryptoProvider>()
.Single();
var keyId = "registry-test-key";
var privateKeyBytes = Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, bcProvider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
bcProvider.UpsertSigningKey(signingKey);
// Act - resolve through registry
var resolution = registry.ResolveSigner(
CryptoCapability.Signing,
SignatureAlgorithms.Ed25519,
keyReference,
bcProvider.Name);
var message = Encoding.UTF8.GetBytes("Registry resolution test");
var signature = await resolution.Signer.SignAsync(message);
var verified = await resolution.Signer.VerifyAsync(message, signature);
// Assert
verified.Should().BeTrue();
resolution.ProviderName.Should().Be(bcProvider.Name);
_output.WriteLine("✓ Registry resolution sign/verify succeeded");
}
#endregion
#region Helper Methods
private static BouncyCastleEd25519CryptoProvider CreateProvider()
=> new();
private static ICryptoSigner SetupSigner(
BouncyCastleEd25519CryptoProvider provider,
string keyId,
int? seed = null)
{
var privateKeyBytes = seed.HasValue
? Enumerable.Range(seed.Value, 32).Select(i => (byte)(i % 256)).ToArray()
: Enumerable.Range(0, 32).Select(i => (byte)i).ToArray();
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
privateKeyBytes,
createdAt: DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
return provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
}
#endregion
}

View File

@@ -0,0 +1,24 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void DescribeKeys_ReturnsEmpty_WhenNoKeysConfigured()
{
var provider = CreateProvider();
if (provider is ICryptoProviderDiagnostics diagnostics)
{
var keys = diagnostics.DescribeKeys().ToList();
keys.Should().BeEmpty();
_output.WriteLine("✓ DescribeKeys returns empty when no keys configured");
}
else
{
_output.WriteLine("⚠ Provider does not implement ICryptoProviderDiagnostics");
}
}
}

View File

@@ -0,0 +1,48 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void GetSigner_UnregisteredKey_ThrowsKeyNotFoundException()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("nonexistent-key", provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, keyReference);
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ GetSigner with unregistered key → KeyNotFoundException");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
var provider = CreateProvider();
Action act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner with null key reference → ArgumentNullException");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void GetSigner_InvalidKeyId_ThrowsArgumentException(string? keyId)
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference(keyId!, provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine($"✓ GetSigner with invalid key ID '{keyId ?? "null"}' → ArgumentException");
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.Extensions.Options;
using StellaOps.Cryptography.Plugin.CryptoPro;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
private static CryptoProGostCryptoProvider CreateProvider()
{
var options = Options.Create(new CryptoProGostProviderOptions());
return new CryptoProGostCryptoProvider(options, null);
}
}

View File

@@ -0,0 +1,47 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void UpsertSigningKey_ThrowsNotSupported()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.GostR3410_2012_256,
new byte[32],
createdAt: FixedNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<NotSupportedException>()
.WithMessage("*managed externally*");
_output.WriteLine("✓ UpsertSigningKey throws NotSupportedException (keys are external)");
}
[Fact]
public void RemoveSigningKey_ReturnsFalse()
{
var provider = CreateProvider();
var result = provider.RemoveSigningKey("any-key");
result.Should().BeFalse("because CryptoPro keys are externally managed");
_output.WriteLine("✓ RemoveSigningKey returns false (keys are external)");
}
[Fact]
public void GetSigningKeys_ReturnsEmpty_WhenNoKeysConfigured()
{
var provider = CreateProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty when no keys configured");
}
}

View File

@@ -0,0 +1,18 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetPasswordHasher("argon2id");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
}

View File

@@ -0,0 +1,14 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider();
provider.Name.Should().Be("ru.cryptopro.csp");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
}

View File

@@ -0,0 +1,44 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void Supports_Signing_GostR3410_2012_256_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/GOST12-256");
}
[Fact]
public void Supports_Signing_GostR3410_2012_512_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.GostR3410_2012_512)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/GOST12-512");
}
[Theory]
[InlineData("gost12-256")]
[InlineData("GOST12-256")]
[InlineData("Gost12-256")]
[InlineData("gost12-512")]
[InlineData("GOST12-512")]
public void Supports_Signing_CaseInsensitive(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Case-insensitive match: {algorithm}");
}
}

View File

@@ -0,0 +1,42 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void AlgorithmSummary_DocumentsSupportedAlgorithms()
{
var provider = CreateProvider();
var supportedForSigning = new[]
{
SignatureAlgorithms.GostR3410_2012_256,
SignatureAlgorithms.GostR3410_2012_512
};
foreach (var algo in supportedForSigning)
{
provider.Supports(CryptoCapability.Signing, algo).Should().BeTrue();
provider.Supports(CryptoCapability.Verification, algo).Should().BeTrue();
}
_output.WriteLine("=== CryptoPro GOST Provider Algorithm Summary ===");
_output.WriteLine("Supported for Signing:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Supported for Verification:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Unsupported Capabilities:");
_output.WriteLine(" - PasswordHashing");
_output.WriteLine(" - ContentHashing");
_output.WriteLine(" - SymmetricEncryption");
_output.WriteLine(" - KeyDerivation");
_output.WriteLine("Note: Keys are managed externally via Windows certificate store");
}
}

View File

@@ -0,0 +1,30 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Theory]
[InlineData(SignatureAlgorithms.Ed25519)]
[InlineData(SignatureAlgorithms.EdDsa)]
[InlineData(SignatureAlgorithms.Es256)]
[InlineData(SignatureAlgorithms.Es384)]
[InlineData(SignatureAlgorithms.Es512)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData(SignatureAlgorithms.Dilithium3)]
[InlineData(SignatureAlgorithms.Falcon512)]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("HS256")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse($"because {algorithm} is not a GOST algorithm");
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
}

View File

@@ -0,0 +1,50 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.PasswordHashing, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.SymmetricEncryption, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ SymmetricEncryption capability not supported");
}
[Fact]
public void Supports_KeyDerivation_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.KeyDerivation, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ KeyDerivation capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.ContentHashing, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
}

View File

@@ -0,0 +1,28 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class CryptoProCapabilityDetectionTests
{
[Fact]
public void Supports_Verification_GostR3410_2012_256_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/GOST12-256");
}
[Fact]
public void Supports_Verification_GostR3410_2012_512_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.GostR3410_2012_512)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/GOST12-512");
}
}

View File

@@ -4,14 +4,9 @@
// Task: SIGNER-5100-007 - Repeat plugin tests for CryptoPro (GOST) plugin (Tasks 4-6 pattern)
// Description: Capability detection tests for CryptoPro GOST crypto plugin
// -----------------------------------------------------------------------------
using System.Runtime.Versioning;
using FluentAssertions;
using Microsoft.Extensions.Options;
using Xunit;
namespace StellaOps.Cryptography.Tests;
/// <summary>
/// Capability detection tests for the CryptoPro GOST crypto provider.
/// These tests validate the provider's capability declarations and algorithm support.
@@ -22,350 +17,13 @@ namespace StellaOps.Cryptography.Tests;
[Trait("Category", "GOST")]
[Trait("Category", "C1")]
[SupportedOSPlatform("windows")]
public sealed class CryptoProCapabilityDetectionTests
public sealed partial class CryptoProCapabilityDetectionTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
public CryptoProCapabilityDetectionTests(ITestOutputHelper output)
{
_output = output;
}
#region Provider Identity Tests
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider();
provider.Name.Should().Be("ru.cryptopro.csp");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
#endregion
#region GOST Signing Capability Tests
[Fact]
public void Supports_Signing_GostR3410_2012_256_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/GOST12-256");
}
[Fact]
public void Supports_Signing_GostR3410_2012_512_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.GostR3410_2012_512)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/GOST12-512");
}
[Theory]
[InlineData("gost12-256")]
[InlineData("GOST12-256")]
[InlineData("Gost12-256")]
[InlineData("gost12-512")]
[InlineData("GOST12-512")]
public void Supports_Signing_CaseInsensitive(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Case-insensitive match: {algorithm}");
}
#endregion
#region GOST Verification Capability Tests
[Fact]
public void Supports_Verification_GostR3410_2012_256_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/GOST12-256");
}
[Fact]
public void Supports_Verification_GostR3410_2012_512_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Verification, SignatureAlgorithms.GostR3410_2012_512)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/GOST12-512");
}
#endregion
#region Unsupported Algorithm Tests
[Theory]
[InlineData(SignatureAlgorithms.Ed25519)]
[InlineData(SignatureAlgorithms.EdDsa)]
[InlineData(SignatureAlgorithms.Es256)]
[InlineData(SignatureAlgorithms.Es384)]
[InlineData(SignatureAlgorithms.Es512)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData(SignatureAlgorithms.Dilithium3)]
[InlineData(SignatureAlgorithms.Falcon512)]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("HS256")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse($"because {algorithm} is not a GOST algorithm");
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
#endregion
#region Unsupported Capability Tests
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.PasswordHashing, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.SymmetricEncryption, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ SymmetricEncryption capability not supported");
}
[Fact]
public void Supports_KeyDerivation_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.KeyDerivation, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ KeyDerivation capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.ContentHashing, SignatureAlgorithms.GostR3410_2012_256)
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
#endregion
#region External Key Management Tests
[Fact]
public void UpsertSigningKey_ThrowsNotSupported()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.GostR3410_2012_256,
new byte[32],
createdAt: DateTimeOffset.UtcNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<NotSupportedException>()
.WithMessage("*managed externally*");
_output.WriteLine("✓ UpsertSigningKey throws NotSupportedException (keys are external)");
}
[Fact]
public void RemoveSigningKey_ReturnsFalse()
{
var provider = CreateProvider();
var result = provider.RemoveSigningKey("any-key");
result.Should().BeFalse("because CryptoPro keys are externally managed");
_output.WriteLine("✓ RemoveSigningKey returns false (keys are external)");
}
[Fact]
public void GetSigningKeys_ReturnsEmpty_WhenNoKeysConfigured()
{
var provider = CreateProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty when no keys configured");
}
#endregion
#region Password Hasher Tests
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetPasswordHasher("argon2id");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not expose password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
#endregion
#region Error Classification Tests
[Fact]
public void GetSigner_UnregisteredKey_ThrowsKeyNotFoundException()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("nonexistent-key", provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, keyReference);
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ GetSigner with unregistered key → KeyNotFoundException");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
var provider = CreateProvider();
Action act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner with null key reference → ArgumentNullException");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void GetSigner_InvalidKeyId_ThrowsArgumentException(string? keyId)
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference(keyId!, provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, keyReference);
act.Should().Throw<ArgumentException>();
_output.WriteLine($"✓ GetSigner with invalid key ID '{keyId ?? "null"}' → ArgumentException");
}
#endregion
#region Provider Diagnostics Tests
[Fact]
public void DescribeKeys_ReturnsEmpty_WhenNoKeysConfigured()
{
var provider = CreateProvider();
// CryptoProGostCryptoProvider implements ICryptoProviderDiagnostics
if (provider is ICryptoProviderDiagnostics diagnostics)
{
var keys = diagnostics.DescribeKeys().ToList();
keys.Should().BeEmpty();
_output.WriteLine("✓ DescribeKeys returns empty when no keys configured");
}
else
{
_output.WriteLine("⚠ Provider does not implement ICryptoProviderDiagnostics");
}
}
#endregion
#region Algorithm Summary
[Fact]
public void AlgorithmSummary_DocumentsSupportedAlgorithms()
{
var provider = CreateProvider();
var supportedForSigning = new[]
{
SignatureAlgorithms.GostR3410_2012_256,
SignatureAlgorithms.GostR3410_2012_512
};
foreach (var algo in supportedForSigning)
{
provider.Supports(CryptoCapability.Signing, algo).Should().BeTrue();
provider.Supports(CryptoCapability.Verification, algo).Should().BeTrue();
}
_output.WriteLine("=== CryptoPro GOST Provider Algorithm Summary ===");
_output.WriteLine("Supported for Signing:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Supported for Verification:");
foreach (var algo in supportedForSigning)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Unsupported Capabilities:");
_output.WriteLine(" - PasswordHashing");
_output.WriteLine(" - ContentHashing");
_output.WriteLine(" - SymmetricEncryption");
_output.WriteLine(" - KeyDerivation");
_output.WriteLine("Note: Keys are managed externally via Windows certificate store");
}
#endregion
#region Helper Methods
private static Plugin.CryptoPro.CryptoProGostCryptoProvider CreateProvider()
{
var options = Options.Create(new Plugin.CryptoPro.CryptoProGostProviderOptions());
return new Plugin.CryptoPro.CryptoProGostCryptoProvider(options, null);
}
#endregion
}

View File

@@ -1,27 +1,25 @@
#if STELLAOPS_CRYPTO_PRO
#if STELLAOPS_CRYPTO_PRO
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.CryptoPro;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class CryptoProGostSignerTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ExportPublicJsonWebKey_ContainsCertificateChain()
{
if (!OperatingSystem.IsWindows())
{
return; // CryptoPro CSP is Windows-only; skip on other platforms
}
if (!string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_CRYPTO_PRO_ENABLED"), "1", StringComparison.Ordinal))
{
return; // opt-in only when a Windows agent has CryptoPro CSP installed
@@ -29,7 +27,7 @@ public class CryptoProGostSignerTests
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var request = new CertificateRequest("CN=stellaops.test", ecdsa, HashAlgorithmName.SHA256);
using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
using var cert = request.CreateSelfSigned(FixedNow.AddDays(-1), FixedNow.AddDays(1));
var entry = new CryptoProGostKeyEntry(
"test-key",

View File

@@ -0,0 +1,15 @@
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Tests;
public partial class CryptoProviderRegistryTests
{
private sealed class FakeHasher : ICryptoHasher
{
public FakeHasher(string algorithmId) => AlgorithmId = algorithmId;
public string AlgorithmId { get; }
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
public string ComputeHashHex(ReadOnlySpan<byte> data) => Convert.ToHexStringLower(ComputeHash(data));
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Tests;
public partial class CryptoProviderRegistryTests
{
private sealed class FakeCryptoProvider : ICryptoProvider
{
private readonly Dictionary<string, FakeSigner> signers = new(StringComparer.Ordinal);
private readonly HashSet<(CryptoCapability Capability, string Algorithm)> supported;
private readonly Dictionary<string, FakeHasher> hashers = new(StringComparer.Ordinal);
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));
if (capability == CryptoCapability.ContentHashing && !hashers.ContainsKey(algorithm))
{
hashers[algorithm] = new FakeHasher(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 ICryptoHasher GetHasher(string algorithmId)
{
if (!hashers.TryGetValue(algorithmId, out var hasher))
{
throw new InvalidOperationException($"Hasher '{algorithmId}' not registered.");
}
return hasher;
}
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 ICryptoSigner CreateEphemeralVerifier(string algorithmId, ReadOnlySpan<byte> publicKeyBytes)
=> new FakeSigner(Name, "ephemeral-verifier", algorithmId);
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());
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Tests;
public partial class CryptoProviderRegistryTests
{
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
};
}
}

View File

@@ -1,18 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class CryptoProviderRegistryTests
public partial class CryptoProviderRegistryTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ResolveOrThrow_RespectsPreferredProviderOrder()
{
var providerA = new FakeCryptoProvider("providerA")
@@ -31,7 +24,7 @@ public class CryptoProviderRegistryTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ResolveSigner_UsesPreferredProviderHint()
{
var providerA = new FakeCryptoProvider("providerA")
@@ -63,7 +56,7 @@ public class CryptoProviderRegistryTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void RegistryOptions_UsesActiveProfileOrder()
{
var options = new StellaOps.Cryptography.DependencyInjection.CryptoProviderRegistryOptions();
@@ -78,128 +71,4 @@ public class CryptoProviderRegistryTests
Assert.Equal(new[] { "ru.cryptopro.csp", "ru.pkcs11" }, resolved);
}
private sealed class FakeCryptoProvider : ICryptoProvider
{
private readonly Dictionary<string, FakeSigner> signers = new(StringComparer.Ordinal);
private readonly HashSet<(CryptoCapability Capability, string Algorithm)> supported;
private readonly Dictionary<string, FakeHasher> hashers = new(StringComparer.Ordinal);
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));
if (capability == CryptoCapability.ContentHashing && !hashers.ContainsKey(algorithm))
{
hashers[algorithm] = new FakeHasher(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 ICryptoHasher GetHasher(string algorithmId)
{
if (!hashers.TryGetValue(algorithmId, out var hasher))
{
throw new InvalidOperationException($"Hasher '{algorithmId}' not registered.");
}
return hasher;
}
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 ICryptoSigner CreateEphemeralVerifier(string algorithmId, ReadOnlySpan<byte> publicKeyBytes)
=> new FakeSigner(Name, "ephemeral-verifier", algorithmId);
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
};
}
private sealed class FakeHasher : ICryptoHasher
{
public FakeHasher(string algorithmId) => AlgorithmId = algorithmId;
public string AlgorithmId { get; }
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
public string ComputeHashHex(ReadOnlySpan<byte> data) => Convert.ToHexStringLower(ComputeHash(data));
}
}

View File

@@ -0,0 +1,49 @@
using System.Security.Cryptography;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class DefaultCryptoHashTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Sha256_MatchesBcl()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = SHA256.HashData(_sample);
var actual = hash.ComputeHash(_sample, HashAlgorithms.Sha256);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Sha512_MatchesBcl()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = SHA512.HashData(_sample);
var actual = hash.ComputeHash(_sample, HashAlgorithms.Sha512);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Gost256_MatchesBouncyCastle()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = ComputeGostDigest(use256: true);
var actual = hash.ComputeHash(_sample, HashAlgorithms.Gost3411_2012_256);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Gost512_MatchesBouncyCastle()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = ComputeGostDigest(use256: false);
var actual = hash.ComputeHash(_sample, HashAlgorithms.Gost3411_2012_512);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
}

View File

@@ -0,0 +1,31 @@
using System.IO;
using System.Security.Cryptography;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class DefaultCryptoHashTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHashHex_Sha256_MatchesBclLowerHex()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = Convert.ToHexStringLower(SHA256.HashData(_sample));
var actual = hash.ComputeHashHex(_sample, HashAlgorithms.Sha256);
Assert.Equal(expected, actual);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComputeHashHex_Sha256_MatchesBclLowerHex_Async()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = Convert.ToHexStringLower(SHA256.HashData(_sample));
await using var stream = new MemoryStream(_sample);
var actual = await hash.ComputeHashHexAsync(stream, HashAlgorithms.Sha256);
Assert.Equal(expected, actual);
}
}

View File

@@ -0,0 +1,20 @@
using System.IO;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class DefaultCryptoHashTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComputeHash_Stream_MatchesBuffer_Async()
{
var hash = CryptoHashFactory.CreateDefault();
await using var stream = new MemoryStream(_sample);
var streamDigest = await hash.ComputeHashAsync(stream, HashAlgorithms.Sha256);
var bufferDigest = hash.ComputeHash(_sample, HashAlgorithms.Sha256);
Assert.Equal(Convert.ToHexString(bufferDigest), Convert.ToHexString(streamDigest));
}
}

View File

@@ -1,99 +1,19 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Tests;
public sealed class DefaultCryptoHashTests
public sealed partial class DefaultCryptoHashTests
{
private static readonly byte[] Sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Sha256_MatchesBcl()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = SHA256.HashData(Sample);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Sha256);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Sha512_MatchesBcl()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = SHA512.HashData(Sample);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Sha512);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Gost256_MatchesBouncyCastle()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = ComputeGostDigest(use256: true);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Gost3411_2012_256);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Gost512_MatchesBouncyCastle()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = ComputeGostDigest(use256: false);
var actual = hash.ComputeHash(Sample, HashAlgorithms.Gost3411_2012_512);
Assert.Equal(Convert.ToHexStringLower(expected), Convert.ToHexStringLower(actual));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComputeHashAsync_Stream_MatchesBuffer()
{
var hash = CryptoHashFactory.CreateDefault();
await using var stream = new MemoryStream(Sample);
var streamDigest = await hash.ComputeHashAsync(stream, HashAlgorithms.Sha256);
var bufferDigest = hash.ComputeHash(Sample, HashAlgorithms.Sha256);
Assert.Equal(Convert.ToHexString(bufferDigest), Convert.ToHexString(streamDigest));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHashHex_Sha256_MatchesBclLowerHex()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = Convert.ToHexStringLower(SHA256.HashData(Sample));
var actual = hash.ComputeHashHex(Sample, HashAlgorithms.Sha256);
Assert.Equal(expected, actual);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComputeHashHexAsync_Sha256_MatchesBclLowerHex()
{
var hash = CryptoHashFactory.CreateDefault();
var expected = Convert.ToHexStringLower(SHA256.HashData(Sample));
await using var stream = new MemoryStream(Sample);
var actual = await hash.ComputeHashHexAsync(stream, HashAlgorithms.Sha256);
Assert.Equal(expected, actual);
}
private static readonly byte[] _sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
private static byte[] ComputeGostDigest(bool use256)
{
Org.BouncyCastle.Crypto.IDigest digest = use256
IDigest digest = use256
? new Gost3411_2012_256Digest()
: new Gost3411_2012_512Digest();
digest.BlockUpdate(Sample, 0, Sample.Length);
digest.BlockUpdate(_sample, 0, _sample.Length);
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;

View File

@@ -2,37 +2,36 @@
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Tests;
public sealed class DefaultCryptoHmacTests
{
private static readonly byte[] Sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
private static readonly byte[] Key = Encoding.UTF8.GetBytes("test-key");
private static readonly byte[] _sample = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
private static readonly byte[] _key = Encoding.UTF8.GetBytes("test-key");
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ComputeHmacHexForPurpose_WebhookInterop_MatchesBclLowerHex()
{
var hmac = DefaultCryptoHmac.CreateForTests();
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(Key, Sample));
var actual = hmac.ComputeHmacHexForPurpose(Key, Sample, HmacPurpose.WebhookInterop);
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(_key, _sample));
var actual = hmac.ComputeHmacHexForPurpose(_key, _sample, HmacPurpose.WebhookInterop);
Assert.Equal(expected, actual);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ComputeHmacHexForPurposeAsync_WebhookInterop_MatchesBclLowerHex()
[Fact]
public async Task ComputeHmacHexForPurpose_WebhookInterop_MatchesBclLowerHexAsync()
{
var hmac = DefaultCryptoHmac.CreateForTests();
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(Key, Sample));
await using var stream = new MemoryStream(Sample);
var actual = await hmac.ComputeHmacHexForPurposeAsync(Key, stream, HmacPurpose.WebhookInterop);
var expected = Convert.ToHexStringLower(HMACSHA256.HashData(_key, _sample));
await using var stream = new MemoryStream(_sample);
var actual = await hmac.ComputeHmacHexForPurposeAsync(_key, stream, HmacPurpose.WebhookInterop);
Assert.Equal(expected, actual);
}
}

View File

@@ -2,20 +2,21 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Tests;
public class DefaultCryptoProviderSigningTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UpsertSigningKey_AllowsSignAndVerifyEs256()
[Fact]
public async Task UpsertSigningKey_AllowsSignAndVerifyEs256Async()
{
var provider = new DefaultCryptoProvider();
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
@@ -25,7 +26,7 @@ public class DefaultCryptoProviderSigningTests
new CryptoKeyReference("revocation-key"),
SignatureAlgorithms.Es256,
privateParameters: in parameters,
createdAt: DateTimeOffset.UtcNow);
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);
@@ -56,13 +57,13 @@ public class DefaultCryptoProviderSigningTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[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);
var signingKey = new CryptoSigningKey(new CryptoKeyReference("key-to-remove"), SignatureAlgorithms.Es256, in parameters, FixedNow);
provider.UpsertSigningKey(signingKey);
Assert.True(provider.RemoveSigningKey(signingKey.Reference.KeyId));

View File

@@ -0,0 +1,34 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Theory]
[InlineData("ECDSA-P256")]
[InlineData("ECDSA-P384")]
[InlineData("ECDSA-P521")]
public void Supports_Signing_EcdsaCurves_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
[Theory]
[InlineData("ECDSA-P256")]
[InlineData("ECDSA-P384")]
[InlineData("ECDSA-P521")]
public void Supports_Verification_EcdsaCurves_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Verification, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Verification/{algorithm}");
}
}

View File

@@ -0,0 +1,19 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Theory]
[InlineData("EdDSA-Ed25519")]
[InlineData("EdDSA-Ed448")]
public void Supports_Signing_EdDsa_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
}

View File

@@ -0,0 +1,31 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetPasswordHasher("PBKDF2");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not support password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
[Fact]
public void GetHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetHasher("SHA256");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not support content hashing*");
_output.WriteLine("✓ GetHasher throws NotSupportedException");
}
}

View File

@@ -0,0 +1,82 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Cryptography.Plugin.EIDAS;
using StellaOps.Cryptography.Plugin.EIDAS.Configuration;
using StellaOps.Cryptography.Plugin.EIDAS.Models;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
private static EidasCryptoProvider CreateProvider()
{
var services = new ServiceCollection();
services.Configure<EidasOptions>(options =>
{
options.SignatureLevel = SignatureLevel.AdES;
options.SignatureFormat = SignatureFormat.CAdES;
options.DefaultAlgorithm = "ECDSA-P256";
options.DigestAlgorithm = "SHA256";
options.Local = new LocalSigningOptions
{
Type = "PKCS12",
Path = "/tmp/test.p12",
Password = "test"
};
options.Tsp = new TspOptions
{
Endpoint = "https://tsp.example.com",
ApiKey = "test-key"
};
});
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Warning));
services.AddHttpClient<TrustServiceProviderClient>();
services.AddSingleton<LocalEidasProvider>();
services.AddSingleton<EidasCryptoProvider>();
var serviceProvider = services.BuildServiceProvider();
return serviceProvider.GetRequiredService<EidasCryptoProvider>();
}
private static EidasCryptoProvider CreateProviderWithKey(string keyId)
{
var services = new ServiceCollection();
services.Configure<EidasOptions>(options =>
{
options.SignatureLevel = SignatureLevel.AdES;
options.SignatureFormat = SignatureFormat.CAdES;
options.DefaultAlgorithm = "ECDSA-P256";
options.DigestAlgorithm = "SHA256";
options.Keys.Add(new EidasKeyConfig
{
KeyId = keyId,
Source = "local"
});
options.Local = new LocalSigningOptions
{
Type = "PKCS12",
Path = "/tmp/test.p12",
Password = "test"
};
options.Tsp = new TspOptions
{
Endpoint = "https://tsp.example.com",
ApiKey = "test-key"
};
});
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Warning));
services.AddHttpClient<TrustServiceProviderClient>();
services.AddSingleton<LocalEidasProvider>();
services.AddSingleton<EidasCryptoProvider>();
var serviceProvider = services.BuildServiceProvider();
return serviceProvider.GetRequiredService<EidasCryptoProvider>();
}
}

View File

@@ -0,0 +1,14 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider();
provider.Name.Should().Be("eidas");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
}

View File

@@ -0,0 +1,19 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Theory]
[InlineData("RSA-PSS-2048")]
[InlineData("RSA-PSS-4096")]
public void Supports_Signing_RsaPss_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
}

View File

@@ -0,0 +1,21 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Fact]
public void GetSigner_ReturnsEidasSigner()
{
var provider = CreateProviderWithKey("test-key-local");
var keyRef = new CryptoKeyReference("test-key-local");
var signer = provider.GetSigner("ECDSA-P256", keyRef);
signer.Should().NotBeNull();
signer.KeyId.Should().Be("test-key-local");
signer.AlgorithmId.Should().Be("ECDSA-P256");
_output.WriteLine("✓ GetSigner returns valid EidasSigner");
}
}

View File

@@ -0,0 +1,83 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Fact]
public void UpsertSigningKey_AddsKey()
{
var provider = CreateProvider();
var keyRef = new CryptoKeyReference("upsert-test");
var signingKey = new CryptoSigningKey(
keyRef,
"ECDSA-P256",
new byte[] { 1, 2, 3, 4 },
FixedNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().Contain(k => k.Reference.KeyId == "upsert-test");
_output.WriteLine("✓ UpsertSigningKey adds key");
}
[Fact]
public void UpsertSigningKey_UpdatesExistingKey()
{
var provider = CreateProvider();
var keyRef = new CryptoKeyReference("update-test");
var signingKey1 = new CryptoSigningKey(keyRef, "ECDSA-P256", new byte[] { 1, 2, 3 }, FixedNow);
var signingKey2 = new CryptoSigningKey(keyRef, "ECDSA-P384", new byte[] { 4, 5, 6 }, FixedNow);
provider.UpsertSigningKey(signingKey1);
provider.UpsertSigningKey(signingKey2);
var keys = provider.GetSigningKeys();
keys.Should().ContainSingle(k => k.Reference.KeyId == "update-test");
keys.Single(k => k.Reference.KeyId == "update-test").AlgorithmId.Should().Be("ECDSA-P384");
_output.WriteLine("✓ UpsertSigningKey updates existing key");
}
[Fact]
public void RemoveSigningKey_RemovesExistingKey()
{
var provider = CreateProvider();
var keyRef = new CryptoKeyReference("remove-test");
var signingKey = new CryptoSigningKey(keyRef, "ECDSA-P256", new byte[] { 1, 2, 3, 4 }, FixedNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().Contain(k => k.Reference.KeyId == "remove-test");
var result = provider.RemoveSigningKey("remove-test");
result.Should().BeTrue();
provider.GetSigningKeys().Should().NotContain(k => k.Reference.KeyId == "remove-test");
_output.WriteLine("✓ RemoveSigningKey removes existing key");
}
[Fact]
public void RemoveSigningKey_NonExistentKey_ReturnsFalse()
{
var provider = CreateProvider();
var result = provider.RemoveSigningKey("nonexistent");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false for non-existent key");
}
[Fact]
public void GetSigningKeys_InitiallyEmpty()
{
var provider = CreateProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty initially");
}
}

View File

@@ -0,0 +1,45 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Fact]
public void AlgorithmSummary_DocumentsSupportedAlgorithms()
{
var provider = CreateProvider();
var supportedAlgorithms = new[]
{
"ECDSA-P256",
"ECDSA-P384",
"ECDSA-P521",
"RSA-PSS-2048",
"RSA-PSS-4096",
"EdDSA-Ed25519",
"EdDSA-Ed448"
};
foreach (var algo in supportedAlgorithms)
{
provider.Supports(CryptoCapability.Signing, algo).Should().BeTrue();
provider.Supports(CryptoCapability.Verification, algo).Should().BeTrue();
}
_output.WriteLine("=== eIDAS Provider Algorithm Summary ===");
_output.WriteLine("Supported for Signing & Verification:");
foreach (var algo in supportedAlgorithms)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Unsupported Capabilities:");
_output.WriteLine(" - PasswordHashing");
_output.WriteLine(" - ContentHashing");
_output.WriteLine(" - SymmetricEncryption");
_output.WriteLine(" - KeyDerivation");
_output.WriteLine("Signature Levels:");
_output.WriteLine(" - QES (Qualified Electronic Signature)");
_output.WriteLine(" - AES (Advanced Electronic Signature)");
_output.WriteLine(" - AdES (Standard Electronic Signature)");
}
}

View File

@@ -0,0 +1,27 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Theory]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("HS256")]
[InlineData("PS256")]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData("UNKNOWN-ALGO")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse($"because {algorithm} is not supported by eIDAS provider");
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
}

View File

@@ -0,0 +1,50 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class EidasCapabilityDetectionTests
{
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.PasswordHashing, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.ContentHashing, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.SymmetricEncryption, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ SymmetricEncryption capability not supported");
}
[Fact]
public void Supports_KeyDerivation_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.KeyDerivation, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ KeyDerivation capability not supported");
}
}

View File

@@ -4,18 +4,8 @@
// Task: SIGNER-5100-008 - Repeat plugin tests for eIDAS plugin (Tasks 4-6 pattern)
// Description: Capability detection and error classification tests for eIDAS crypto plugin
// -----------------------------------------------------------------------------
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography.Plugin.EIDAS;
using StellaOps.Cryptography.Plugin.EIDAS.Configuration;
using StellaOps.Cryptography.Plugin.EIDAS.Models;
using Xunit;
namespace StellaOps.Cryptography.Tests;
/// <summary>
/// Capability detection and error classification tests for the eIDAS crypto provider.
/// Validates:
@@ -27,415 +17,13 @@ namespace StellaOps.Cryptography.Tests;
[Trait("Category", "CryptoPlugin")]
[Trait("Category", "eIDAS")]
[Trait("Category", "C1")]
public sealed class EidasCapabilityDetectionTests
public sealed partial class EidasCapabilityDetectionTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
public EidasCapabilityDetectionTests(ITestOutputHelper output)
{
_output = output;
}
#region Provider Identity Tests
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider();
provider.Name.Should().Be("eidas");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
#endregion
#region ECDSA Capability Tests
[Theory]
[InlineData("ECDSA-P256")]
[InlineData("ECDSA-P384")]
[InlineData("ECDSA-P521")]
public void Supports_Signing_EcdsaCurves_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
[Theory]
[InlineData("ECDSA-P256")]
[InlineData("ECDSA-P384")]
[InlineData("ECDSA-P521")]
public void Supports_Verification_EcdsaCurves_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Verification, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Verification/{algorithm}");
}
#endregion
#region RSA-PSS Capability Tests
[Theory]
[InlineData("RSA-PSS-2048")]
[InlineData("RSA-PSS-4096")]
public void Supports_Signing_RsaPss_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
#endregion
#region EdDSA Capability Tests
[Theory]
[InlineData("EdDSA-Ed25519")]
[InlineData("EdDSA-Ed448")]
public void Supports_Signing_EdDsa_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
#endregion
#region Unsupported Algorithm Tests
[Theory]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("ES256")] // Not the eIDAS format
[InlineData("ES384")]
[InlineData("HS256")]
[InlineData("PS256")]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData("UNKNOWN-ALGO")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse($"because {algorithm} is not supported by eIDAS provider");
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
#endregion
#region Unsupported Capability Tests
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.PasswordHashing, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.ContentHashing, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.SymmetricEncryption, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ SymmetricEncryption capability not supported");
}
[Fact]
public void Supports_KeyDerivation_ReturnsFalse()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.KeyDerivation, "ECDSA-P256")
.Should().BeFalse();
_output.WriteLine("✓ KeyDerivation capability not supported");
}
#endregion
#region Hasher/PasswordHasher Tests
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetPasswordHasher("PBKDF2");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not support password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
[Fact]
public void GetHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetHasher("SHA256");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not support content hashing*");
_output.WriteLine("✓ GetHasher throws NotSupportedException");
}
#endregion
#region Signer Retrieval Tests
[Fact]
public void GetSigner_ReturnsEidasSigner()
{
var provider = CreateProviderWithKey("test-key-local");
var keyRef = new CryptoKeyReference("test-key-local");
var signer = provider.GetSigner("ECDSA-P256", keyRef);
signer.Should().NotBeNull();
signer.KeyId.Should().Be("test-key-local");
signer.AlgorithmId.Should().Be("ECDSA-P256");
_output.WriteLine("✓ GetSigner returns valid EidasSigner");
}
#endregion
#region Signing Key Management Tests
[Fact]
public void UpsertSigningKey_AddsKey()
{
var provider = CreateProvider();
var keyRef = new CryptoKeyReference("upsert-test");
var signingKey = new CryptoSigningKey(
keyRef,
"ECDSA-P256",
new byte[] { 1, 2, 3, 4 },
DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().Contain(k => k.Reference.KeyId == "upsert-test");
_output.WriteLine("✓ UpsertSigningKey adds key");
}
[Fact]
public void UpsertSigningKey_UpdatesExistingKey()
{
var provider = CreateProvider();
var keyRef = new CryptoKeyReference("update-test");
var signingKey1 = new CryptoSigningKey(keyRef, "ECDSA-P256", new byte[] { 1, 2, 3 }, DateTimeOffset.UtcNow);
var signingKey2 = new CryptoSigningKey(keyRef, "ECDSA-P384", new byte[] { 4, 5, 6 }, DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey1);
provider.UpsertSigningKey(signingKey2);
var keys = provider.GetSigningKeys();
keys.Should().ContainSingle(k => k.Reference.KeyId == "update-test");
keys.Single(k => k.Reference.KeyId == "update-test").AlgorithmId.Should().Be("ECDSA-P384");
_output.WriteLine("✓ UpsertSigningKey updates existing key");
}
[Fact]
public void RemoveSigningKey_RemovesExistingKey()
{
var provider = CreateProvider();
var keyRef = new CryptoKeyReference("remove-test");
var signingKey = new CryptoSigningKey(keyRef, "ECDSA-P256", new byte[] { 1, 2, 3, 4 }, DateTimeOffset.UtcNow);
provider.UpsertSigningKey(signingKey);
provider.GetSigningKeys().Should().Contain(k => k.Reference.KeyId == "remove-test");
var result = provider.RemoveSigningKey("remove-test");
result.Should().BeTrue();
provider.GetSigningKeys().Should().NotContain(k => k.Reference.KeyId == "remove-test");
_output.WriteLine("✓ RemoveSigningKey removes existing key");
}
[Fact]
public void RemoveSigningKey_NonExistentKey_ReturnsFalse()
{
var provider = CreateProvider();
var result = provider.RemoveSigningKey("nonexistent");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false for non-existent key");
}
[Fact]
public void GetSigningKeys_InitiallyEmpty()
{
var provider = CreateProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty initially");
}
#endregion
#region Algorithm Summary
[Fact]
public void AlgorithmSummary_DocumentsSupportedAlgorithms()
{
var provider = CreateProvider();
var supportedAlgorithms = new[]
{
"ECDSA-P256",
"ECDSA-P384",
"ECDSA-P521",
"RSA-PSS-2048",
"RSA-PSS-4096",
"EdDSA-Ed25519",
"EdDSA-Ed448"
};
foreach (var algo in supportedAlgorithms)
{
provider.Supports(CryptoCapability.Signing, algo).Should().BeTrue();
provider.Supports(CryptoCapability.Verification, algo).Should().BeTrue();
}
_output.WriteLine("=== eIDAS Provider Algorithm Summary ===");
_output.WriteLine("Supported for Signing & Verification:");
foreach (var algo in supportedAlgorithms)
{
_output.WriteLine($" - {algo}");
}
_output.WriteLine("Unsupported Capabilities:");
_output.WriteLine(" - PasswordHashing");
_output.WriteLine(" - ContentHashing");
_output.WriteLine(" - SymmetricEncryption");
_output.WriteLine(" - KeyDerivation");
_output.WriteLine("Signature Levels:");
_output.WriteLine(" - QES (Qualified Electronic Signature)");
_output.WriteLine(" - AES (Advanced Electronic Signature)");
_output.WriteLine(" - AdES (Standard Electronic Signature)");
}
#endregion
#region Helper Methods
private static EidasCryptoProvider CreateProvider()
{
var services = new ServiceCollection();
services.Configure<EidasOptions>(options =>
{
options.SignatureLevel = SignatureLevel.AdES;
options.SignatureFormat = SignatureFormat.CAdES;
options.DefaultAlgorithm = "ECDSA-P256";
options.DigestAlgorithm = "SHA256";
options.Local = new LocalSigningOptions
{
Type = "PKCS12",
Path = "/tmp/test.p12",
Password = "test"
};
options.Tsp = new TspOptions
{
Endpoint = "https://tsp.example.com",
ApiKey = "test-key"
};
});
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Warning));
services.AddHttpClient<TrustServiceProviderClient>();
services.AddSingleton<LocalEidasProvider>();
services.AddSingleton<EidasCryptoProvider>();
var serviceProvider = services.BuildServiceProvider();
return serviceProvider.GetRequiredService<EidasCryptoProvider>();
}
private static EidasCryptoProvider CreateProviderWithKey(string keyId)
{
var services = new ServiceCollection();
services.Configure<EidasOptions>(options =>
{
options.SignatureLevel = SignatureLevel.AdES;
options.SignatureFormat = SignatureFormat.CAdES;
options.DefaultAlgorithm = "ECDSA-P256";
options.DigestAlgorithm = "SHA256";
options.Keys.Add(new EidasKeyConfig
{
KeyId = keyId,
Source = "local"
});
options.Local = new LocalSigningOptions
{
Type = "PKCS12",
Path = "/tmp/test.p12",
Password = "test"
};
options.Tsp = new TspOptions
{
Endpoint = "https://tsp.example.com",
ApiKey = "test-key"
};
});
services.AddLogging(builder => builder.SetMinimumLevel(LogLevel.Warning));
services.AddHttpClient<TrustServiceProviderClient>();
services.AddSingleton<LocalEidasProvider>();
services.AddSingleton<EidasCryptoProvider>();
var serviceProvider = services.BuildServiceProvider();
return serviceProvider.GetRequiredService<EidasCryptoProvider>();
}
#endregion
}

View File

@@ -3,15 +3,14 @@ using System;
using System.Linq;
using System.Security.Cryptography;
using StellaOps.Cryptography.Plugin.CryptoPro;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class GostSignatureEncodingTests
{
[Trait("Category", TestCategories.Unit)]
[Theory]
[Theory]
[InlineData(32)]
[InlineData(64)]
public void RawAndDer_RoundTrip(int coordinateLength)
@@ -28,7 +27,7 @@ public class GostSignatureEncodingTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ToDer_Throws_When_Length_Invalid()
{
var raw = new byte[10];
@@ -36,7 +35,7 @@ public class GostSignatureEncodingTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ToRaw_Throws_When_Not_Der()
{
var raw = new byte[64];

View File

@@ -0,0 +1,68 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void Supports_Signing_Es256_ReturnsTrue()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.Signing, KmsAlgorithms.Es256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/ES256");
}
[Fact]
public void Supports_Verification_Es256_ReturnsTrue()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.Verification, KmsAlgorithms.Es256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/ES256");
}
[Theory]
[InlineData(SignatureAlgorithms.Ed25519)]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData("RS256")]
[InlineData("RS512")]
[InlineData("PS256")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse();
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.PasswordHashing, KmsAlgorithms.Es256)
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.ContentHashing, KmsAlgorithms.Es256)
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
}

View File

@@ -0,0 +1,21 @@
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void ErrorCodeMappingSummary_DocumentsExpectedMappings()
{
_output.WriteLine("=== KMS/HSM Error Code Mapping Reference ===");
_output.WriteLine(" KeyNotFoundException → KMS_KEY_NOT_FOUND");
_output.WriteLine(" InvalidOperationException → KMS_INVALID_OPERATION");
_output.WriteLine(" ArgumentNullException → KMS_INVALID_ARGUMENT");
_output.WriteLine("");
_output.WriteLine("Backend-specific errors:");
_output.WriteLine(" AWS KMS: KMSNotFoundException → KMS_KEY_NOT_FOUND");
_output.WriteLine(" AWS KMS: KMSInvalidStateException → KMS_KEY_DISABLED");
_output.WriteLine(" GCP KMS: StatusCode.NotFound → KMS_KEY_NOT_FOUND");
_output.WriteLine(" GCP KMS: StatusCode.FailedPrecondition → KMS_KEY_DISABLED");
_output.WriteLine(" PKCS#11: CKR_KEY_HANDLE_INVALID → KMS_KEY_NOT_FOUND");
_output.WriteLine(" PKCS#11: CKR_TOKEN_NOT_PRESENT → KMS_HSM_UNAVAILABLE");
}
}

View File

@@ -0,0 +1,31 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void GetPasswordHasher_ThrowsInvalidOperationException()
{
var provider = CreateProvider(new MockKmsClient());
Action act = () => provider.GetPasswordHasher("PBKDF2");
act.Should().Throw<InvalidOperationException>()
.WithMessage("*does not support password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws InvalidOperationException");
}
[Fact]
public void GetHasher_ThrowsInvalidOperationException()
{
var provider = CreateProvider(new MockKmsClient());
Action act = () => provider.GetHasher("SHA-256");
act.Should().Throw<InvalidOperationException>()
.WithMessage("*does not support content hashing*");
_output.WriteLine("✓ GetHasher throws InvalidOperationException");
}
}

View File

@@ -0,0 +1,21 @@
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
private static KmsCryptoProvider CreateProvider(IKmsClient kmsClient)
=> new(kmsClient);
private static void RegisterKey(KmsCryptoProvider provider, string keyId)
{
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var metadata = new Dictionary<string, string?> { ["kms.version"] = "v1" };
var signingKey = new CryptoSigningKey(
keyReference,
KmsAlgorithms.Es256,
new byte[32],
FixedNow,
metadata: metadata);
provider.UpsertSigningKey(signingKey);
}
}

View File

@@ -0,0 +1,67 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void UpsertSigningKey_ValidKey_Succeeds()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("kms-key-001", provider.Name);
var metadata = new Dictionary<string, string?> { ["kms.version"] = "v1" };
var signingKey = new CryptoSigningKey(
keyReference,
KmsAlgorithms.Es256,
new byte[32],
FixedNow,
metadata: metadata);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().NotThrow();
_output.WriteLine("✓ UpsertSigningKey with valid key succeeds");
}
[Fact]
public void UpsertSigningKey_MissingVersionMetadata_Throws()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("kms-key-001", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
KmsAlgorithms.Es256,
new byte[32],
FixedNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*kms.version*");
_output.WriteLine("✓ UpsertSigningKey without version metadata → InvalidOperationException");
}
[Fact]
public void UpsertSigningKey_UnsupportedAlgorithm_Throws()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("kms-key-001", provider.Name);
var metadata = new Dictionary<string, string?> { ["kms.version"] = "v1" };
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
new byte[32],
FixedNow,
metadata: metadata);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*only supports*");
_output.WriteLine("✓ UpsertSigningKey with unsupported algorithm → InvalidOperationException");
}
}

View File

@@ -0,0 +1,46 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void RemoveSigningKey_ExistingKey_ReturnsTrue()
{
var provider = CreateProvider(new MockKmsClient());
var keyId = "kms-key-to-remove";
RegisterKey(provider, keyId);
var result = provider.RemoveSigningKey(keyId);
result.Should().BeTrue();
_output.WriteLine("✓ RemoveSigningKey removes existing key");
}
[Fact]
public void RemoveSigningKey_NonExistentKey_ReturnsFalse()
{
var provider = CreateProvider(new MockKmsClient());
var result = provider.RemoveSigningKey("nonexistent");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false for non-existent key");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void RemoveSigningKey_InvalidKeyId_ReturnsFalse(string? keyId)
{
var provider = CreateProvider(new MockKmsClient());
var result = provider.RemoveSigningKey(keyId!);
result.Should().BeFalse();
_output.WriteLine($"✓ RemoveSigningKey with '{keyId ?? "null"}' returns false");
}
}

View File

@@ -0,0 +1,64 @@
using FluentAssertions;
using StellaOps.Cryptography.Kms;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public async Task MockKmsClient_Sign_ReturnsValidSignatureAsync()
{
var mockClient = new MockKmsClient();
var data = new byte[] { 1, 2, 3, 4, 5 };
var result = await mockClient.SignAsync("test-key", "v1", data);
result.Should().NotBeNull();
result.Signature.Should().NotBeEmpty();
result.KeyId.Should().Be("test-key");
_output.WriteLine("✓ MockKmsClient.SignAsync returns valid result");
}
[Fact]
public async Task MockKmsClient_Verify_MatchingSignature_ReturnsTrueAsync()
{
var mockClient = new MockKmsClient();
var data = new byte[] { 1, 2, 3, 4, 5 };
var signResult = await mockClient.SignAsync("test-key", "v1", data);
var verified = await mockClient.VerifyAsync("test-key", "v1", data, signResult.Signature);
verified.Should().BeTrue();
_output.WriteLine("✓ MockKmsClient sign/verify roundtrip succeeds");
}
[Fact]
public async Task MockKmsClient_Verify_TamperedData_ReturnsFalseAsync()
{
var mockClient = new MockKmsClient();
var data = new byte[] { 1, 2, 3, 4, 5 };
var tamperedData = new byte[] { 1, 2, 3, 4, 6 };
var signResult = await mockClient.SignAsync("test-key", "v1", data);
var verified = await mockClient.VerifyAsync("test-key", "v1", tamperedData, signResult.Signature);
verified.Should().BeFalse();
_output.WriteLine("✓ MockKmsClient correctly rejects tampered data");
}
[Fact]
public async Task MockKmsClient_GetMetadata_ReturnsKeyInfoAsync()
{
var mockClient = new MockKmsClient();
var metadata = await mockClient.GetMetadataAsync("test-key");
metadata.Should().NotBeNull();
metadata.KeyId.Should().Be("test-key");
metadata.State.Should().Be(KmsKeyState.Active);
_output.WriteLine("✓ MockKmsClient.GetMetadataAsync returns valid metadata");
}
}

View File

@@ -0,0 +1,91 @@
using StellaOps.Cryptography.Kms;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
private sealed class MockKmsClient : IKmsClient
{
private readonly Dictionary<string, byte[]> _signatures = new();
public Task<KmsSignResult> SignAsync(
string keyId,
string? keyVersion,
ReadOnlyMemory<byte> data,
CancellationToken cancellationToken = default)
{
var signature = System.Security.Cryptography.SHA256.HashData(data.Span);
var signatureKey = $"{keyId}:{keyVersion}:{Convert.ToBase64String(data.ToArray())}";
_signatures[signatureKey] = signature;
return Task.FromResult(new KmsSignResult(keyId, keyVersion ?? "v1", KmsAlgorithms.Es256, signature));
}
public Task<bool> VerifyAsync(
string keyId,
string? keyVersion,
ReadOnlyMemory<byte> data,
ReadOnlyMemory<byte> signature,
CancellationToken cancellationToken = default)
{
var expectedSignature = System.Security.Cryptography.SHA256.HashData(data.Span);
return Task.FromResult(signature.Span.SequenceEqual(expectedSignature));
}
public Task<KmsKeyMetadata> GetMetadataAsync(string keyId, CancellationToken cancellationToken = default)
{
var versions = System.Collections.Immutable.ImmutableArray.Create(
new KmsKeyVersionMetadata(
"v1",
KmsKeyState.Active,
FixedNow.AddMonths(-1),
null,
string.Empty,
"P-256"));
var metadata = new KmsKeyMetadata(
keyId,
KmsAlgorithms.Es256,
KmsKeyState.Active,
FixedNow.AddMonths(-1),
versions);
return Task.FromResult(metadata);
}
public Task<KmsKeyMaterial> ExportAsync(
string keyId,
string? keyVersion,
CancellationToken cancellationToken = default)
{
var material = new KmsKeyMaterial(
keyId,
keyVersion ?? "v1",
KmsAlgorithms.Es256,
"P-256",
D: Array.Empty<byte>(),
Qx: new byte[32],
Qy: new byte[32],
FixedNow.AddMonths(-1));
return Task.FromResult(material);
}
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
{
var versions = System.Collections.Immutable.ImmutableArray.Create(
new KmsKeyVersionMetadata(
"v2",
KmsKeyState.Active,
FixedNow,
null,
string.Empty,
"P-256"));
var metadata = new KmsKeyMetadata(
keyId,
KmsAlgorithms.Es256,
KmsKeyState.Active,
FixedNow,
versions);
return Task.FromResult(metadata);
}
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
}

View File

@@ -0,0 +1,15 @@
using FluentAssertions;
using StellaOps.Cryptography.Kms;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider(new MockKmsClient());
provider.Name.Should().Be("kms");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
}

View File

@@ -0,0 +1,66 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Kms;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class KmsHsmConnectorTests
{
[Fact]
public void GetSigner_RegisteredKey_ReturnsKmsSigner()
{
var mockClient = new MockKmsClient();
var provider = CreateProvider(mockClient);
var keyId = "kms-key-signer";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signer = provider.GetSigner(KmsAlgorithms.Es256, keyReference);
signer.Should().NotBeNull();
signer.Should().BeAssignableTo<ICryptoSigner>();
_output.WriteLine("✓ GetSigner returns KmsSigner");
}
[Fact]
public void GetSigner_UnregisteredKey_ThrowsKeyNotFoundException()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("unregistered", provider.Name);
Action act = () => provider.GetSigner(KmsAlgorithms.Es256, keyReference);
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ GetSigner with unregistered key → KeyNotFoundException");
}
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
var provider = CreateProvider(new MockKmsClient());
var keyId = "kms-key-wrong-algo";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ GetSigner with unsupported algorithm → InvalidOperationException");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
var provider = CreateProvider(new MockKmsClient());
Action act = () => provider.GetSigner(KmsAlgorithms.Es256, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner with null key reference → ArgumentNullException");
}
}

View File

@@ -4,13 +4,8 @@
// Task: SIGNER-5100-010 - Add KMS/HSM connector tests (remote signing providers): fixture-based request/response snapshots
// Description: KMS/HSM connector tests with fixture-based request/response validation
// -----------------------------------------------------------------------------
using FluentAssertions;
using StellaOps.Cryptography.Kms;
using Xunit;
namespace StellaOps.Cryptography.Tests;
/// <summary>
/// KMS/HSM connector tests for remote signing providers.
/// Tests the KmsCryptoProvider which delegates to IKmsClient implementations:
@@ -24,486 +19,13 @@ namespace StellaOps.Cryptography.Tests;
[Trait("Category", "KMS")]
[Trait("Category", "HSM")]
[Trait("Category", "C1")]
public sealed class KmsHsmConnectorTests
public sealed partial class KmsHsmConnectorTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
private readonly ITestOutputHelper _output;
public KmsHsmConnectorTests(ITestOutputHelper output)
{
_output = output;
}
#region Provider Identity Tests
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider(new MockKmsClient());
provider.Name.Should().Be("kms");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
#endregion
#region Capability Tests
[Fact]
public void Supports_Signing_Es256_ReturnsTrue()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.Signing, KmsAlgorithms.Es256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/ES256");
}
[Fact]
public void Supports_Verification_Es256_ReturnsTrue()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.Verification, KmsAlgorithms.Es256)
.Should().BeTrue();
_output.WriteLine("✓ Supports Verification/ES256");
}
[Theory]
[InlineData(SignatureAlgorithms.Ed25519)]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.Sm2)]
[InlineData("RS256")]
[InlineData("RS512")]
[InlineData("PS256")]
public void Supports_Signing_UnsupportedAlgorithm_ReturnsFalse(string algorithm)
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeFalse();
_output.WriteLine($"✓ Correctly rejects unsupported algorithm: {algorithm}");
}
[Fact]
public void Supports_PasswordHashing_ReturnsFalse()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.PasswordHashing, KmsAlgorithms.Es256)
.Should().BeFalse();
_output.WriteLine("✓ PasswordHashing capability not supported");
}
[Fact]
public void Supports_ContentHashing_ReturnsFalse()
{
var provider = CreateProvider(new MockKmsClient());
provider.Supports(CryptoCapability.ContentHashing, KmsAlgorithms.Es256)
.Should().BeFalse();
_output.WriteLine("✓ ContentHashing capability not supported");
}
#endregion
#region Key Registration Tests
[Fact]
public void UpsertSigningKey_ValidKey_Succeeds()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("kms-key-001", provider.Name);
var metadata = new Dictionary<string, string?> { ["kms.version"] = "v1" };
var signingKey = new CryptoSigningKey(
keyReference,
KmsAlgorithms.Es256,
new byte[32],
DateTimeOffset.UtcNow,
metadata: metadata);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().NotThrow();
_output.WriteLine("✓ UpsertSigningKey with valid key succeeds");
}
[Fact]
public void UpsertSigningKey_MissingVersionMetadata_Throws()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("kms-key-001", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
KmsAlgorithms.Es256,
new byte[32],
DateTimeOffset.UtcNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*kms.version*");
_output.WriteLine("✓ UpsertSigningKey without version metadata → InvalidOperationException");
}
[Fact]
public void UpsertSigningKey_UnsupportedAlgorithm_Throws()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("kms-key-001", provider.Name);
var metadata = new Dictionary<string, string?> { ["kms.version"] = "v1" };
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Ed25519,
new byte[32],
DateTimeOffset.UtcNow,
metadata: metadata);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<InvalidOperationException>()
.WithMessage($"*only supports*");
_output.WriteLine("✓ UpsertSigningKey with unsupported algorithm → InvalidOperationException");
}
[Fact]
public void RemoveSigningKey_ExistingKey_ReturnsTrue()
{
var provider = CreateProvider(new MockKmsClient());
var keyId = "kms-key-to-remove";
RegisterKey(provider, keyId);
var result = provider.RemoveSigningKey(keyId);
result.Should().BeTrue();
_output.WriteLine("✓ RemoveSigningKey removes existing key");
}
[Fact]
public void RemoveSigningKey_NonExistentKey_ReturnsFalse()
{
var provider = CreateProvider(new MockKmsClient());
var result = provider.RemoveSigningKey("nonexistent");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false for non-existent key");
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void RemoveSigningKey_InvalidKeyId_ReturnsFalse(string? keyId)
{
var provider = CreateProvider(new MockKmsClient());
var result = provider.RemoveSigningKey(keyId!);
result.Should().BeFalse();
_output.WriteLine($"✓ RemoveSigningKey with '{keyId ?? "null"}' returns false");
}
#endregion
#region Signer Retrieval Tests
[Fact]
public void GetSigner_RegisteredKey_ReturnsKmsSigner()
{
var mockClient = new MockKmsClient();
var provider = CreateProvider(mockClient);
var keyId = "kms-key-signer";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var signer = provider.GetSigner(KmsAlgorithms.Es256, keyReference);
signer.Should().NotBeNull();
signer.Should().BeAssignableTo<ICryptoSigner>();
_output.WriteLine("✓ GetSigner returns KmsSigner");
}
[Fact]
public void GetSigner_UnregisteredKey_ThrowsKeyNotFoundException()
{
var provider = CreateProvider(new MockKmsClient());
var keyReference = new CryptoKeyReference("unregistered", provider.Name);
Action act = () => provider.GetSigner(KmsAlgorithms.Es256, keyReference);
act.Should().Throw<KeyNotFoundException>()
.WithMessage("*not registered*");
_output.WriteLine("✓ GetSigner with unregistered key → KeyNotFoundException");
}
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
var provider = CreateProvider(new MockKmsClient());
var keyId = "kms-key-wrong-algo";
RegisterKey(provider, keyId);
var keyReference = new CryptoKeyReference(keyId, provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not supported*");
_output.WriteLine("✓ GetSigner with unsupported algorithm → InvalidOperationException");
}
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
var provider = CreateProvider(new MockKmsClient());
Action act = () => provider.GetSigner(KmsAlgorithms.Es256, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner with null key reference → ArgumentNullException");
}
#endregion
#region Hasher Tests
[Fact]
public void GetPasswordHasher_ThrowsInvalidOperationException()
{
var provider = CreateProvider(new MockKmsClient());
Action act = () => provider.GetPasswordHasher("PBKDF2");
act.Should().Throw<InvalidOperationException>()
.WithMessage("*does not support password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws InvalidOperationException");
}
[Fact]
public void GetHasher_ThrowsInvalidOperationException()
{
var provider = CreateProvider(new MockKmsClient());
Action act = () => provider.GetHasher("SHA-256");
act.Should().Throw<InvalidOperationException>()
.WithMessage("*does not support content hashing*");
_output.WriteLine("✓ GetHasher throws InvalidOperationException");
}
#endregion
#region KMS Client Interface Tests
[Fact]
public async Task MockKmsClient_Sign_ReturnsValidSignature()
{
var mockClient = new MockKmsClient();
var data = new byte[] { 1, 2, 3, 4, 5 };
var result = await mockClient.SignAsync("test-key", "v1", data);
result.Should().NotBeNull();
result.Signature.Should().NotBeEmpty();
result.KeyId.Should().Be("test-key");
_output.WriteLine("✓ MockKmsClient.SignAsync returns valid result");
}
[Fact]
public async Task MockKmsClient_Verify_MatchingSignature_ReturnsTrue()
{
var mockClient = new MockKmsClient();
var data = new byte[] { 1, 2, 3, 4, 5 };
var signResult = await mockClient.SignAsync("test-key", "v1", data);
var verified = await mockClient.VerifyAsync("test-key", "v1", data, signResult.Signature);
verified.Should().BeTrue();
_output.WriteLine("✓ MockKmsClient sign/verify roundtrip succeeds");
}
[Fact]
public async Task MockKmsClient_Verify_TamperedData_ReturnsFalse()
{
var mockClient = new MockKmsClient();
var data = new byte[] { 1, 2, 3, 4, 5 };
var tamperedData = new byte[] { 1, 2, 3, 4, 6 };
var signResult = await mockClient.SignAsync("test-key", "v1", data);
var verified = await mockClient.VerifyAsync("test-key", "v1", tamperedData, signResult.Signature);
verified.Should().BeFalse();
_output.WriteLine("✓ MockKmsClient correctly rejects tampered data");
}
[Fact]
public async Task MockKmsClient_GetMetadata_ReturnsKeyInfo()
{
var mockClient = new MockKmsClient();
var metadata = await mockClient.GetMetadataAsync("test-key");
metadata.Should().NotBeNull();
metadata.KeyId.Should().Be("test-key");
metadata.State.Should().Be(KmsKeyState.Active);
_output.WriteLine("✓ MockKmsClient.GetMetadataAsync returns valid metadata");
}
#endregion
#region Error Code Mapping
[Fact]
public void ErrorCodeMappingSummary_DocumentsExpectedMappings()
{
_output.WriteLine("=== KMS/HSM Error Code Mapping Reference ===");
_output.WriteLine(" KeyNotFoundException → KMS_KEY_NOT_FOUND");
_output.WriteLine(" InvalidOperationException → KMS_INVALID_OPERATION");
_output.WriteLine(" ArgumentNullException → KMS_INVALID_ARGUMENT");
_output.WriteLine("");
_output.WriteLine("Backend-specific errors:");
_output.WriteLine(" AWS KMS: KMSNotFoundException → KMS_KEY_NOT_FOUND");
_output.WriteLine(" AWS KMS: KMSInvalidStateException → KMS_KEY_DISABLED");
_output.WriteLine(" GCP KMS: StatusCode.NotFound → KMS_KEY_NOT_FOUND");
_output.WriteLine(" GCP KMS: StatusCode.FailedPrecondition → KMS_KEY_DISABLED");
_output.WriteLine(" PKCS#11: CKR_KEY_HANDLE_INVALID → KMS_KEY_NOT_FOUND");
_output.WriteLine(" PKCS#11: CKR_TOKEN_NOT_PRESENT → KMS_HSM_UNAVAILABLE");
}
#endregion
#region Helper Methods
private static KmsCryptoProvider CreateProvider(IKmsClient kmsClient)
=> new(kmsClient);
private static void RegisterKey(KmsCryptoProvider provider, string keyId)
{
var keyReference = new CryptoKeyReference(keyId, provider.Name);
var metadata = new Dictionary<string, string?> { ["kms.version"] = "v1" };
var signingKey = new CryptoSigningKey(
keyReference,
KmsAlgorithms.Es256,
new byte[32],
DateTimeOffset.UtcNow,
metadata: metadata);
provider.UpsertSigningKey(signingKey);
}
#endregion
#region Mock KMS Client
/// <summary>
/// Mock KMS client for testing without external dependencies.
/// </summary>
private sealed class MockKmsClient : IKmsClient
{
private readonly Dictionary<string, byte[]> _signatures = new();
public Task<KmsSignResult> SignAsync(
string keyId,
string? keyVersion,
ReadOnlyMemory<byte> data,
CancellationToken cancellationToken = default)
{
var signature = System.Security.Cryptography.SHA256.HashData(data.Span);
var signatureKey = $"{keyId}:{keyVersion}:{Convert.ToBase64String(data.ToArray())}";
_signatures[signatureKey] = signature;
return Task.FromResult(new KmsSignResult(keyId, keyVersion ?? "v1", KmsAlgorithms.Es256, signature));
}
public Task<bool> VerifyAsync(
string keyId,
string? keyVersion,
ReadOnlyMemory<byte> data,
ReadOnlyMemory<byte> signature,
CancellationToken cancellationToken = default)
{
var expectedSignature = System.Security.Cryptography.SHA256.HashData(data.Span);
return Task.FromResult(signature.Span.SequenceEqual(expectedSignature));
}
public Task<KmsKeyMetadata> GetMetadataAsync(string keyId, CancellationToken cancellationToken = default)
{
var versions = System.Collections.Immutable.ImmutableArray.Create(
new KmsKeyVersionMetadata(
"v1",
KmsKeyState.Active,
DateTimeOffset.UtcNow.AddMonths(-1),
null,
string.Empty,
"P-256"));
var metadata = new KmsKeyMetadata(
keyId,
KmsAlgorithms.Es256,
KmsKeyState.Active,
DateTimeOffset.UtcNow.AddMonths(-1),
versions);
return Task.FromResult(metadata);
}
public Task<KmsKeyMaterial> ExportAsync(
string keyId,
string? keyVersion,
CancellationToken cancellationToken = default)
{
// Return mock key material with empty private key (verification only)
var material = new KmsKeyMaterial(
keyId,
keyVersion ?? "v1",
KmsAlgorithms.Es256,
"P-256",
D: Array.Empty<byte>(),
Qx: new byte[32],
Qy: new byte[32],
DateTimeOffset.UtcNow.AddMonths(-1));
return Task.FromResult(material);
}
public Task<KmsKeyMetadata> RotateAsync(string keyId, CancellationToken cancellationToken = default)
{
var versions = System.Collections.Immutable.ImmutableArray.Create(
new KmsKeyVersionMetadata(
"v2",
KmsKeyState.Active,
DateTimeOffset.UtcNow,
null,
string.Empty,
"P-256"));
var metadata = new KmsKeyMetadata(
keyId,
KmsAlgorithms.Es256,
KmsKeyState.Active,
DateTimeOffset.UtcNow,
versions);
return Task.FromResult(metadata);
}
public Task RevokeAsync(string keyId, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
#endregion
}

View File

@@ -2,18 +2,18 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Xunit;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class LibsodiumCryptoProviderTests
{
private static readonly DateTimeOffset FixedNow = new(2024, 1, 15, 10, 30, 0, TimeSpan.Zero);
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LibsodiumProvider_SignsAndVerifiesEs256()
[Fact]
public async Task LibsodiumProvider_SignsAndVerifiesEs256Async()
{
var provider = new LibsodiumCryptoProvider();
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
@@ -23,7 +23,7 @@ public class LibsodiumCryptoProviderTests
new CryptoKeyReference("libsodium-key"),
SignatureAlgorithms.Es256,
privateParameters: in parameters,
createdAt: DateTimeOffset.UtcNow);
createdAt: FixedNow);
provider.UpsertSigningKey(signingKey);

View File

@@ -0,0 +1,59 @@
using FluentAssertions;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class OfflineVerificationCryptoProviderTests
{
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("SHA-256")]
[InlineData("SHA-384")]
[InlineData("SHA-512")]
[InlineData("SHA256")]
[InlineData("SHA384")]
[InlineData("SHA512")]
public void GetHasher_SupportedAlgorithms_ReturnsHasher(string algorithmId)
{
var hasher = _provider.GetHasher(algorithmId);
hasher.Should().NotBeNull();
hasher.AlgorithmId.Should().NotBeNullOrWhiteSpace();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetHasher_UnsupportedAlgorithm_ThrowsNotSupportedException()
{
Action act = () => _provider.GetHasher("MD5");
act.Should().Throw<NotSupportedException>()
.WithMessage("*MD5*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetHasher_SHA256_ComputesCorrectHash()
{
var hasher = _provider.GetHasher("SHA-256");
var data = "Hello, World!"u8.ToArray();
var hash = hasher.ComputeHash(data);
hash.Should().NotBeNullOrEmpty();
hash.Length.Should().Be(32);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetHasher_SHA256_ProducesDeterministicOutput()
{
var hasher1 = _provider.GetHasher("SHA-256");
var hasher2 = _provider.GetHasher("SHA-256");
var data = "Test data"u8.ToArray();
var hash1 = hasher1.ComputeHash(data);
var hash2 = hasher2.ComputeHash(data);
hash1.Should().Equal(hash2, "Same data should produce same hash");
}
}

View File

@@ -0,0 +1,16 @@
using FluentAssertions;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class OfflineVerificationCryptoProviderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetPasswordHasher_ThrowsNotSupportedException()
{
Action act = () => _provider.GetPasswordHasher("PBKDF2");
act.Should().Throw<NotSupportedException>()
.WithMessage("*not supported*");
}
}

View File

@@ -0,0 +1,45 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class OfflineVerificationCryptoProviderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsNotSupportedException()
{
var keyRef = new CryptoKeyReference("test-key");
Action act = () => _provider.GetSigner("UNKNOWN", keyRef);
act.Should().Throw<NotSupportedException>()
.WithMessage("*UNKNOWN*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateEphemeralVerifier_UnsupportedAlgorithm_ThrowsNotSupportedException()
{
var publicKeyBytes = new byte[64];
Action act = () => _provider.CreateEphemeralVerifier("UNKNOWN", publicKeyBytes);
act.Should().Throw<NotSupportedException>()
.WithMessage("*UNKNOWN*");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("ES512")]
public void CreateEphemeralVerifier_EcdsaAlgorithms_ReturnsVerifier(string algorithmId)
{
var publicKeyBytes = new byte[91];
Action act = () => _provider.CreateEphemeralVerifier(algorithmId, publicKeyBytes);
act.Should().NotThrow<NotSupportedException>($"{algorithmId} should be supported");
}
}

View File

@@ -0,0 +1,99 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class OfflineVerificationCryptoProviderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Name_ReturnsOfflineVerification()
{
var name = _provider.Name;
name.Should().Be("offline-verification");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("ES512")]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("PS384")]
[InlineData("PS512")]
public void Supports_SigningAlgorithms_ReturnsTrue(string algorithmId)
{
var supports = _provider.Supports(CryptoCapability.Signing, algorithmId);
supports.Should().BeTrue($"{algorithmId} should be supported for signing");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("ES512")]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("PS384")]
[InlineData("PS512")]
public void Supports_VerificationAlgorithms_ReturnsTrue(string algorithmId)
{
var supports = _provider.Supports(CryptoCapability.Verification, algorithmId);
supports.Should().BeTrue($"{algorithmId} should be supported for verification");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("SHA-256")]
[InlineData("SHA-384")]
[InlineData("SHA-512")]
[InlineData("SHA256")]
[InlineData("SHA384")]
[InlineData("SHA512")]
public void Supports_HashAlgorithms_ReturnsTrue(string algorithmId)
{
var supports = _provider.Supports(CryptoCapability.ContentHashing, algorithmId);
supports.Should().BeTrue($"{algorithmId} should be supported for content hashing");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("PBKDF2")]
[InlineData("Argon2id")]
public void Supports_PasswordHashingAlgorithms_ReturnsTrue(string algorithmId)
{
var supports = _provider.Supports(CryptoCapability.PasswordHashing, algorithmId);
supports.Should().BeTrue($"{algorithmId} should be reported as supported for password hashing");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256K")]
[InlineData("EdDSA")]
[InlineData("UNKNOWN")]
public void Supports_UnsupportedAlgorithms_ReturnsFalse(string algorithmId)
{
var supports = _provider.Supports(CryptoCapability.Signing, algorithmId);
supports.Should().BeFalse($"{algorithmId} should not be supported");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
var supports = _provider.Supports(CryptoCapability.SymmetricEncryption, "AES-256-GCM");
supports.Should().BeFalse("Symmetric encryption should not be supported");
}
}

View File

@@ -1,12 +1,6 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.OfflineVerification;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Tests;
public sealed class OfflineVerificationCryptoProviderTests
public sealed partial class OfflineVerificationCryptoProviderTests
{
private readonly OfflineVerificationCryptoProvider _provider;
@@ -14,233 +8,4 @@ public sealed class OfflineVerificationCryptoProviderTests
{
_provider = new OfflineVerificationCryptoProvider();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Name_ReturnsOfflineVerification()
{
// Act
var name = _provider.Name;
// Assert
name.Should().Be("offline-verification");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("ES512")]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("PS384")]
[InlineData("PS512")]
public void Supports_SigningAlgorithms_ReturnsTrue(string algorithmId)
{
// Act
var supports = _provider.Supports(CryptoCapability.Signing, algorithmId);
// Assert
supports.Should().BeTrue($"{algorithmId} should be supported for signing");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("ES512")]
[InlineData("RS256")]
[InlineData("RS384")]
[InlineData("RS512")]
[InlineData("PS256")]
[InlineData("PS384")]
[InlineData("PS512")]
public void Supports_VerificationAlgorithms_ReturnsTrue(string algorithmId)
{
// Act
var supports = _provider.Supports(CryptoCapability.Verification, algorithmId);
// Assert
supports.Should().BeTrue($"{algorithmId} should be supported for verification");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("SHA-256")]
[InlineData("SHA-384")]
[InlineData("SHA-512")]
[InlineData("SHA256")]
[InlineData("SHA384")]
[InlineData("SHA512")]
public void Supports_HashAlgorithms_ReturnsTrue(string algorithmId)
{
// Act
var supports = _provider.Supports(CryptoCapability.ContentHashing, algorithmId);
// Assert
supports.Should().BeTrue($"{algorithmId} should be supported for content hashing");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("PBKDF2")]
[InlineData("Argon2id")]
public void Supports_PasswordHashingAlgorithms_ReturnsTrue(string algorithmId)
{
// Act
var supports = _provider.Supports(CryptoCapability.PasswordHashing, algorithmId);
// Assert
supports.Should().BeTrue($"{algorithmId} should be reported as supported for password hashing");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256K")]
[InlineData("EdDSA")]
[InlineData("UNKNOWN")]
public void Supports_UnsupportedAlgorithms_ReturnsFalse(string algorithmId)
{
// Act
var supports = _provider.Supports(CryptoCapability.Signing, algorithmId);
// Assert
supports.Should().BeFalse($"{algorithmId} should not be supported");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Supports_SymmetricEncryption_ReturnsFalse()
{
// Act
var supports = _provider.Supports(CryptoCapability.SymmetricEncryption, "AES-256-GCM");
// Assert
supports.Should().BeFalse("Symmetric encryption should not be supported");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("SHA-256")]
[InlineData("SHA-384")]
[InlineData("SHA-512")]
[InlineData("SHA256")] // Alias test
[InlineData("SHA384")] // Alias test
[InlineData("SHA512")] // Alias test
public void GetHasher_SupportedAlgorithms_ReturnsHasher(string algorithmId)
{
// Act
var hasher = _provider.GetHasher(algorithmId);
// Assert
hasher.Should().NotBeNull();
hasher.AlgorithmId.Should().NotBeNullOrWhiteSpace();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetHasher_UnsupportedAlgorithm_ThrowsNotSupportedException()
{
// Act
Action act = () => _provider.GetHasher("MD5");
// Assert
act.Should().Throw<NotSupportedException>()
.WithMessage("*MD5*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetHasher_SHA256_ComputesCorrectHash()
{
// Arrange
var hasher = _provider.GetHasher("SHA-256");
var data = "Hello, World!"u8.ToArray();
// Act
var hash = hasher.ComputeHash(data);
// Assert
hash.Should().NotBeNullOrEmpty();
hash.Length.Should().Be(32); // SHA-256 produces 32 bytes
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetHasher_SHA256_ProducesDeterministicOutput()
{
// Arrange
var hasher1 = _provider.GetHasher("SHA-256");
var hasher2 = _provider.GetHasher("SHA-256");
var data = "Test data"u8.ToArray();
// Act
var hash1 = hasher1.ComputeHash(data);
var hash2 = hasher2.ComputeHash(data);
// Assert
hash1.Should().Equal(hash2, "Same data should produce same hash");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetPasswordHasher_ThrowsNotSupportedException()
{
// Act
Action act = () => _provider.GetPasswordHasher("PBKDF2");
// Assert
act.Should().Throw<NotSupportedException>()
.WithMessage("*not supported*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsNotSupportedException()
{
// Arrange
var keyRef = new CryptoKeyReference("test-key");
// Act
Action act = () => _provider.GetSigner("UNKNOWN", keyRef);
// Assert
act.Should().Throw<NotSupportedException>()
.WithMessage("*UNKNOWN*");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CreateEphemeralVerifier_UnsupportedAlgorithm_ThrowsNotSupportedException()
{
// Arrange
var publicKeyBytes = new byte[64];
// Act
Action act = () => _provider.CreateEphemeralVerifier("UNKNOWN", publicKeyBytes);
// Assert
act.Should().Throw<NotSupportedException>()
.WithMessage("*UNKNOWN*");
}
[Trait("Category", TestCategories.Unit)]
[Theory]
[InlineData("ES256")]
[InlineData("ES384")]
[InlineData("ES512")]
public void CreateEphemeralVerifier_EcdsaAlgorithms_ReturnsVerifier(string algorithmId)
{
// Arrange
// Create a minimal SPKI-formatted EC public key (this is a placeholder - real keys would be valid SPKI)
var publicKeyBytes = new byte[91]; // Approximate size for EC public key in SPKI format
// Act
Action act = () => _provider.CreateEphemeralVerifier(algorithmId, publicKeyBytes);
// Assert - we expect it to return a verifier or throw a specific crypto exception, not NotSupportedException
act.Should().NotThrow<NotSupportedException>($"{algorithmId} should be supported");
}
}

View File

@@ -0,0 +1,64 @@
using FluentAssertions;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Providers.OfflineVerification;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class OfflineVerificationProviderTests
{
private static readonly DateTimeOffset FixedNow = DateTimeOffset.Parse("2026-02-03T00:00:00Z");
[Fact]
public void Supports_ReturnsTrue_ForKnownAlgorithms()
{
var provider = new OfflineVerificationCryptoProvider();
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Es256).Should().BeTrue();
provider.Supports(CryptoCapability.ContentHashing, HashAlgorithms.Sha256).Should().BeTrue();
provider.Supports(CryptoCapability.PasswordHashing, "PBKDF2").Should().BeTrue();
}
[Fact]
public void Supports_ReturnsFalse_ForUnknownAlgorithms()
{
var provider = new OfflineVerificationCryptoProvider();
provider.Supports(CryptoCapability.Signing, "ED25519").Should().BeFalse();
provider.Supports(CryptoCapability.ContentHashing, "MD5").Should().BeFalse();
provider.Supports(CryptoCapability.PasswordHashing, "BCRYPT").Should().BeFalse();
}
[Fact]
public void UpsertSigningKey_AllowsSignerRetrieval()
{
var provider = new OfflineVerificationCryptoProvider();
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var parameters = ecdsa.ExportParameters(true);
var keyRef = new CryptoKeyReference("offline-key-1", provider.Name);
var signingKey = new CryptoSigningKey(keyRef, SignatureAlgorithms.Es256, parameters, FixedNow);
provider.UpsertSigningKey(signingKey);
var signer = provider.GetSigner(SignatureAlgorithms.Es256, keyRef);
signer.KeyId.Should().Be("offline-key-1");
signer.AlgorithmId.Should().Be(SignatureAlgorithms.Es256);
}
[Fact]
public void GetSigner_Throws_WhenKeyMissing()
{
var provider = new OfflineVerificationCryptoProvider();
Action action = () => provider.GetSigner(
SignatureAlgorithms.Es256,
new CryptoKeyReference("missing", provider.Name));
action.Should().Throw<KeyNotFoundException>();
}
}

View File

@@ -1,5 +1,4 @@
using System.Text;
using System.Threading.Tasks;
using Org.BouncyCastle.Asn1.CryptoPro;
using Org.BouncyCastle.Asn1.Rosstandart;
using Org.BouncyCastle.Crypto;
@@ -9,16 +8,15 @@ using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Security;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.OpenSslGost;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class OpenSslGostSignerTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task SignAndVerify_WithManagedProvider_Succeeds()
[Fact]
public async Task SignAndVerify_WithManagedProvider_SucceedsAsync()
{
var keyPair = GenerateKeyPair();
var entry = new OpenSslGostKeyEntry(

View File

@@ -1,12 +1,11 @@
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class PasswordHashOptionsTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Validate_DoesNotThrow_ForDefaults()
{
var options = new PasswordHashOptions();
@@ -14,7 +13,7 @@ public class PasswordHashOptionsTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Validate_Throws_WhenMemoryInvalid()
{
var options = new PasswordHashOptions

View File

@@ -1,16 +1,15 @@
using System;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class Pbkdf2PasswordHasherTests
{
private readonly Pbkdf2PasswordHasher hasher = new();
private readonly Pbkdf2PasswordHasher _hasher = new();
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Hash_ProducesLegacyFormat()
{
var options = new PasswordHashOptions
@@ -19,13 +18,13 @@ public class Pbkdf2PasswordHasherTests
Iterations = 210_000
};
var encoded = hasher.Hash("s3cret", options);
var encoded = _hasher.Hash("s3cret", options);
Assert.StartsWith("PBKDF2.", encoded, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Verify_Succeeds_ForCorrectPassword()
{
var options = new PasswordHashOptions
@@ -34,14 +33,14 @@ public class Pbkdf2PasswordHasherTests
Iterations = 210_000
};
var encoded = hasher.Hash("s3cret", options);
var encoded = _hasher.Hash("s3cret", options);
Assert.True(hasher.Verify("s3cret", encoded));
Assert.False(hasher.Verify("other", encoded));
Assert.True(_hasher.Verify("s3cret", encoded));
Assert.False(_hasher.Verify("other", encoded));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void NeedsRehash_DetectsIterationChange()
{
var options = new PasswordHashOptions
@@ -50,11 +49,11 @@ public class Pbkdf2PasswordHasherTests
Iterations = 100_000
};
var encoded = hasher.Hash("s3cret", options);
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));
Assert.True(_hasher.NeedsRehash(encoded, higher));
Assert.False(_hasher.NeedsRehash(encoded, options));
}
}

View File

@@ -1,56 +1,54 @@
#if STELLAOPS_PKCS11
using System;
using System.IO;
using System.Security.Cryptography;
using Microsoft.Extensions.Options;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
using System.Collections.Generic;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Cryptography.Tests;
public class Pkcs11GostProviderTests
public sealed class Pkcs11GostProviderTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DescribeKeys_ExposesLibraryPathAndThumbprint()
[Fact]
public void AddPkcs11GostProvider_RegistersProvider()
{
if (!string.Equals(Environment.GetEnvironmentVariable("STELLAOPS_PKCS11_ENABLED"), "1", StringComparison.Ordinal))
{
return; // opt-in only when PKCS#11 libs/slots are available
}
var services = new ServiceCollection();
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var req = new CertificateRequest("CN=pkcs11.test", ecdsa, HashAlgorithmName.SHA256);
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
services.AddPkcs11GostProvider();
var certPath = Path.Combine(Path.GetTempPath(), $"pkcs11-{Guid.NewGuid():N}.cer");
File.WriteAllBytes(certPath, cert.Export(X509ContentType.Cert));
using var provider = services.BuildServiceProvider();
var registered = provider.GetServices<ICryptoProvider>();
registered.Should().ContainSingle(item => item is Pkcs11GostCryptoProvider);
}
[Fact]
public void GetSigner_ThrowsWhenKeyMissing()
{
var provider = new Pkcs11GostCryptoProvider();
var keyRef = new CryptoKeyReference("missing-key", provider.Name);
var act = () => provider.GetSigner(SignatureAlgorithms.GostR3410_2012_256, keyRef);
act.Should().Throw<KeyNotFoundException>();
}
[Fact]
public void ProviderOptions_CloneCopiesKeys()
{
var options = new Pkcs11GostProviderOptions();
options.Keys.Add(new Pkcs11GostKeyOptions
{
KeyId = "test-key",
Algorithm = SignatureAlgorithms.GostR3410_2012_256,
LibraryPath = "/tmp/libpkcs11-placeholder.so",
PrivateKeyLabel = "priv",
PublicKeyLabel = "pub",
CertificatePath = certPath,
SignMechanismId = Pkcs11Mechanisms.DefaultGost12_256Signature
KeyId = "key-1",
LibraryPath = "/tmp/pkcs11.so",
TokenLabel = "token-a"
});
var provider = new Pkcs11GostCryptoProvider(Options.Create(options));
var clone = options.Clone();
Assert.True(provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.GostR3410_2012_256));
var descriptor = Assert.Single(provider.DescribeKeys());
Assert.Equal("test-key", descriptor.KeyId);
Assert.Equal("/tmp/libpkcs11-placeholder.so", descriptor.Metadata["library"]);
Assert.Equal(cert.Thumbprint, descriptor.Metadata["thumbprint"], ignoreCase: true);
Assert.Equal("priv", descriptor.Metadata["privateKeyLabel"]);
clone.Keys.Should().HaveCount(1);
clone.Keys[0].KeyId.Should().Be("key-1");
clone.Keys[0].LibraryPath.Should().Be("/tmp/pkcs11.so");
clone.Keys[0].Should().NotBeSameAs(options.Keys[0]);
}
}
#endif

View File

@@ -0,0 +1,85 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.PqSoft;
using System;
using System.Collections.Generic;
using System.IO;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class PqSoftCryptoProviderTests
{
[Fact]
public void GateBehavior_RespectsEnvironmentFlag()
{
var previous = Environment.GetEnvironmentVariable("PQ_SOFT_ALLOWED");
try
{
Environment.SetEnvironmentVariable("PQ_SOFT_ALLOWED", null);
var provider = CreateProvider(new PqSoftProviderOptions { RequireEnvironmentGate = true });
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Dilithium3).Should().BeFalse();
Environment.SetEnvironmentVariable("PQ_SOFT_ALLOWED", "1");
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Dilithium3).Should().BeTrue();
}
finally
{
Environment.SetEnvironmentVariable("PQ_SOFT_ALLOWED", previous);
}
}
[Fact]
public void Constructor_SkipsMissingKeyFile()
{
var missingPath = Path.Combine(Path.GetTempPath(), "stellaops-tests", "pqsoft-missing.key");
if (File.Exists(missingPath))
{
File.Delete(missingPath);
}
var options = new PqSoftProviderOptions
{
RequireEnvironmentGate = false,
Keys = new List<PqSoftKeyOptions>
{
new PqSoftKeyOptions
{
KeyId = "missing-key",
Algorithm = SignatureAlgorithms.Dilithium3,
PrivateKeyPath = missingPath
}
}
};
var provider = CreateProvider(options);
provider.GetSigningKeys().Should().BeEmpty();
}
[Fact]
public void UpsertSigningKey_RegistersSigner()
{
var provider = CreateProvider(new PqSoftProviderOptions { RequireEnvironmentGate = false });
var signingKey = new CryptoSigningKey(
new CryptoKeyReference("pq-key-1", provider.Name),
SignatureAlgorithms.Dilithium3,
new byte[] { 1, 2, 3, 4, 5 },
DateTimeOffset.Parse("2026-02-03T00:00:00Z"));
provider.UpsertSigningKey(signingKey);
var signer = provider.GetSigner(SignatureAlgorithms.Dilithium3, signingKey.Reference);
signer.KeyId.Should().Be("pq-key-1");
signer.AlgorithmId.Should().Be(SignatureAlgorithms.Dilithium3);
}
private static PqSoftCryptoProvider CreateProvider(PqSoftProviderOptions options)
=> new(Options.Create(options));
}

View File

@@ -2,15 +2,14 @@ using System.Security.Cryptography;
using System.Text;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Digests;
using Xunit;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed class Sha256DigestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Normalize_AllowsBareHex_WhenPrefixNotRequired()
{
var hex = new string('a', Sha256Digest.HexLength);
@@ -18,7 +17,7 @@ public sealed class Sha256DigestTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Normalize_NormalizesPrefixAndHexToLower()
{
var hexUpper = new string('A', Sha256Digest.HexLength);
@@ -28,7 +27,7 @@ public sealed class Sha256DigestTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Normalize_RequiresPrefix_WhenConfigured()
{
var hex = new string('a', Sha256Digest.HexLength);
@@ -38,7 +37,7 @@ public sealed class Sha256DigestTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void ExtractHex_ReturnsLowercaseHex()
{
var hexUpper = new string('A', Sha256Digest.HexLength);
@@ -46,7 +45,7 @@ public sealed class Sha256DigestTests
}
[Trait("Category", TestCategories.Unit)]
[Fact]
[Fact]
public void Compute_UsesCryptoHashStack()
{
var hash = CryptoHashFactory.CreateDefault();

View File

@@ -0,0 +1,21 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Theory]
[InlineData("fips.sim")]
[InlineData("eidas.sim")]
[InlineData("kcmvp.sim")]
[InlineData("world.sim")]
public void Supports_Signing_ComplianceModes_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm} (Compliance Mode)");
}
}

View File

@@ -0,0 +1,27 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Fact]
public void DescribeKeys_ReturnsConfiguredAlgorithms()
{
var provider = CreateProvider();
if (provider is ICryptoProviderDiagnostics diagnostics)
{
var keys = diagnostics.DescribeKeys().ToList();
keys.Should().NotBeEmpty();
foreach (var key in keys)
{
key.Provider.Should().Be("sim.crypto.remote");
key.Metadata.Should().ContainKey("simulation");
key.Metadata["simulation"].Should().Be("true");
}
_output.WriteLine($"✓ DescribeKeys returns {keys.Count} algorithm descriptors");
}
}
}

View File

@@ -0,0 +1,20 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Theory]
[InlineData(SignatureAlgorithms.Es256)]
[InlineData(SignatureAlgorithms.Es384)]
[InlineData(SignatureAlgorithms.Es512)]
public void Supports_Signing_Ecdsa_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm} (ECDSA)");
}
}

View File

@@ -0,0 +1,31 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Fact]
public void GetPasswordHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetPasswordHasher("argon2id");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not handle password hashing*");
_output.WriteLine("✓ GetPasswordHasher throws NotSupportedException");
}
[Fact]
public void GetHasher_ThrowsNotSupported()
{
var provider = CreateProvider();
Action act = () => provider.GetHasher("SHA-256");
act.Should().Throw<NotSupportedException>()
.WithMessage("*does not handle hashing*");
_output.WriteLine("✓ GetHasher throws NotSupportedException");
}
}

View File

@@ -0,0 +1,47 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Fact]
public void UpsertSigningKey_ThrowsNotSupported()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signingKey = new CryptoSigningKey(
keyReference,
SignatureAlgorithms.Sm2,
new byte[32],
createdAt: FixedNow);
Action act = () => provider.UpsertSigningKey(signingKey);
act.Should().Throw<NotSupportedException>()
.WithMessage("*uses remote keys*");
_output.WriteLine("✓ UpsertSigningKey throws NotSupportedException (remote keys only)");
}
[Fact]
public void RemoveSigningKey_ReturnsFalse()
{
var provider = CreateProvider();
var result = provider.RemoveSigningKey("any-key");
result.Should().BeFalse();
_output.WriteLine("✓ RemoveSigningKey returns false (remote keys only)");
}
[Fact]
public void GetSigningKeys_ReturnsEmpty()
{
var provider = CreateProvider();
provider.GetSigningKeys().Should().BeEmpty();
_output.WriteLine("✓ GetSigningKeys returns empty (remote keys)");
}
}

View File

@@ -0,0 +1,46 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Fact]
public void GetSigner_NullKeyReference_ThrowsArgumentNullException()
{
var provider = CreateProvider();
Action act = () => provider.GetSigner(SignatureAlgorithms.Sm2, null!);
act.Should().Throw<ArgumentNullException>();
_output.WriteLine("✓ GetSigner with null key reference → ArgumentNullException");
}
[Fact]
public void GetSigner_UnsupportedAlgorithm_ThrowsInvalidOperationException()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
Action act = () => provider.GetSigner(SignatureAlgorithms.Ed25519, keyReference);
act.Should().Throw<InvalidOperationException>()
.WithMessage("*not enabled for simulation*");
_output.WriteLine("✓ GetSigner with unsupported algorithm → InvalidOperationException");
}
[Fact]
public void GetSigner_ValidAlgorithm_ReturnsSigner()
{
var provider = CreateProvider();
var keyReference = new CryptoKeyReference("test-key", provider.Name);
var signer = provider.GetSigner(SignatureAlgorithms.Sm2, keyReference);
signer.Should().NotBeNull();
signer.AlgorithmId.Should().Be(SignatureAlgorithms.Sm2);
_output.WriteLine("✓ GetSigner returns valid signer");
}
}

View File

@@ -0,0 +1,21 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Theory]
[InlineData(SignatureAlgorithms.GostR3410_2012_256)]
[InlineData(SignatureAlgorithms.GostR3410_2012_512)]
[InlineData("ru.magma.sim")]
[InlineData("ru.kuznyechik.sim")]
public void Supports_Signing_Gost_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm} (GOST)");
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.Extensions.Options;
using StellaOps.Cryptography.Plugin.SimRemote;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
private static SimRemoteProvider CreateProvider()
{
var mockHttpClient = CreateMockHttpClient();
var options = Options.Create(new SimRemoteProviderOptions());
return new SimRemoteProvider(mockHttpClient, options, null);
}
private static SimRemoteProvider CreateProviderWithOptions(SimRemoteProviderOptions options)
{
var mockHttpClient = CreateMockHttpClient();
return new SimRemoteProvider(mockHttpClient, Options.Create(options), null);
}
private static SimRemoteHttpClient CreateMockHttpClient()
{
var httpClient = new HttpClient();
return new SimRemoteHttpClient(httpClient);
}
}

View File

@@ -0,0 +1,20 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Theory]
[InlineData(SignatureAlgorithms.Dilithium3)]
[InlineData(SignatureAlgorithms.Falcon512)]
[InlineData("pq.sim")]
public void Supports_Signing_PostQuantum_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm} (Post-Quantum)");
}
}

View File

@@ -0,0 +1,14 @@
using FluentAssertions;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Fact]
public void Provider_Name_IsExpected()
{
var provider = CreateProvider();
provider.Name.Should().Be("sim.crypto.remote");
_output.WriteLine($"✓ Provider name: {provider.Name}");
}
}

View File

@@ -0,0 +1,30 @@
using FluentAssertions;
using StellaOps.Cryptography;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public sealed partial class SimRemoteCapabilityDetectionTests
{
[Fact]
public void Supports_Signing_Sm2_ReturnsTrue()
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, SignatureAlgorithms.Sm2)
.Should().BeTrue();
_output.WriteLine("✓ Supports Signing/SM2");
}
[Theory]
[InlineData("sm.sim")]
[InlineData("sm2.sim")]
public void Supports_Signing_SmSimulationModes_ReturnsTrue(string algorithm)
{
var provider = CreateProvider();
provider.Supports(CryptoCapability.Signing, algorithm)
.Should().BeTrue();
_output.WriteLine($"✓ Supports Signing/{algorithm}");
}
}

Some files were not shown because too many files have changed in this diff Show More