Files
git.stella-ops.org/src/__Libraries/StellaOps.Cryptography.Plugin.SmSoft/SmSoftCryptoProvider.cs

292 lines
9.6 KiB
C#

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;
/// <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(_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<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(_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<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();
}