release orchestrator v1 draft and build fixes
This commit is contained in:
364
src/Cryptography/StellaOps.Cryptography.Plugin.Sm/SmPlugin.cs
Normal file
364
src/Cryptography/StellaOps.Cryptography.Plugin.Sm/SmPlugin.cs
Normal file
@@ -0,0 +1,364 @@
|
||||
namespace StellaOps.Cryptography.Plugin.Sm;
|
||||
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
using Org.BouncyCastle.Crypto.Engines;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Modes;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Crypto.Signers;
|
||||
using Org.BouncyCastle.Math;
|
||||
using Org.BouncyCastle.Math.EC;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Plugin.Abstractions;
|
||||
using StellaOps.Plugin.Abstractions.Capabilities;
|
||||
using StellaOps.Plugin.Abstractions.Context;
|
||||
using StellaOps.Plugin.Abstractions.Health;
|
||||
using StellaOps.Plugin.Abstractions.Lifecycle;
|
||||
|
||||
/// <summary>
|
||||
/// Chinese national cryptographic standards plugin.
|
||||
/// Implements SM2 (signatures), SM3 (hash), and SM4 (symmetric encryption).
|
||||
/// </summary>
|
||||
public sealed class SmPlugin : CryptoPluginBase
|
||||
{
|
||||
private SmOptions? _options;
|
||||
private AsymmetricCipherKeyPair? _keyPair;
|
||||
private readonly SecureRandom _random = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PluginInfo Info => new(
|
||||
Id: "com.stellaops.crypto.sm",
|
||||
Name: "Chinese SM Cryptography Provider",
|
||||
Version: "1.0.0",
|
||||
Vendor: "Stella Ops",
|
||||
Description: "Chinese national cryptographic standards SM2/SM3/SM4 (GM/T 0003-0004)",
|
||||
LicenseId: "AGPL-3.0-or-later");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<string> SupportedAlgorithms => new[]
|
||||
{
|
||||
"SM2-SM3",
|
||||
"SM2-SHA256",
|
||||
"SM3",
|
||||
"SM4-CBC",
|
||||
"SM4-ECB",
|
||||
"SM4-GCM"
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task InitializeCryptoServiceAsync(IPluginContext context, CancellationToken ct)
|
||||
{
|
||||
_options = context.Configuration.Bind<SmOptions>() ?? new SmOptions();
|
||||
|
||||
if (!string.IsNullOrEmpty(_options.PrivateKeyHex))
|
||||
{
|
||||
LoadKeyFromHex(_options.PrivateKeyHex);
|
||||
Context?.Logger.Info("SM provider initialized with configured key");
|
||||
}
|
||||
else if (_options.GenerateKeyOnInit)
|
||||
{
|
||||
_keyPair = GenerateSm2KeyPair();
|
||||
Context?.Logger.Info("SM provider initialized with generated key pair");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanHandle(CryptoOperation operation, string algorithm)
|
||||
{
|
||||
return algorithm.StartsWith("SM", StringComparison.OrdinalIgnoreCase) &&
|
||||
SupportedAlgorithms.Contains(algorithm, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> SignAsync(ReadOnlyMemory<byte> data, CryptoSignOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (_keyPair == null)
|
||||
{
|
||||
throw new InvalidOperationException("No signing key available.");
|
||||
}
|
||||
|
||||
// SM2 signature with SM3 digest
|
||||
var signer = new SM2Signer();
|
||||
signer.Init(true, _keyPair.Private);
|
||||
signer.BlockUpdate(data.Span.ToArray(), 0, data.Length);
|
||||
var signature = signer.GenerateSignature();
|
||||
|
||||
Context?.Logger.Debug("Signed {DataLength} bytes with SM2", data.Length);
|
||||
|
||||
return Task.FromResult(signature);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CryptoVerifyOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (_keyPair == null)
|
||||
{
|
||||
throw new InvalidOperationException("No verification key available.");
|
||||
}
|
||||
|
||||
var verifier = new SM2Signer();
|
||||
verifier.Init(false, _keyPair.Public);
|
||||
verifier.BlockUpdate(data.Span.ToArray(), 0, data.Length);
|
||||
var isValid = verifier.VerifySignature(signature.ToArray());
|
||||
|
||||
Context?.Logger.Debug("Verified SM2 signature: {IsValid}", isValid);
|
||||
|
||||
return Task.FromResult(isValid);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> EncryptAsync(ReadOnlyMemory<byte> data, CryptoEncryptOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
var keyBytes = GetSymmetricKey(options.KeyId);
|
||||
byte[] encrypted;
|
||||
|
||||
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
encrypted = Sm4GcmEncrypt(data.ToArray(), keyBytes, options.Iv, options.Aad);
|
||||
}
|
||||
else if (algorithm.Contains("CBC", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
encrypted = Sm4CbcEncrypt(data.ToArray(), keyBytes, options.Iv);
|
||||
}
|
||||
else
|
||||
{
|
||||
encrypted = Sm4EcbEncrypt(data.ToArray(), keyBytes);
|
||||
}
|
||||
|
||||
Context?.Logger.Debug("Encrypted {DataLength} bytes with {Algorithm}", data.Length, algorithm);
|
||||
|
||||
return Task.FromResult(encrypted);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> DecryptAsync(ReadOnlyMemory<byte> data, CryptoDecryptOptions options, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var algorithm = options.Algorithm;
|
||||
var keyBytes = GetSymmetricKey(options.KeyId);
|
||||
byte[] decrypted;
|
||||
|
||||
if (algorithm.Contains("GCM", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
decrypted = Sm4GcmDecrypt(data.ToArray(), keyBytes, options.Iv, options.Aad);
|
||||
}
|
||||
else if (algorithm.Contains("CBC", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
decrypted = Sm4CbcDecrypt(data.ToArray(), keyBytes, options.Iv);
|
||||
}
|
||||
else
|
||||
{
|
||||
decrypted = Sm4EcbDecrypt(data.ToArray(), keyBytes);
|
||||
}
|
||||
|
||||
Context?.Logger.Debug("Decrypted {DataLength} bytes with {Algorithm}", data.Length, algorithm);
|
||||
|
||||
return Task.FromResult(decrypted);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<byte[]> HashAsync(ReadOnlyMemory<byte> data, string algorithm, CancellationToken ct)
|
||||
{
|
||||
EnsureActive();
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var digest = new SM3Digest();
|
||||
digest.BlockUpdate(data.Span.ToArray(), 0, data.Length);
|
||||
var hash = new byte[digest.GetDigestSize()];
|
||||
digest.DoFinal(hash, 0);
|
||||
|
||||
Context?.Logger.Debug("Computed SM3 hash of {DataLength} bytes", data.Length);
|
||||
|
||||
return Task.FromResult(hash);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ValueTask DisposeAsync()
|
||||
{
|
||||
_keyPair = null;
|
||||
State = PluginLifecycleState.Stopped;
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private AsymmetricCipherKeyPair GenerateSm2KeyPair()
|
||||
{
|
||||
var domainParams = GetSm2DomainParameters();
|
||||
var generator = new ECKeyPairGenerator();
|
||||
generator.Init(new ECKeyGenerationParameters(domainParams, _random));
|
||||
return generator.GenerateKeyPair();
|
||||
}
|
||||
|
||||
private void LoadKeyFromHex(string privateKeyHex)
|
||||
{
|
||||
var d = new BigInteger(privateKeyHex, 16);
|
||||
var domainParams = GetSm2DomainParameters();
|
||||
var privateKey = new ECPrivateKeyParameters(d, domainParams);
|
||||
var q = domainParams.G.Multiply(d);
|
||||
var publicKey = new ECPublicKeyParameters(q, domainParams);
|
||||
_keyPair = new AsymmetricCipherKeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
private static ECDomainParameters GetSm2DomainParameters()
|
||||
{
|
||||
// SM2 recommended parameters (GM/T 0003.5-2012)
|
||||
var p = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF", 16);
|
||||
var a = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC", 16);
|
||||
var b = new BigInteger("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93", 16);
|
||||
var n = new BigInteger("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123", 16);
|
||||
var h = BigInteger.One;
|
||||
var gx = new BigInteger("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7", 16);
|
||||
var gy = new BigInteger("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0", 16);
|
||||
|
||||
var curve = new FpCurve(p, a, b, n, h);
|
||||
var g = curve.CreatePoint(gx, gy);
|
||||
return new ECDomainParameters(curve, g, n, h);
|
||||
}
|
||||
|
||||
private byte[] GetSymmetricKey(string keyId)
|
||||
{
|
||||
// Derive 128-bit key from key ID using SM3
|
||||
var digest = new SM3Digest();
|
||||
var keyIdBytes = System.Text.Encoding.UTF8.GetBytes(keyId);
|
||||
digest.BlockUpdate(keyIdBytes, 0, keyIdBytes.Length);
|
||||
var hash = new byte[32];
|
||||
digest.DoFinal(hash, 0);
|
||||
return hash.Take(16).ToArray(); // SM4 uses 128-bit keys
|
||||
}
|
||||
|
||||
private byte[] Sm4EcbEncrypt(byte[] data, byte[] key)
|
||||
{
|
||||
var engine = new SM4Engine();
|
||||
engine.Init(true, new KeyParameter(key));
|
||||
return ProcessBlocks(engine, data);
|
||||
}
|
||||
|
||||
private byte[] Sm4EcbDecrypt(byte[] data, byte[] key)
|
||||
{
|
||||
var engine = new SM4Engine();
|
||||
engine.Init(false, new KeyParameter(key));
|
||||
return ProcessBlocks(engine, data);
|
||||
}
|
||||
|
||||
private byte[] Sm4CbcEncrypt(byte[] data, byte[] key, byte[]? iv)
|
||||
{
|
||||
iv ??= GenerateIv(16);
|
||||
var cipher = new CbcBlockCipher(new SM4Engine());
|
||||
cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
|
||||
var encrypted = ProcessBlocks(cipher, data);
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
var result = new byte[iv.Length + encrypted.Length];
|
||||
Array.Copy(iv, 0, result, 0, iv.Length);
|
||||
Array.Copy(encrypted, 0, result, iv.Length, encrypted.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] Sm4CbcDecrypt(byte[] data, byte[] key, byte[]? iv)
|
||||
{
|
||||
if (iv == null)
|
||||
{
|
||||
// Extract IV from ciphertext
|
||||
iv = data.Take(16).ToArray();
|
||||
data = data.Skip(16).ToArray();
|
||||
}
|
||||
|
||||
var cipher = new CbcBlockCipher(new SM4Engine());
|
||||
cipher.Init(false, new ParametersWithIV(new KeyParameter(key), iv));
|
||||
return ProcessBlocks(cipher, data);
|
||||
}
|
||||
|
||||
private byte[] Sm4GcmEncrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
|
||||
{
|
||||
iv ??= GenerateIv(12);
|
||||
var cipher = new GcmBlockCipher(new SM4Engine());
|
||||
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad ?? Array.Empty<byte>());
|
||||
cipher.Init(true, parameters);
|
||||
|
||||
var output = new byte[cipher.GetOutputSize(data.Length)];
|
||||
var len = cipher.ProcessBytes(data, 0, data.Length, output, 0);
|
||||
cipher.DoFinal(output, len);
|
||||
|
||||
// Prepend IV to ciphertext
|
||||
var result = new byte[iv.Length + output.Length];
|
||||
Array.Copy(iv, 0, result, 0, iv.Length);
|
||||
Array.Copy(output, 0, result, iv.Length, output.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] Sm4GcmDecrypt(byte[] data, byte[] key, byte[]? iv, byte[]? aad)
|
||||
{
|
||||
if (iv == null)
|
||||
{
|
||||
iv = data.Take(12).ToArray();
|
||||
data = data.Skip(12).ToArray();
|
||||
}
|
||||
|
||||
var cipher = new GcmBlockCipher(new SM4Engine());
|
||||
var parameters = new AeadParameters(new KeyParameter(key), 128, iv, aad ?? Array.Empty<byte>());
|
||||
cipher.Init(false, parameters);
|
||||
|
||||
var output = new byte[cipher.GetOutputSize(data.Length)];
|
||||
var len = cipher.ProcessBytes(data, 0, data.Length, output, 0);
|
||||
cipher.DoFinal(output, len);
|
||||
return output;
|
||||
}
|
||||
|
||||
private static byte[] ProcessBlocks(IBlockCipher engine, byte[] data)
|
||||
{
|
||||
var blockSize = engine.GetBlockSize();
|
||||
var paddedLength = ((data.Length + blockSize - 1) / blockSize) * blockSize;
|
||||
var padded = new byte[paddedLength];
|
||||
Array.Copy(data, padded, data.Length);
|
||||
|
||||
var output = new byte[paddedLength];
|
||||
for (var i = 0; i < paddedLength; i += blockSize)
|
||||
{
|
||||
engine.ProcessBlock(padded, i, output, i);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private byte[] GenerateIv(int length)
|
||||
{
|
||||
var iv = new byte[length];
|
||||
_random.NextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for SM cryptography plugin.
|
||||
/// </summary>
|
||||
public sealed class SmOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key in hexadecimal format.
|
||||
/// </summary>
|
||||
public string? PrivateKeyHex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new key pair on initialization if no key configured.
|
||||
/// </summary>
|
||||
public bool GenerateKeyOnInit { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// User identifier for SM2 signature (ZA computation).
|
||||
/// </summary>
|
||||
public string UserId { get; init; } = "1234567812345678";
|
||||
}
|
||||
Reference in New Issue
Block a user