288 lines
11 KiB
C#
288 lines
11 KiB
C#
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using StellaOps.Auth.ServerIntegration;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Options;
|
|
using StellaOps.Cryptography;
|
|
using StellaOps.Cryptography.Plugin.SmSoft;
|
|
using System.Linq;
|
|
using System.Text.Json.Serialization;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
builder.Services.AddLogging();
|
|
builder.Services.AddSingleton(TimeProvider.System);
|
|
// Minimal crypto registry: only SM2 soft provider, no remote/http probing
|
|
builder.Services.AddSingleton<ICryptoProviderRegistry>(_ =>
|
|
{
|
|
var smOpts = Options.Create(new StellaOps.Cryptography.Plugin.SmSoft.SmSoftProviderOptions
|
|
{
|
|
RequireEnvironmentGate = false
|
|
});
|
|
var smProvider = new SmSoftCryptoProvider(smOpts);
|
|
var providers = new ICryptoProvider[] { smProvider };
|
|
var preferred = new[] { "cn.sm.soft" };
|
|
return new CryptoProviderRegistry(providers, preferred);
|
|
});
|
|
|
|
builder.Services.AddHttpContextAccessor();
|
|
builder.Services.AddEndpointsApiExplorer();
|
|
|
|
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
|
|
|
if (builder.Environment.IsDevelopment())
|
|
{
|
|
builder.TryAddStellaOpsLocalBinding("smremote");
|
|
}
|
|
var app = builder.Build();
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.LogStellaOpsLocalHostname("smremote");
|
|
}
|
|
|
|
app.UseStellaOpsCors();
|
|
|
|
app.MapGet("/health", () => Results.Ok(new SmHealthResponse("ok")));
|
|
|
|
app.MapGet("/status", (ICryptoProviderRegistry registry) =>
|
|
{
|
|
var algorithms = new[] { SignatureAlgorithms.Sm2 };
|
|
return Results.Ok(new SmStatusResponse(true, "cn.sm.soft", algorithms));
|
|
});
|
|
|
|
app.MapPost("/hash", (HashRequest req) =>
|
|
{
|
|
if (req is null ||
|
|
!TryGetSupportedHashAlgorithm(req.AlgorithmId, out var algorithmId) ||
|
|
!TryDecodeBase64(req.PayloadBase64, out var payload))
|
|
{
|
|
return Results.BadRequest("missing or invalid fields");
|
|
}
|
|
|
|
var digest = new Org.BouncyCastle.Crypto.Digests.SM3Digest();
|
|
digest.BlockUpdate(payload, 0, payload.Length);
|
|
var hash = new byte[digest.GetDigestSize()];
|
|
digest.DoFinal(hash, 0);
|
|
|
|
return Results.Ok(new HashResponse(
|
|
algorithmId,
|
|
Convert.ToBase64String(hash),
|
|
Convert.ToHexString(hash).ToLowerInvariant()));
|
|
});
|
|
|
|
app.MapPost("/encrypt", (EncryptRequest req) =>
|
|
{
|
|
if (req is null ||
|
|
!TryGetSupportedSm4Algorithm(req.AlgorithmId, out var algorithmId) ||
|
|
!TryDecodeBase64(req.KeyBase64, out var keyBytes) ||
|
|
!TryDecodeBase64(req.PayloadBase64, out var payload))
|
|
{
|
|
return Results.BadRequest("missing or invalid fields");
|
|
}
|
|
|
|
if (keyBytes.Length != 16)
|
|
{
|
|
return Results.BadRequest("invalid sm4 key length");
|
|
}
|
|
|
|
var cipher = new Org.BouncyCastle.Crypto.Paddings.PaddedBufferedBlockCipher(
|
|
new Org.BouncyCastle.Crypto.Engines.SM4Engine(),
|
|
new Org.BouncyCastle.Crypto.Paddings.Pkcs7Padding());
|
|
cipher.Init(true, new Org.BouncyCastle.Crypto.Parameters.KeyParameter(keyBytes));
|
|
var ciphertext = ProcessCipher(cipher, payload);
|
|
|
|
return Results.Ok(new EncryptResponse(algorithmId, Convert.ToBase64String(ciphertext)));
|
|
});
|
|
|
|
app.MapPost("/decrypt", (DecryptRequest req) =>
|
|
{
|
|
if (req is null ||
|
|
!TryGetSupportedSm4Algorithm(req.AlgorithmId, out var algorithmId) ||
|
|
!TryDecodeBase64(req.KeyBase64, out var keyBytes) ||
|
|
!TryDecodeBase64(req.CiphertextBase64, out var ciphertext))
|
|
{
|
|
return Results.BadRequest("missing or invalid fields");
|
|
}
|
|
|
|
if (keyBytes.Length != 16)
|
|
{
|
|
return Results.BadRequest("invalid sm4 key length");
|
|
}
|
|
|
|
try
|
|
{
|
|
var cipher = new Org.BouncyCastle.Crypto.Paddings.PaddedBufferedBlockCipher(
|
|
new Org.BouncyCastle.Crypto.Engines.SM4Engine(),
|
|
new Org.BouncyCastle.Crypto.Paddings.Pkcs7Padding());
|
|
cipher.Init(false, new Org.BouncyCastle.Crypto.Parameters.KeyParameter(keyBytes));
|
|
var payload = ProcessCipher(cipher, ciphertext);
|
|
return Results.Ok(new DecryptResponse(algorithmId, Convert.ToBase64String(payload)));
|
|
}
|
|
catch (Org.BouncyCastle.Crypto.InvalidCipherTextException)
|
|
{
|
|
return Results.BadRequest("invalid ciphertext");
|
|
}
|
|
});
|
|
|
|
app.MapPost("/sign", async (SignRequest req, ICryptoProviderRegistry registry, TimeProvider timeProvider, CancellationToken ct) =>
|
|
{
|
|
if (req is null ||
|
|
string.IsNullOrWhiteSpace(req.KeyId) ||
|
|
string.IsNullOrWhiteSpace(req.AlgorithmId) ||
|
|
!TryDecodeBase64(req.PayloadBase64, out var payload))
|
|
{
|
|
return Results.BadRequest("missing or invalid fields");
|
|
}
|
|
|
|
var provider = ResolveProvider(registry);
|
|
EnsureKeySeeded(provider, req.KeyId, timeProvider);
|
|
|
|
var resolution = registry.ResolveSigner(CryptoCapability.Signing, req.AlgorithmId, new CryptoKeyReference(req.KeyId, provider.Name), provider.Name);
|
|
var signer = resolution.Signer;
|
|
var signature = await signer.SignAsync(payload, ct);
|
|
return Results.Ok(new SignResponse(Convert.ToBase64String(signature)));
|
|
});
|
|
|
|
app.MapPost("/verify", async (VerifyRequest req, ICryptoProviderRegistry registry, TimeProvider timeProvider, CancellationToken ct) =>
|
|
{
|
|
if (req is null || string.IsNullOrWhiteSpace(req.KeyId) || string.IsNullOrWhiteSpace(req.AlgorithmId) ||
|
|
!TryDecodeBase64(req.PayloadBase64, out var payload) ||
|
|
!TryDecodeBase64(req.Signature, out var signature))
|
|
{
|
|
return Results.BadRequest("missing or invalid fields");
|
|
}
|
|
|
|
var provider = ResolveProvider(registry);
|
|
EnsureKeySeeded(provider, req.KeyId, timeProvider);
|
|
|
|
var resolution = registry.ResolveSigner(CryptoCapability.Signing, req.AlgorithmId, new CryptoKeyReference(req.KeyId, provider.Name), provider.Name);
|
|
var signer = resolution.Signer;
|
|
var ok = await signer.VerifyAsync(payload, signature, ct);
|
|
return Results.Ok(new VerifyResponse(ok));
|
|
});
|
|
|
|
app.Run();
|
|
|
|
static ICryptoProvider ResolveProvider(ICryptoProviderRegistry registry)
|
|
{
|
|
if (registry.TryResolve("cn.sm.remote.http", out var remote) && remote is not null)
|
|
{
|
|
return remote;
|
|
}
|
|
|
|
if (registry.TryResolve("cn.sm.soft", out var soft) && soft is not null)
|
|
{
|
|
return soft;
|
|
}
|
|
|
|
return registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Sm2);
|
|
}
|
|
|
|
static void EnsureKeySeeded(ICryptoProvider provider, string keyId, TimeProvider timeProvider)
|
|
{
|
|
// The soft provider hides private material via GetSigningKeys(), so rely on diagnostics DescribeKeys() to detect presence.
|
|
if (provider is ICryptoProviderDiagnostics diag &&
|
|
diag.DescribeKeys().Any(k => k.KeyId.Equals(keyId, StringComparison.OrdinalIgnoreCase))) return;
|
|
|
|
var curve = Org.BouncyCastle.Asn1.GM.GMNamedCurves.GetByName("SM2P256V1");
|
|
var domain = new Org.BouncyCastle.Crypto.Parameters.ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());
|
|
var generator = new Org.BouncyCastle.Crypto.Generators.ECKeyPairGenerator("EC");
|
|
generator.Init(new Org.BouncyCastle.Crypto.Parameters.ECKeyGenerationParameters(domain, new Org.BouncyCastle.Security.SecureRandom()));
|
|
var pair = generator.GenerateKeyPair();
|
|
var privateDer = Org.BouncyCastle.Pkcs.PrivateKeyInfoFactory.CreatePrivateKeyInfo(pair.Private).GetDerEncoded();
|
|
|
|
provider.UpsertSigningKey(new CryptoSigningKey(
|
|
new CryptoKeyReference(keyId, provider.Name),
|
|
SignatureAlgorithms.Sm2,
|
|
privateDer,
|
|
timeProvider.GetUtcNow()));
|
|
}
|
|
|
|
static bool TryDecodeBase64(string value, out byte[] result)
|
|
{
|
|
result = Array.Empty<byte>();
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
result = Convert.FromBase64String(value);
|
|
return true;
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool TryGetSupportedHashAlgorithm(string? value, out string algorithmId)
|
|
{
|
|
algorithmId = string.IsNullOrWhiteSpace(value) ? HashAlgorithms.Sm3 : value.Trim();
|
|
if (algorithmId.Equals(HashAlgorithms.Sm3, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
algorithmId = HashAlgorithms.Sm3;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static bool TryGetSupportedSm4Algorithm(string? value, out string algorithmId)
|
|
{
|
|
algorithmId = string.IsNullOrWhiteSpace(value) ? "SM4-ECB" : value.Trim().ToUpperInvariant();
|
|
if (algorithmId is "SM4" or "SM4-ECB")
|
|
{
|
|
algorithmId = "SM4-ECB";
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static byte[] ProcessCipher(Org.BouncyCastle.Crypto.BufferedBlockCipher cipher, byte[] input)
|
|
{
|
|
var output = new byte[cipher.GetOutputSize(input.Length)];
|
|
var written = cipher.ProcessBytes(input, 0, input.Length, output, 0);
|
|
written += cipher.DoFinal(output, written);
|
|
return output[..written];
|
|
}
|
|
|
|
public sealed record SmHealthResponse([property: JsonPropertyName("status")] string Status);
|
|
public sealed record SmStatusResponse(bool Available, string Provider, IEnumerable<string> Algorithms);
|
|
public sealed record HashRequest(
|
|
[property: JsonPropertyName("algorithmId")] string? AlgorithmId,
|
|
[property: JsonPropertyName("payloadBase64")] string PayloadBase64);
|
|
public sealed record HashResponse(
|
|
[property: JsonPropertyName("algorithmId")] string AlgorithmId,
|
|
[property: JsonPropertyName("hashBase64")] string HashBase64,
|
|
[property: JsonPropertyName("hashHex")] string HashHex);
|
|
public sealed record EncryptRequest(
|
|
[property: JsonPropertyName("algorithmId")] string? AlgorithmId,
|
|
[property: JsonPropertyName("keyBase64")] string KeyBase64,
|
|
[property: JsonPropertyName("payloadBase64")] string PayloadBase64);
|
|
public sealed record EncryptResponse(
|
|
[property: JsonPropertyName("algorithmId")] string AlgorithmId,
|
|
[property: JsonPropertyName("ciphertextBase64")] string CiphertextBase64);
|
|
public sealed record DecryptRequest(
|
|
[property: JsonPropertyName("algorithmId")] string? AlgorithmId,
|
|
[property: JsonPropertyName("keyBase64")] string KeyBase64,
|
|
[property: JsonPropertyName("ciphertextBase64")] string CiphertextBase64);
|
|
public sealed record DecryptResponse(
|
|
[property: JsonPropertyName("algorithmId")] string AlgorithmId,
|
|
[property: JsonPropertyName("payloadBase64")] string PayloadBase64);
|
|
public sealed record SignRequest([property: JsonPropertyName("keyId")] string KeyId,
|
|
[property: JsonPropertyName("algorithmId")] string AlgorithmId,
|
|
[property: JsonPropertyName("payloadBase64")] string PayloadBase64);
|
|
public sealed record SignResponse([property: JsonPropertyName("signature")] string Signature);
|
|
public sealed record VerifyRequest([property: JsonPropertyName("keyId")] string KeyId,
|
|
[property: JsonPropertyName("algorithmId")] string AlgorithmId,
|
|
[property: JsonPropertyName("payloadBase64")] string PayloadBase64,
|
|
[property: JsonPropertyName("signature")] string Signature);
|
|
public sealed record VerifyResponse([property: JsonPropertyName("valid")] bool Valid);
|
|
|
|
// Expose Program class for WebApplicationFactory in tests
|
|
public partial class Program;
|