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(_ => { 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(); 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 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;