using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Cryptography; namespace StellaOps.Cryptography.Plugin.SmRemote; /// /// SM2 provider delegating to a remote SM microservice (HTTP). /// Designed to be swapped with hardware-backed service when available. /// public sealed class SmRemoteHttpProvider : ICryptoProvider, ICryptoProviderDiagnostics { private const string ProviderNameConst = "cn.sm.remote.http"; private const string GateEnv = "SM_REMOTE_ALLOWED"; private readonly SmRemoteHttpClient client; private readonly ILogger? logger; private readonly ConcurrentDictionary entries = new(StringComparer.OrdinalIgnoreCase); private readonly SmRemoteStatus status; public SmRemoteHttpProvider( SmRemoteHttpClient client, IOptions? optionsAccessor = null, ILogger? logger = null) { this.client = client ?? throw new ArgumentNullException(nameof(client)); this.logger = logger; var options = optionsAccessor?.Value ?? new SmRemoteProviderOptions(); status = options.SkipProbe ? new SmRemoteStatus { IsAvailable = true, ProviderName = ProviderNameConst, SupportedAlgorithms = new[] { SignatureAlgorithms.Sm2 } } : ProbeStatus(); foreach (var key in options.Keys) { entries[key.KeyId] = new SmKeyEntry(key.KeyId, key.RemoteKeyId ?? key.KeyId); } } public string Name => ProviderNameConst; public bool Supports(CryptoCapability capability, string algorithmId) { if (!GateEnabled() || !status.IsAvailable) { return false; } return capability is CryptoCapability.Signing or CryptoCapability.Verification && string.Equals(algorithmId, SignatureAlgorithms.Sm2, StringComparison.OrdinalIgnoreCase); } public IPasswordHasher GetPasswordHasher(string algorithmId) => throw new NotSupportedException("SM remote provider does not expose password hashing."); public ICryptoHasher GetHasher(string algorithmId) => throw new NotSupportedException("SM remote provider does not expose hashing."); public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference) { if (!Supports(CryptoCapability.Signing, algorithmId)) { throw new InvalidOperationException($"Algorithm '{algorithmId}' not supported by '{Name}'."); } var entry = entries.GetOrAdd(keyReference.KeyId, id => new SmKeyEntry(id, id)); return new SmRemoteSigner(client, entry.RemoteKeyId, algorithmId); } public void UpsertSigningKey(CryptoSigningKey signingKey) { if (!Supports(CryptoCapability.Signing, signingKey.AlgorithmId)) { throw new InvalidOperationException($"Algorithm '{signingKey.AlgorithmId}' not supported by '{Name}'."); } entries[signingKey.Reference.KeyId] = new SmKeyEntry(signingKey.Reference.KeyId, signingKey.Reference.KeyId); } public bool RemoveSigningKey(string keyId) => entries.TryRemove(keyId, out _); public IReadOnlyCollection GetSigningKeys() => Array.Empty(); public IEnumerable DescribeKeys() => entries.Values.Select(e => new CryptoProviderKeyDescriptor(Name, e.KeyId, SignatureAlgorithms.Sm2, new Dictionary(StringComparer.OrdinalIgnoreCase) { ["provider"] = Name, ["remoteKeyId"] = e.RemoteKeyId, ["simulation"] = "remote-soft" })); private SmRemoteStatus ProbeStatus() { try { var probe = client.GetStatusAsync().GetAwaiter().GetResult(); return probe; } catch (Exception ex) { logger?.LogWarning(ex, "SM remote service probe failed"); return new SmRemoteStatus { IsAvailable = false, Error = ex.Message }; } } private static bool GateEnabled() { var value = Environment.GetEnvironmentVariable(GateEnv); return string.IsNullOrEmpty(value) || string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase); } private sealed record SmKeyEntry(string KeyId, string RemoteKeyId); }