292 lines
9.6 KiB
C#
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();
|
|
}
|