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);
}