278 lines
10 KiB
C#
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()
|
|
{
|
|
}
|
|
}
|
|
}
|