This commit is contained in:
StellaOps Bot
2025-12-09 00:20:52 +02:00
parent 3d01bf9edc
commit bc0762e97d
261 changed files with 14033 additions and 4427 deletions

View File

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

View File

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

View File

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

View File

@@ -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" };
}

View File

@@ -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>