feat(api): Implement Console Export Client and Models
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
mock-dev-release / package-mock-release (push) Has been cancelled

- Added ConsoleExportClient for managing export requests and responses.
- Introduced ConsoleExportRequest and ConsoleExportResponse models.
- Implemented methods for creating and retrieving exports with appropriate headers.

feat(crypto): Add Software SM2/SM3 Cryptography Provider

- Implemented SmSoftCryptoProvider for software-only SM2/SM3 cryptography.
- Added support for signing and verification using SM2 algorithm.
- Included hashing functionality with SM3 algorithm.
- Configured options for loading keys from files and environment gate checks.

test(crypto): Add unit tests for SmSoftCryptoProvider

- Created comprehensive tests for signing, verifying, and hashing functionalities.
- Ensured correct behavior for key management and error handling.

feat(api): Enhance Console Export Models

- Expanded ConsoleExport models to include detailed status and event types.
- Added support for various export formats and notification options.

test(time): Implement TimeAnchorPolicyService tests

- Developed tests for TimeAnchorPolicyService to validate time anchors.
- Covered scenarios for anchor validation, drift calculation, and policy enforcement.
This commit is contained in:
StellaOps Bot
2025-12-07 00:27:33 +02:00
parent 9bd6a73926
commit 0de92144d2
229 changed files with 32351 additions and 1481 deletions

View File

@@ -10,6 +10,7 @@ using StellaOps.Cryptography.Plugin.CryptoPro;
#endif
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
using StellaOps.Cryptography.Plugin.OpenSslGost;
using StellaOps.Cryptography.Plugin.SmSoft;
namespace StellaOps.Cryptography.DependencyInjection;
@@ -66,6 +67,7 @@ public static class CryptoServiceCollectionExtensions
services.TryAddSingleton<ICryptoHash, DefaultCryptoHash>();
services.TryAddSingleton<ICryptoHmac, DefaultCryptoHmac>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, SmSoftCryptoProvider>());
services.TryAddSingleton<ICryptoProviderRegistry>(sp =>
{

View File

@@ -12,6 +12,7 @@
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />

View File

@@ -0,0 +1,290 @@
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Asn1.GM;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Plugin.SmSoft;
/// <summary>
/// Software-only SM2/SM3 provider (non-certified). Guarded by SM_SOFT_ALLOWED env by default.
/// </summary>
public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
{
private const string EnvGate = "SM_SOFT_ALLOWED";
private readonly ConcurrentDictionary<string, SmSoftKeyEntry> keys = new(StringComparer.OrdinalIgnoreCase);
private readonly ILogger<SmSoftCryptoProvider> logger;
private readonly SmSoftProviderOptions options;
public SmSoftCryptoProvider(
IOptions<SmSoftProviderOptions>? optionsAccessor = null,
ILogger<SmSoftCryptoProvider>? logger = null)
{
options = optionsAccessor?.Value ?? new SmSoftProviderOptions();
this.logger = logger ?? NullLogger<SmSoftCryptoProvider>.Instance;
foreach (var key in options.Keys)
{
TryLoadKeyFromFile(key);
}
}
public string Name => "cn.sm.soft";
public bool Supports(CryptoCapability capability, string algorithmId)
{
if (!GateEnabled())
{
return false;
}
return capability switch
{
CryptoCapability.Signing or CryptoCapability.Verification
=> string.Equals(algorithmId, SignatureAlgorithms.Sm2, StringComparison.OrdinalIgnoreCase),
CryptoCapability.ContentHashing
=> string.Equals(algorithmId, HashAlgorithms.Sm3, StringComparison.OrdinalIgnoreCase),
_ => false
};
}
public IPasswordHasher GetPasswordHasher(string algorithmId)
=> throw new NotSupportedException("SM provider does not expose password hashing.");
public ICryptoHasher GetHasher(string algorithmId)
{
EnsureAllowed();
if (!string.Equals(algorithmId, HashAlgorithms.Sm3, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Hash algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
return new Sm3CryptoHasher();
}
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
EnsureAllowed();
ArgumentNullException.ThrowIfNull(keyReference);
if (!string.Equals(algorithmId, SignatureAlgorithms.Sm2, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Signing algorithm '{algorithmId}' is not supported by provider '{Name}'.");
}
if (!keys.TryGetValue(keyReference.KeyId, out var entry))
{
throw new KeyNotFoundException($"Signing key '{keyReference.KeyId}' is not registered with provider '{Name}'.");
}
return new Sm2SoftSigner(entry);
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
{
EnsureAllowed();
ArgumentNullException.ThrowIfNull(signingKey);
if (!string.Equals(signingKey.AlgorithmId, SignatureAlgorithms.Sm2, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Signing algorithm '{signingKey.AlgorithmId}' is not supported by provider '{Name}'.");
}
// Accept raw key bytes (PKCS#8 DER) or ECParameters are not SM2-compatible in BCL.
if (signingKey.PrivateKey.IsEmpty)
{
throw new InvalidOperationException("SM2 provider requires raw private key bytes (PKCS#8 DER).");
}
var keyPair = LoadKeyPair(signingKey.PrivateKey.ToArray());
var entry = new SmSoftKeyEntry(signingKey.Reference.KeyId, keyPair);
keys.AddOrUpdate(signingKey.Reference.KeyId, entry, (_, _) => entry);
}
public bool RemoveSigningKey(string keyId)
{
if (string.IsNullOrWhiteSpace(keyId))
{
return false;
}
return keys.TryRemove(keyId, out _);
}
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> Array.Empty<CryptoSigningKey>(); // software keys are managed externally or via raw bytes; we don't expose private material.
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys()
{
foreach (var entry in keys.Values)
{
yield return new CryptoProviderKeyDescriptor(
Name,
entry.KeyId,
SignatureAlgorithms.Sm2,
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["provider"] = Name,
["label"] = entry.KeyId,
["software"] = "true",
["certified"] = "false"
});
}
}
private bool GateEnabled()
{
if (!options.RequireEnvironmentGate)
{
return true;
}
return string.Equals(Environment.GetEnvironmentVariable(EnvGate), "1", StringComparison.OrdinalIgnoreCase);
}
private void EnsureAllowed()
{
if (!GateEnabled())
{
throw new InvalidOperationException(
$"Provider '{Name}' is disabled. Set {EnvGate}=1 (or disable RequireEnvironmentGate) to enable software SM2/SM3.");
}
}
private void TryLoadKeyFromFile(SmSoftKeyOptions key)
{
if (string.IsNullOrWhiteSpace(key.KeyId) || string.IsNullOrWhiteSpace(key.PrivateKeyPath))
{
return;
}
try
{
var bytes = File.ReadAllBytes(key.PrivateKeyPath);
var keyPair = LoadKeyPair(bytes);
var entry = new SmSoftKeyEntry(key.KeyId, keyPair);
keys.TryAdd(key.KeyId, entry);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to load SM2 key {KeyId} from {Path}", key.KeyId, key.PrivateKeyPath);
}
}
private static AsymmetricCipherKeyPair LoadKeyPair(byte[] data)
{
// Try PEM first, then DER PKCS#8
try
{
using var reader = new StreamReader(new MemoryStream(data));
var pem = new PemReader(reader).ReadObject();
if (pem is AsymmetricCipherKeyPair pair)
{
return pair;
}
if (pem is ECPrivateKeyParameters priv)
{
var q = priv.Parameters.G.Multiply(priv.D);
var pub = new ECPublicKeyParameters(q, priv.Parameters);
return new AsymmetricCipherKeyPair(pub, priv);
}
}
catch
{
// Fall through to DER parsing
}
var key = PrivateKeyFactory.CreateKey(data);
if (key is ECPrivateKeyParameters ecPriv)
{
var q = ecPriv.Parameters.G.Multiply(ecPriv.D);
var pub = new ECPublicKeyParameters(q, ecPriv.Parameters);
return new AsymmetricCipherKeyPair(pub, ecPriv);
}
throw new InvalidOperationException("Unsupported SM2 key format. Expect PEM or PKCS#8 DER.");
}
}
internal sealed record SmSoftKeyEntry(string KeyId, AsymmetricCipherKeyPair KeyPair);
internal sealed class Sm2SoftSigner : ICryptoSigner
{
private static readonly byte[] DefaultUserId = System.Text.Encoding.ASCII.GetBytes("1234567812345678");
private readonly SmSoftKeyEntry entry;
public Sm2SoftSigner(SmSoftKeyEntry entry)
{
this.entry = entry;
}
public string KeyId => entry.KeyId;
public string AlgorithmId => SignatureAlgorithms.Sm2;
public async ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var signer = new SM2Signer();
signer.Init(true, new ParametersWithID(entry.KeyPair.Private, DefaultUserId));
signer.BlockUpdate(data.Span);
return await Task.FromResult(signer.GenerateSignature());
}
public async ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var verifier = new SM2Signer();
verifier.Init(false, new ParametersWithID(entry.KeyPair.Public, DefaultUserId));
verifier.BlockUpdate(data.Span);
var result = verifier.VerifySignature(signature.Span.ToArray());
return await Task.FromResult(result);
}
public JsonWebKey ExportPublicJsonWebKey()
{
var pub = (ECPublicKeyParameters)entry.KeyPair.Public;
var q = pub.Q.Normalize();
var x = q.XCoord.GetEncoded();
var y = q.YCoord.GetEncoded();
return new JsonWebKey
{
Kid = KeyId,
Kty = "EC",
Crv = "SM2",
Alg = SignatureAlgorithms.Sm2,
Use = "sig",
X = Base64UrlEncoder.Encode(x),
Y = Base64UrlEncoder.Encode(y)
};
}
}
internal sealed class Sm3CryptoHasher : ICryptoHasher
{
public string AlgorithmId => HashAlgorithms.Sm3;
public byte[] ComputeHash(ReadOnlySpan<byte> data)
{
var digest = new Org.BouncyCastle.Crypto.Digests.SM3Digest();
digest.BlockUpdate(data);
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;
}
public string ComputeHashHex(ReadOnlySpan<byte> data)
=> Convert.ToHexString(ComputeHash(data)).ToLowerInvariant();
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
namespace StellaOps.Cryptography.Plugin.SmSoft;
/// <summary>
/// Configuration for the software-only SM provider.
/// </summary>
public sealed class SmSoftProviderOptions
{
/// <summary>
/// Optional key entries loaded from PEM/DER to seed the provider.
/// </summary>
public IList<SmSoftKeyOptions> Keys { get; } = new List<SmSoftKeyOptions>();
/// <summary>
/// Require an explicit opt-in (default: true). If false, provider is active without env gate.
/// </summary>
public bool RequireEnvironmentGate { get; set; } = true;
}
public sealed class SmSoftKeyOptions
{
public string KeyId { get; set; } = string.Empty;
/// <summary>Private key file path (PEM or DER) for SM2.</summary>
public string PrivateKeyPath { get; set; } = string.Empty;
/// <summary>Signature algorithm, default SM2.</summary>
public string Algorithm { get; set; } = StellaOps.Cryptography.SignatureAlgorithms.Sm2;
/// <summary>Optional label or metadata.</summary>
public string? Label { get; set; }
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -12,5 +12,5 @@ public static class SignatureAlgorithms
public const string EdDsa = "EdDSA";
public const string GostR3410_2012_256 = "GOST12-256";
public const string GostR3410_2012_512 = "GOST12-512";
public const string Sm2 = "SM2";
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Text;
using System.Threading.Tasks;
using Org.BouncyCastle.Asn1.GM;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Asn1.Pkcs;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.SmSoft;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class SmSoftCryptoProviderTests : IDisposable
{
private readonly string? _originalGate;
public SmSoftCryptoProviderTests()
{
_originalGate = Environment.GetEnvironmentVariable("SM_SOFT_ALLOWED");
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", "1");
}
[Fact]
public async Task SignAndVerify_Sm2_Works()
{
var provider = new SmSoftCryptoProvider();
var key = GenerateSm2Key();
provider.UpsertSigningKey(key);
var signer = provider.GetSigner(SignatureAlgorithms.Sm2, key.Reference);
var payload = Encoding.UTF8.GetBytes("sm2-payload");
var signature = await signer.SignAsync(payload);
Assert.True(await signer.VerifyAsync(payload, signature));
var jwk = signer.ExportPublicJsonWebKey();
Assert.Equal(SignatureAlgorithms.Sm2, jwk.Alg);
Assert.Equal("SM2", jwk.Crv);
Assert.Equal(key.Reference.KeyId, jwk.Kid);
Assert.False(string.IsNullOrEmpty(jwk.X));
Assert.False(string.IsNullOrEmpty(jwk.Y));
}
[Fact]
public void Hash_Sm3_Works()
{
var provider = new SmSoftCryptoProvider();
var hasher = provider.GetHasher(HashAlgorithms.Sm3);
var digest = hasher.ComputeHashHex(Encoding.UTF8.GetBytes("abc"));
// Known SM3("abc") = 66c7f0f462eeedd9d1f2d46bdc10e4e2 4167c4875cf2f7a2 297da02b8f4ba8e0
Assert.Equal("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0", digest);
}
private static CryptoSigningKey GenerateSm2Key()
{
var generator = new ECKeyPairGenerator("EC");
var curve = GMNamedCurves.GetByName("SM2P256V1");
var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());
generator.Init(new ECKeyGenerationParameters(domain, new SecureRandom(new CryptoApiRandomGenerator())));
var pair = generator.GenerateKeyPair();
var privateDer = PrivateKeyInfoFactory.CreatePrivateKeyInfo(pair.Private).ToAsn1Object().GetDerEncoded();
var keyRef = new CryptoKeyReference("sm-soft-test");
return new CryptoSigningKey(keyRef, SignatureAlgorithms.Sm2, privateDer, DateTimeOffset.UtcNow);
}
public void Dispose()
{
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", _originalGate);
}
}

View File

@@ -20,6 +20,7 @@
<ProjectReference Include="../../StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.BouncyCastle/StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.OpenSslGost/StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.SmSoft/StellaOps.Cryptography.Plugin.SmSoft.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.CryptoPro/StellaOps.Cryptography.Plugin.CryptoPro.csproj" />