// SPDX-License-Identifier: BUSL-1.1 using Microsoft.Extensions.Logging.Abstractions; using StellaOps.Cryptography.Models; using StellaOps.Cryptography.Plugin.Eidas; using StellaOps.Cryptography.Plugin.Fips; using StellaOps.Cryptography.Plugin.Gost; using StellaOps.Cryptography.Plugin.Hsm; using StellaOps.Cryptography.Plugin.Sm; using StellaOps.Plugin.Abstractions.Capabilities; using StellaOps.Plugin.Abstractions.Lifecycle; using StellaOps.Plugin.Testing; namespace StellaOps.Cryptography.Tests; public sealed class CryptoProviderPluginBehaviorTests { [Fact] public async Task FipsPlugin_RoundTripsSignVerifyAndEncryptDecrypt() { var plugin = new FipsPlugin(); var context = CreateContext(); var payload = "stella-fips-payload"u8.ToArray(); await plugin.InitializeAsync(context, CancellationToken.None); Assert.Equal(PluginLifecycleState.Active, plugin.State); Assert.True(plugin.CanHandle(CryptoOperation.Sign, "RSA-SHA256")); Assert.False(plugin.CanHandle(CryptoOperation.Sign, "SM2-SM3")); var signature = await plugin.SignAsync(payload, new CryptoSignOptions("RSA-SHA256", "fips-signing-key"), CancellationToken.None); Assert.NotEmpty(signature); var isValid = await plugin.VerifyAsync( payload, signature, new CryptoVerifyOptions("RSA-SHA256", "fips-signing-key"), CancellationToken.None); Assert.True(isValid); var isTamperedValid = await plugin.VerifyAsync( "tampered-fips-payload"u8.ToArray(), signature, new CryptoVerifyOptions("RSA-SHA256", "fips-signing-key"), CancellationToken.None); Assert.False(isTamperedValid); var encrypted = await plugin.EncryptAsync( payload, new CryptoEncryptOptions("AES-256-GCM", "fips-encryption-key"), CancellationToken.None); var decrypted = await plugin.DecryptAsync( encrypted, new CryptoDecryptOptions("AES-256-GCM", "fips-encryption-key"), CancellationToken.None); Assert.Equal(payload, decrypted); await plugin.DisposeAsync(); Assert.Equal(PluginLifecycleState.Stopped, plugin.State); } [Fact] public async Task GostPlugin_SignsAndVerifiesAndSupportsHashing() { var plugin = new GostPlugin(); var context = CreateContext(); var payload = "stella-gost-payload"u8.ToArray(); await plugin.InitializeAsync(context, CancellationToken.None); Assert.Equal(PluginLifecycleState.Active, plugin.State); Assert.True(plugin.CanHandle(CryptoOperation.Sign, "GOST-R34.10-2012-256")); Assert.False(plugin.CanHandle(CryptoOperation.Sign, "RSA-SHA256")); var signature = await plugin.SignAsync(payload, new CryptoSignOptions("GOST-R34.10-2012-256", "gost-signing-key"), CancellationToken.None); Assert.NotEmpty(signature); var isValid = await plugin.VerifyAsync( payload, signature, new CryptoVerifyOptions("GOST-R34.10-2012-256", "gost-signing-key"), CancellationToken.None); Assert.True(isValid); var hash = await plugin.HashAsync(payload, "GOST-R34.11-2012-256", CancellationToken.None); Assert.Equal(32, hash.Length); await plugin.DisposeAsync(); Assert.Equal(PluginLifecycleState.Stopped, plugin.State); } [Fact] public async Task SmPlugin_RoundTripsSignVerifyAndEncryptDecrypt() { var plugin = new SmPlugin(); var context = CreateContext(); var payload = "stella-sm-payload"u8.ToArray(); await plugin.InitializeAsync(context, CancellationToken.None); Assert.Equal(PluginLifecycleState.Active, plugin.State); Assert.True(plugin.CanHandle(CryptoOperation.Sign, "SM2-SM3")); Assert.False(plugin.CanHandle(CryptoOperation.Sign, "RSA-SHA256")); var signature = await plugin.SignAsync(payload, new CryptoSignOptions("SM2-SM3", "sm-signing-key"), CancellationToken.None); Assert.NotEmpty(signature); var isValid = await plugin.VerifyAsync( payload, signature, new CryptoVerifyOptions("SM2-SM3", "sm-signing-key"), CancellationToken.None); Assert.True(isValid); var encrypted = await plugin.EncryptAsync( payload, new CryptoEncryptOptions("SM4-GCM", "sm-encryption-key"), CancellationToken.None); var decrypted = await plugin.DecryptAsync( encrypted, new CryptoDecryptOptions("SM4-GCM", "sm-encryption-key"), CancellationToken.None); Assert.Equal(payload, decrypted); await plugin.DisposeAsync(); Assert.Equal(PluginLifecycleState.Stopped, plugin.State); } [Fact] public async Task HsmPlugin_SimulationMode_RoundTripsAndReportsHealth() { var plugin = new HsmPlugin(); var context = CreateContext(); var payload = "stella-hsm-payload"u8.ToArray(); await plugin.InitializeAsync(context, CancellationToken.None); Assert.Equal(PluginLifecycleState.Active, plugin.State); Assert.True(plugin.CanHandle(CryptoOperation.Sign, "HSM-RSA-SHA256")); Assert.False(plugin.CanHandle(CryptoOperation.Sign, "GOST-R34.10-2012-256")); var signature = await plugin.SignAsync(payload, new CryptoSignOptions("HSM-RSA-SHA256", "hsm-signing-key"), CancellationToken.None); Assert.NotEmpty(signature); var isValid = await plugin.VerifyAsync( payload, signature, new CryptoVerifyOptions("HSM-RSA-SHA256", "hsm-signing-key"), CancellationToken.None); Assert.True(isValid); var encrypted = await plugin.EncryptAsync( payload, new CryptoEncryptOptions("HSM-AES-256-GCM", "hsm-encryption-key"), CancellationToken.None); var decrypted = await plugin.DecryptAsync( encrypted, new CryptoDecryptOptions("HSM-AES-256-GCM", "hsm-encryption-key"), CancellationToken.None); Assert.Equal(payload, decrypted); var health = await plugin.HealthCheckAsync(CancellationToken.None); Assert.Equal("Healthy", health.Status.ToString()); var unsupported = Assert.ThrowsAsync(() => plugin.SignAsync(payload, new CryptoSignOptions("HSM-UNKNOWN", "hsm-signing-key"), CancellationToken.None)); await unsupported; await plugin.DisposeAsync(); Assert.Equal(PluginLifecycleState.Stopped, plugin.State); } [Fact] public async Task EidasPlugin_WithoutCertificate_FailsClosedForSigning() { var plugin = new EidasPlugin(); var context = CreateContext(); var payload = "stella-eidas-payload"u8.ToArray(); await plugin.InitializeAsync(context, CancellationToken.None); Assert.Equal(PluginLifecycleState.Active, plugin.State); Assert.True(plugin.CanHandle(CryptoOperation.Sign, "eIDAS-RSA-SHA256")); Assert.False(plugin.CanHandle(CryptoOperation.Sign, "RSA-SHA256")); var exception = await Assert.ThrowsAsync(() => plugin.SignAsync(payload, new CryptoSignOptions("eIDAS-RSA-SHA256", "eidas-signing-key"), CancellationToken.None)); Assert.Contains("No signing certificate configured", exception.Message, StringComparison.OrdinalIgnoreCase); await plugin.DisposeAsync(); Assert.Equal(PluginLifecycleState.Stopped, plugin.State); } [Fact] public async Task MultiProfileSigner_SignsWithAllConfiguredProfiles_UsingFixedTimeProvider() { var fixedNow = new DateTimeOffset(2026, 2, 11, 8, 0, 0, TimeSpan.Zero); var signers = new IContentSigner[] { new TestContentSigner("signer-ed", SignatureProfile.EdDsa, "Ed25519"), new TestContentSigner("signer-gost", SignatureProfile.Gost2012, "GOST3410-2012-256") }; using var signer = new MultiProfileSigner(signers, NullLogger.Instance, new FixedTimeProvider(fixedNow)); var result = await signer.SignAllAsync("stella-multiprofile-payload"u8.ToArray(), CancellationToken.None); Assert.Equal(2, result.Signatures.Count); Assert.Equal(fixedNow, result.SignedAt); Assert.Contains(result.Signatures, s => s.Profile == SignatureProfile.EdDsa); Assert.Contains(result.Signatures, s => s.Profile == SignatureProfile.Gost2012); } [Fact] public async Task MultiProfileSigner_WhenAnySignerFails_PropagatesException() { var signers = new IContentSigner[] { new TestContentSigner("signer-ed", SignatureProfile.EdDsa, "Ed25519"), new TestContentSigner("signer-fail", SignatureProfile.Eidas, "eIDAS-RSA-SHA256", shouldThrow: true) }; using var signer = new MultiProfileSigner(signers, NullLogger.Instance); await Assert.ThrowsAsync(() => signer.SignAllAsync("stella-multiprofile-fail"u8.ToArray(), CancellationToken.None)); } private static TestPluginContext CreateContext() { var options = new PluginTestHostOptions { EnableLogging = false }; return new TestPluginContext(options); } private sealed class FixedTimeProvider(DateTimeOffset fixedNow) : TimeProvider { public override DateTimeOffset GetUtcNow() => fixedNow; } private sealed class TestContentSigner( string keyId, SignatureProfile profile, string algorithm, bool shouldThrow = false) : IContentSigner { public string KeyId => keyId; public SignatureProfile Profile => profile; public string Algorithm => algorithm; public Task SignAsync(ReadOnlyMemory payload, CancellationToken ct = default) { if (shouldThrow) { throw new InvalidOperationException($"Signing failed for {keyId}"); } var signature = payload.ToArray(); Array.Reverse(signature); return Task.FromResult(new SignatureResult { KeyId = keyId, Profile = profile, Algorithm = algorithm, Signature = signature, SignedAt = new DateTimeOffset(2026, 2, 11, 8, 0, 0, TimeSpan.Zero) }); } public byte[]? GetPublicKey() => [1, 2, 3, 4]; public void Dispose() { } } }