Files
git.stella-ops.org/src/Cryptography/__Tests/StellaOps.Cryptography.Tests/CryptoProviderPluginBehaviorTests.cs
2026-02-12 10:27:23 +02:00

278 lines
10 KiB
C#

// 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<NotSupportedException>(() =>
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<InvalidOperationException>(() =>
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<MultiProfileSigner>.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<MultiProfileSigner>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() =>
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<SignatureResult> SignAsync(ReadOnlyMemory<byte> 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()
{
}
}
}