up
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.SmRemote;
|
||||
|
||||
public sealed class SmRemoteHttpClient
|
||||
{
|
||||
private readonly HttpClient client;
|
||||
|
||||
public SmRemoteHttpClient(HttpClient client)
|
||||
{
|
||||
this.client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
}
|
||||
|
||||
public async Task<SmRemoteStatus> GetStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await client.GetAsync("/status", cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var status = await response.Content.ReadFromJsonAsync<SmRemoteStatus>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return status ?? new SmRemoteStatus { IsAvailable = false, Error = "empty response" };
|
||||
}
|
||||
|
||||
public async Task<string> SignAsync(string keyId, string algorithmId, byte[] pae, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new SmRemoteSignRequest
|
||||
{
|
||||
KeyId = keyId,
|
||||
AlgorithmId = algorithmId,
|
||||
PayloadBase64 = Convert.ToBase64String(pae)
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/sign", request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var envelope = await response.Content.ReadFromJsonAsync<SmRemoteSignResponse>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (envelope is null || string.IsNullOrWhiteSpace(envelope.Signature))
|
||||
{
|
||||
throw new InvalidOperationException("SM remote sign response was empty.");
|
||||
}
|
||||
|
||||
return envelope.Signature;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyAsync(string keyId, string algorithmId, byte[] pae, string signatureBase64, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var request = new SmRemoteVerifyRequest
|
||||
{
|
||||
KeyId = keyId,
|
||||
AlgorithmId = algorithmId,
|
||||
PayloadBase64 = Convert.ToBase64String(pae),
|
||||
Signature = signatureBase64
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/verify", request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<SmRemoteVerifyResponse>(cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return result?.Valid == true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SmRemoteSignRequest
|
||||
{
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
public string AlgorithmId { get; set; } = string.Empty;
|
||||
public string PayloadBase64 { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SmRemoteSignResponse
|
||||
{
|
||||
public string Signature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SmRemoteVerifyRequest
|
||||
{
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
public string AlgorithmId { get; set; } = string.Empty;
|
||||
public string PayloadBase64 { get; set; } = string.Empty;
|
||||
public string Signature { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class SmRemoteVerifyResponse
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SmRemoteStatus
|
||||
{
|
||||
public bool IsAvailable { get; set; }
|
||||
public string? ProviderName { get; set; }
|
||||
public string[] SupportedAlgorithms { get; set; } = Array.Empty<string>();
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.SmRemote;
|
||||
|
||||
/// <summary>
|
||||
/// SM2 provider delegating to a remote SM microservice (HTTP).
|
||||
/// Designed to be swapped with hardware-backed service when available.
|
||||
/// </summary>
|
||||
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<SmRemoteHttpProvider>? logger;
|
||||
private readonly ConcurrentDictionary<string, SmKeyEntry> entries = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SmRemoteStatus status;
|
||||
|
||||
public SmRemoteHttpProvider(
|
||||
SmRemoteHttpClient client,
|
||||
IOptions<SmRemoteProviderOptions>? optionsAccessor = null,
|
||||
ILogger<SmRemoteHttpProvider>? 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<CryptoSigningKey> GetSigningKeys() => Array.Empty<CryptoSigningKey>();
|
||||
|
||||
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys() =>
|
||||
entries.Values.Select(e => new CryptoProviderKeyDescriptor(Name, e.KeyId, SignatureAlgorithms.Sm2,
|
||||
new Dictionary<string, string?>(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);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.SmRemote;
|
||||
|
||||
public sealed class SmRemoteProviderOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Base address of the SM remote microservice.
|
||||
/// </summary>
|
||||
public string BaseAddress { get; set; } = "http://localhost:56080";
|
||||
|
||||
/// <summary>
|
||||
/// Keys to pre-register with the provider.
|
||||
/// </summary>
|
||||
public List<SmRemoteKeyOptions> Keys { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Skip service probe (useful for tests).
|
||||
/// </summary>
|
||||
public bool SkipProbe { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SmRemoteKeyOptions
|
||||
{
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
public string? RemoteKeyId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.SmRemote;
|
||||
|
||||
internal sealed class SmRemoteSigner : ICryptoSigner
|
||||
{
|
||||
private readonly SmRemoteHttpClient client;
|
||||
private readonly string remoteKeyId;
|
||||
|
||||
public SmRemoteSigner(SmRemoteHttpClient client, string remoteKeyId, string algorithmId)
|
||||
{
|
||||
this.client = client ?? throw new ArgumentNullException(nameof(client));
|
||||
this.remoteKeyId = remoteKeyId ?? throw new ArgumentNullException(nameof(remoteKeyId));
|
||||
AlgorithmId = algorithmId ?? throw new ArgumentNullException(nameof(algorithmId));
|
||||
}
|
||||
|
||||
public string KeyId => remoteKeyId;
|
||||
public string AlgorithmId { get; }
|
||||
|
||||
public async ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var signatureBase64 = await client.SignAsync(remoteKeyId, AlgorithmId, data.ToArray(), cancellationToken).ConfigureAwait(false);
|
||||
return Convert.FromBase64String(signatureBase64);
|
||||
}
|
||||
|
||||
public async ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sigBase64 = Convert.ToBase64String(signature.ToArray());
|
||||
return await client.VerifyAsync(remoteKeyId, AlgorithmId, data.ToArray(), sigBase64, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Microsoft.IdentityModel.Tokens.JsonWebKey ExportPublicJsonWebKey()
|
||||
=> new() { Kid = remoteKeyId, Alg = AlgorithmId, Kty = "EC" };
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user