save checkpoint: save features
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
// 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin\StellaOps.Cryptography.Plugin.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Eidas\StellaOps.Cryptography.Plugin.Eidas.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Fips\StellaOps.Cryptography.Plugin.Fips.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Gost\StellaOps.Cryptography.Plugin.Gost.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Hsm\StellaOps.Cryptography.Plugin.Hsm.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Cryptography.Plugin.Sm\StellaOps.Cryptography.Plugin.Sm.csproj" />
|
||||
<ProjectReference Include="..\..\..\Plugin\StellaOps.Plugin.Testing\StellaOps.Plugin.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -6,3 +6,4 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Cryptography/__Tests/StellaOps.Cryptography.Tests/StellaOps.Cryptography.Tests.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| QA-CRYPTO-RECHECK-004 | DONE | Added deterministic plugin behavior tests (FIPS/GOST/SM/HSM/eIDAS fail-closed + MultiProfileSigner) to close checked-feature replay coverage gaps. |
|
||||
|
||||
Reference in New Issue
Block a user