using FluentAssertions; using StellaOps.Cryptography; using StellaOps.Cryptography.Plugin.OfflineVerification; using System.Security.Cryptography; using Xunit; namespace StellaOps.Cryptography.Plugin.OfflineVerification.Tests; public class OfflineVerificationProviderTests { private readonly OfflineVerificationCryptoProvider _provider; public OfflineVerificationProviderTests() { _provider = new OfflineVerificationCryptoProvider(); } [Trait("Category", TestCategories.Unit)] [Fact] public void Name_ReturnsCorrectValue() { // Assert _provider.Name.Should().Be("offline-verification"); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData(CryptoCapability.Signing, "ES256", true)] [InlineData(CryptoCapability.Signing, "ES384", true)] [InlineData(CryptoCapability.Signing, "ES512", true)] [InlineData(CryptoCapability.Signing, "RS256", true)] [InlineData(CryptoCapability.Signing, "RS384", true)] [InlineData(CryptoCapability.Signing, "RS512", true)] [InlineData(CryptoCapability.Signing, "PS256", true)] [InlineData(CryptoCapability.Signing, "PS384", true)] [InlineData(CryptoCapability.Signing, "PS512", true)] [InlineData(CryptoCapability.Verification, "ES256", true)] [InlineData(CryptoCapability.Verification, "RS256", true)] [InlineData(CryptoCapability.Verification, "PS256", true)] [InlineData(CryptoCapability.ContentHashing, "SHA-256", true)] [InlineData(CryptoCapability.ContentHashing, "SHA-384", true)] [InlineData(CryptoCapability.ContentHashing, "SHA-512", true)] [InlineData(CryptoCapability.ContentHashing, "SHA256", true)] [InlineData(CryptoCapability.PasswordHashing, "PBKDF2", true)] [InlineData(CryptoCapability.PasswordHashing, "Argon2id", true)] [InlineData(CryptoCapability.Signing, "UNSUPPORTED", false)] [InlineData(CryptoCapability.SymmetricEncryption, "AES-256", false)] public void Supports_ReturnCorrectResult(CryptoCapability capability, string algorithmId, bool expected) { // Act var result = _provider.Supports(capability, algorithmId); // Assert result.Should().Be(expected); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("SHA-256", "hello world", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")] [InlineData("SHA-384", "hello world", "fdbd8e75a67f29f701a4e040385e2e23986303ea10239211af907fcbb83578b3e417cb71ce646efd0819dd8c088de1bd")] [InlineData("SHA-512", "hello world", "309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f")] [InlineData("SHA256", "hello world", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9")] // Alternative form public void GetHasher_ComputesCorrectHash(string algorithmId, string input, string expectedHex) { // Arrange var hasher = _provider.GetHasher(algorithmId); var inputBytes = System.Text.Encoding.UTF8.GetBytes(input); // Act var hash = hasher.ComputeHash(inputBytes); var actualHex = Convert.ToHexString(hash).ToLowerInvariant(); // Assert actualHex.Should().Be(expectedHex); } [Trait("Category", TestCategories.Unit)] [Fact] public void GetHasher_WithUnsupportedAlgorithm_ThrowsNotSupportedException() { // Act var act = () => _provider.GetHasher("MD5"); // Assert act.Should().Throw() .WithMessage("*MD5*"); } [Trait("Category", TestCategories.Unit)] [Fact] public void GetPasswordHasher_ThrowsNotSupportedException() { // Act var act = () => _provider.GetPasswordHasher("PBKDF2"); // Assert act.Should().Throw() .WithMessage("*Password hashing*"); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("ES256")] [InlineData("ES384")] [InlineData("ES512")] public void CreateEphemeralVerifier_ForEcdsa_VerifiesSignatureCorrectly(string algorithmId) { // Arrange - Create a real ECDSA key, sign a message using var ecdsa = ECDsa.Create(); var curve = algorithmId switch { "ES256" => ECCurve.NamedCurves.nistP256, "ES384" => ECCurve.NamedCurves.nistP384, "ES512" => ECCurve.NamedCurves.nistP521, _ => throw new NotSupportedException() }; ecdsa.GenerateKey(curve); var hashAlgorithm = algorithmId switch { "ES256" => HashAlgorithmName.SHA256, "ES384" => HashAlgorithmName.SHA384, "ES512" => HashAlgorithmName.SHA512, _ => throw new NotSupportedException() }; var message = System.Text.Encoding.UTF8.GetBytes("ephemeral verifier test"); var signature = ecdsa.SignData(message, hashAlgorithm); // Export public key in SubjectPublicKeyInfo format var publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); // Act - Create ephemeral verifier from public key var ephemeralVerifier = _provider.CreateEphemeralVerifier(algorithmId, publicKeyBytes); // Assert - Verify signature using ephemeral verifier var isValid = ephemeralVerifier.VerifyAsync(message, signature, default).GetAwaiter().GetResult(); isValid.Should().BeTrue("ephemeral verifier should verify signature from original key"); } [Trait("Category", TestCategories.Unit)] [Fact] public void CreateEphemeralVerifier_ForRsaPkcs1_VerifiesSignatureCorrectly() { // Arrange - Create a real RSA key, sign a message using var rsa = RSA.Create(2048); var message = System.Text.Encoding.UTF8.GetBytes("ephemeral rsa verifier test"); var signature = rsa.SignData(message, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // Export public key in SubjectPublicKeyInfo format var publicKeyBytes = rsa.ExportSubjectPublicKeyInfo(); // Act - Create ephemeral verifier from public key var ephemeralVerifier = _provider.CreateEphemeralVerifier("RS256", publicKeyBytes); // Assert - Verify signature using ephemeral verifier var isValid = ephemeralVerifier.VerifyAsync(message, signature, default).GetAwaiter().GetResult(); isValid.Should().BeTrue("ephemeral RSA verifier should verify PKCS1 signature from original key"); } [Trait("Category", TestCategories.Unit)] [Fact] public void CreateEphemeralVerifier_ForRsaPss_VerifiesSignatureCorrectly() { // Arrange - Create a real RSA key, sign a message using var rsa = RSA.Create(2048); var message = System.Text.Encoding.UTF8.GetBytes("ephemeral rsa pss verifier test"); var signature = rsa.SignData(message, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); // Export public key in SubjectPublicKeyInfo format var publicKeyBytes = rsa.ExportSubjectPublicKeyInfo(); // Act - Create ephemeral verifier from public key var ephemeralVerifier = _provider.CreateEphemeralVerifier("PS256", publicKeyBytes); // Assert - Verify signature using ephemeral verifier var isValid = ephemeralVerifier.VerifyAsync(message, signature, default).GetAwaiter().GetResult(); isValid.Should().BeTrue("ephemeral RSA verifier should verify PSS signature from original key"); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("ES256")] [InlineData("PS256")] public void EphemeralVerifier_SignAsync_ThrowsNotSupportedException(string algorithmId) { // Arrange - Create a dummy public key byte[] publicKeyBytes; if (algorithmId.StartsWith("ES")) { using var ecdsa = ECDsa.Create(); publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); } else { using var rsa = RSA.Create(2048); publicKeyBytes = rsa.ExportSubjectPublicKeyInfo(); } var ephemeralVerifier = _provider.CreateEphemeralVerifier(algorithmId, publicKeyBytes); // Act var message = System.Text.Encoding.UTF8.GetBytes("test"); var act = async () => await ephemeralVerifier.VerifyAsync(message, System.Text.Encoding.UTF8.GetBytes("invalid-signature"), default); // Assert - should return false, not throw var result = act().GetAwaiter().GetResult(); result.Should().BeFalse(); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("ES256")] [InlineData("PS256")] public void EphemeralVerifier_WithTamperedMessage_FailsVerification(string algorithmId) { // Arrange - Create key and sign original message byte[] publicKeyBytes; byte[] signature; var originalMessage = System.Text.Encoding.UTF8.GetBytes("original message"); var tamperedMessage = System.Text.Encoding.UTF8.GetBytes("tampered message"); if (algorithmId.StartsWith("ES")) { using var ecdsa = ECDsa.Create(); signature = ecdsa.SignData(originalMessage, HashAlgorithmName.SHA256); publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); } else { using var rsa = RSA.Create(2048); var padding = algorithmId.StartsWith("PS") ? RSASignaturePadding.Pss : RSASignaturePadding.Pkcs1; signature = rsa.SignData(originalMessage, HashAlgorithmName.SHA256, padding); publicKeyBytes = rsa.ExportSubjectPublicKeyInfo(); } var ephemeralVerifier = _provider.CreateEphemeralVerifier(algorithmId, publicKeyBytes); // Act var isValid = ephemeralVerifier.VerifyAsync(tamperedMessage, signature, default).GetAwaiter().GetResult(); // Assert isValid.Should().BeFalse("ephemeral verifier should fail with tampered message"); } [Trait("Category", TestCategories.Unit)] [Fact] public void CreateEphemeralVerifier_WithUnsupportedAlgorithm_ThrowsNotSupportedException() { // Arrange - Create a dummy public key using var ecdsa = ECDsa.Create(); var publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); // Act var act = () => _provider.CreateEphemeralVerifier("UNSUPPORTED", publicKeyBytes); // Assert act.Should().Throw() .WithMessage("*UNSUPPORTED*"); } [Trait("Category", TestCategories.Unit)] [Theory] [InlineData("ES256")] [InlineData("PS256")] public void EphemeralVerifier_HasCorrectProperties(string algorithmId) { // Arrange - Create a dummy public key byte[] publicKeyBytes; using (var ecdsa = ECDsa.Create()) { publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo(); using StellaOps.TestKit; } // Act var ephemeralVerifier = _provider.CreateEphemeralVerifier(algorithmId, publicKeyBytes); // Assert ephemeralVerifier.KeyId.Should().Be("ephemeral"); ephemeralVerifier.AlgorithmId.Should().Be(algorithmId); } }