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.Asn1.Pkcs; 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; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using static StellaOps.Localization.T; namespace StellaOps.Cryptography.Plugin.SmSoft; /// /// Software-only SM2/SM3 provider (non-certified). Guarded by SM_SOFT_ALLOWED env by default. /// public sealed class SmSoftCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics { private const string EnvGate = "SM_SOFT_ALLOWED"; private readonly ConcurrentDictionary keys = new(StringComparer.OrdinalIgnoreCase); private readonly ILogger logger; private readonly SmSoftProviderOptions options; public SmSoftCryptoProvider( IOptions? optionsAccessor = null, ILogger? logger = null) { options = optionsAccessor?.Value ?? new SmSoftProviderOptions(); this.logger = logger ?? NullLogger.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(_t("crypto.sm.no_password_hashing")); public ICryptoHasher GetHasher(string algorithmId) { EnsureAllowed(); if (!string.Equals(algorithmId, HashAlgorithms.Sm3, StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException(_t("crypto.provider.hash_not_supported", algorithmId, 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(_t("crypto.provider.algorithm_not_supported", algorithmId, Name)); } if (!keys.TryGetValue(keyReference.KeyId, out var entry)) { throw new KeyNotFoundException(_t("crypto.provider.key_not_registered", keyReference.KeyId, 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(_t("crypto.provider.algorithm_not_supported", signingKey.AlgorithmId, Name)); } // Accept raw key bytes (PKCS#8 DER) or ECParameters are not SM2-compatible in BCL. if (signingKey.PrivateKey.IsEmpty) { throw new InvalidOperationException(_t("crypto.sm.raw_key_required")); } 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 GetSigningKeys() => Array.Empty(); // software keys are managed externally or via raw bytes; we don't expose private material. public IEnumerable DescribeKeys() { foreach (var entry in keys.Values) { yield return new CryptoProviderKeyDescriptor( Name, entry.KeyId, SignatureAlgorithms.Sm2, new Dictionary(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(_t("crypto.sm.disabled", Name, EnvGate)); } } 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(_t("crypto.sm.unsupported_format")); } } 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 SignAsync(ReadOnlyMemory 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 VerifyAsync(ReadOnlyMemory data, ReadOnlyMemory 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 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 data) => Convert.ToHexString(ComputeHash(data)).ToLowerInvariant(); }