up
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Validates and normalises crypto provider registry options for RU/GOST baselines.
|
||||
/// </summary>
|
||||
public static class CryptoProviderRegistryValidator
|
||||
{
|
||||
private static readonly StringComparer OrdinalIgnoreCase = StringComparer.OrdinalIgnoreCase;
|
||||
|
||||
public static void EnforceRuLinuxDefaults(CryptoProviderRegistryOptions options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var enableOpenSsl = GetEnvFlag("STELLAOPS_CRYPTO_ENABLE_RU_OPENSSL", defaultValue: OperatingSystem.IsLinux());
|
||||
var enablePkcs11 = GetEnvFlag("STELLAOPS_CRYPTO_ENABLE_RU_PKCS11", defaultValue: true);
|
||||
var enableWineCsp = GetEnvFlag("STELLAOPS_CRYPTO_ENABLE_RU_WINECSP", defaultValue: false);
|
||||
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
var enableCryptoPro = GetEnvFlag("STELLAOPS_CRYPTO_ENABLE_RU_CSP", defaultValue: OperatingSystem.IsWindows());
|
||||
#endif
|
||||
|
||||
options.ActiveProfile = string.IsNullOrWhiteSpace(options.ActiveProfile)
|
||||
? "ru-offline"
|
||||
: options.ActiveProfile;
|
||||
|
||||
EnsureBaselineProfiles(options);
|
||||
EnsureDefaultPreferred(options.PreferredProviders, enableOpenSsl, enablePkcs11, enableWineCsp
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
, enableCryptoPro
|
||||
#endif
|
||||
);
|
||||
|
||||
if (options.Profiles.TryGetValue(options.ActiveProfile, out var profile))
|
||||
{
|
||||
EnsureDefaultPreferred(profile.PreferredProviders, enableOpenSsl, enablePkcs11, enableWineCsp
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
, enableCryptoPro
|
||||
#endif
|
||||
);
|
||||
}
|
||||
|
||||
var resolved = options.ResolvePreferredProviders();
|
||||
if (resolved.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Crypto provider registry cannot be empty. Configure at least one provider for RU deployments.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() && enableOpenSsl &&
|
||||
!resolved.Contains("ru.openssl.gost", OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException("Linux RU baseline requires provider 'ru.openssl.gost' (set STELLAOPS_CRYPTO_ENABLE_RU_OPENSSL=0 to override explicitly).");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() && !enableOpenSsl && !enablePkcs11)
|
||||
{
|
||||
throw new InvalidOperationException("RU Linux baseline is misconfigured: both ru.openssl.gost and ru.pkcs11 are disabled via environment. Enable at least one provider.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool GetEnvFlag(string name, bool defaultValue)
|
||||
{
|
||||
var raw = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return raw.Equals("1", StringComparison.OrdinalIgnoreCase) ||
|
||||
raw.Equals("true", StringComparison.OrdinalIgnoreCase) ||
|
||||
raw.Equals("yes", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void EnsureBaselineProfiles(CryptoProviderRegistryOptions options)
|
||||
{
|
||||
if (!options.PreferredProviders.Any())
|
||||
{
|
||||
options.PreferredProviders.Add("default");
|
||||
}
|
||||
|
||||
if (!options.Profiles.TryGetValue("ru-offline", out var ruOffline))
|
||||
{
|
||||
ruOffline = new CryptoProviderProfileOptions();
|
||||
options.Profiles["ru-offline"] = ruOffline;
|
||||
}
|
||||
|
||||
if (!options.Profiles.ContainsKey("ru-linux-soft"))
|
||||
{
|
||||
options.Profiles["ru-linux-soft"] = new CryptoProviderProfileOptions();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureDefaultPreferred(
|
||||
IList<string> providers,
|
||||
bool enableOpenSsl,
|
||||
bool enablePkcs11,
|
||||
bool enableWineCsp
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
, bool enableCryptoPro
|
||||
#endif
|
||||
)
|
||||
{
|
||||
InsertIfMissing(providers, "default");
|
||||
|
||||
if (enableOpenSsl)
|
||||
{
|
||||
InsertIfMissing(providers, "ru.openssl.gost");
|
||||
}
|
||||
|
||||
if (enablePkcs11)
|
||||
{
|
||||
InsertIfMissing(providers, "ru.pkcs11");
|
||||
}
|
||||
|
||||
if (enableWineCsp)
|
||||
{
|
||||
InsertIfMissing(providers, "ru.winecsp.http");
|
||||
}
|
||||
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
if (enableCryptoPro && OperatingSystem.IsWindows())
|
||||
{
|
||||
InsertIfMissing(providers, "ru.cryptopro.csp");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void InsertIfMissing(IList<string> providers, string name)
|
||||
{
|
||||
for (var i = 0; i < providers.Count; i++)
|
||||
{
|
||||
if (string.Equals(providers[i], name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
providers.Insert(0, name);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
#endif
|
||||
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
|
||||
using StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
using StellaOps.Cryptography.Plugin.SmRemote;
|
||||
using StellaOps.Cryptography.Plugin.SmSoft;
|
||||
using StellaOps.Cryptography.Plugin.PqSoft;
|
||||
using StellaOps.Cryptography.Plugin.WineCsp;
|
||||
@@ -69,7 +70,17 @@ public static class CryptoServiceCollectionExtensions
|
||||
|
||||
services.TryAddSingleton<ICryptoHash, DefaultCryptoHash>();
|
||||
services.TryAddSingleton<ICryptoHmac, DefaultCryptoHmac>();
|
||||
services.AddOptions<SmRemoteProviderOptions>();
|
||||
services.AddHttpClient<SmRemoteHttpClient>((sp, httpClient) =>
|
||||
{
|
||||
var opts = sp.GetService<IOptions<SmRemoteProviderOptions>>()?.Value;
|
||||
if (opts is not null && !string.IsNullOrWhiteSpace(opts.BaseAddress))
|
||||
{
|
||||
httpClient.BaseAddress = new Uri(opts.BaseAddress);
|
||||
}
|
||||
});
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, SmSoftCryptoProvider>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, StellaOps.Cryptography.Plugin.SmRemote.SmRemoteHttpProvider>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, PqSoftCryptoProvider>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, FipsSoftCryptoProvider>());
|
||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, EidasSoftCryptoProvider>());
|
||||
@@ -171,41 +182,8 @@ public static class CryptoServiceCollectionExtensions
|
||||
}
|
||||
#endif
|
||||
|
||||
services.PostConfigure<CryptoProviderRegistryOptions>(options =>
|
||||
{
|
||||
EnsurePreferred(options.PreferredProviders);
|
||||
foreach (var profile in options.Profiles.Values)
|
||||
{
|
||||
EnsurePreferred(profile.PreferredProviders);
|
||||
}
|
||||
});
|
||||
services.PostConfigure<CryptoProviderRegistryOptions>(CryptoProviderRegistryValidator.EnforceRuLinuxDefaults);
|
||||
|
||||
return services;
|
||||
|
||||
static void EnsurePreferred(IList<string> providers)
|
||||
{
|
||||
InsertIfMissing(providers, "ru.pkcs11");
|
||||
InsertIfMissing(providers, "ru.openssl.gost");
|
||||
InsertIfMissing(providers, "ru.winecsp.http");
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
InsertIfMissing(providers, "ru.cryptopro.csp");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void InsertIfMissing(IList<string> providers, string name)
|
||||
{
|
||||
for (var i = 0; i < providers.Count; i++)
|
||||
{
|
||||
if (string.Equals(providers[i], name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
providers.Insert(0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.SmSoft\StellaOps.Cryptography.Plugin.SmSoft.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.SmRemote\StellaOps.Cryptography.Plugin.SmRemote.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.PqSoft\StellaOps.Cryptography.Plugin.PqSoft.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.WineCsp\StellaOps.Cryptography.Plugin.WineCsp.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography.Plugin.SmRemote;
|
||||
using StellaOps.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.SmRemote.Tests;
|
||||
|
||||
public class SmRemoteHttpProviderTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Service_EndToEnd_SignsAndVerifies()
|
||||
{
|
||||
Environment.SetEnvironmentVariable("SM_SOFT_ALLOWED", "1");
|
||||
using var app = new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(_ => { });
|
||||
|
||||
var client = app.CreateClient();
|
||||
var status = await client.GetFromJsonAsync<SmStatusResponse>("/status");
|
||||
status.Should().NotBeNull();
|
||||
status!.Available.Should().BeTrue();
|
||||
|
||||
var payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("pae"));
|
||||
var signResp = await client.PostAsJsonAsync("/sign", new SignRequest("sm2-test", SignatureAlgorithms.Sm2, payload));
|
||||
signResp.EnsureSuccessStatusCode();
|
||||
var sign = await signResp.Content.ReadFromJsonAsync<SignResponse>();
|
||||
sign.Should().NotBeNull();
|
||||
|
||||
var verifyResp = await client.PostAsJsonAsync("/verify", new VerifyRequest("sm2-test", SignatureAlgorithms.Sm2, payload, sign!.Signature));
|
||||
verifyResp.EnsureSuccessStatusCode();
|
||||
var verify = await verifyResp.Content.ReadFromJsonAsync<VerifyResponse>();
|
||||
verify!.Valid.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SignsAndVerifiesViaHttp()
|
||||
{
|
||||
var handler = new StubHandler();
|
||||
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://localhost:8080") };
|
||||
|
||||
var client = new SmRemoteHttpClient(httpClient);
|
||||
var provider = new SmRemoteHttpProvider(
|
||||
client,
|
||||
Options.Create(new SmRemoteProviderOptions
|
||||
{
|
||||
SkipProbe = true,
|
||||
Keys = { new SmRemoteKeyOptions { KeyId = "sm2-1" } }
|
||||
}),
|
||||
NullLogger<SmRemoteHttpProvider>.Instance);
|
||||
|
||||
var signer = provider.GetSigner(SignatureAlgorithms.Sm2, new CryptoKeyReference("sm2-1", provider.Name));
|
||||
var data = System.Text.Encoding.UTF8.GetBytes("pae");
|
||||
|
||||
var signature = await signer.SignAsync(data);
|
||||
var ok = await signer.VerifyAsync(data, signature);
|
||||
|
||||
ok.Should().BeTrue();
|
||||
handler.SignCalled.Should().BeTrue();
|
||||
handler.VerifyCalled.Should().BeTrue();
|
||||
}
|
||||
|
||||
private sealed class StubHandler : HttpMessageHandler
|
||||
{
|
||||
public bool SignCalled { get; private set; }
|
||||
public bool VerifyCalled { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.RequestUri?.AbsolutePath == "/status")
|
||||
{
|
||||
var status = new SmRemoteStatus { IsAvailable = true, ProviderName = "stub", SupportedAlgorithms = new[] { SignatureAlgorithms.Sm2 } };
|
||||
return Task.FromResult(Json(status));
|
||||
}
|
||||
|
||||
if (request.RequestUri?.AbsolutePath == "/sign")
|
||||
{
|
||||
SignCalled = true;
|
||||
var signature = Convert.ToBase64String(new byte[] { 1, 2, 3 });
|
||||
return Task.FromResult(Json(new SmRemoteSignResponse { Signature = signature }));
|
||||
}
|
||||
|
||||
if (request.RequestUri?.AbsolutePath == "/verify")
|
||||
{
|
||||
VerifyCalled = true;
|
||||
return Task.FromResult(Json(new SmRemoteVerifyResponse { Valid = true }));
|
||||
}
|
||||
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
|
||||
}
|
||||
|
||||
private static HttpResponseMessage Json<T>(T payload)
|
||||
{
|
||||
var resp = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
resp.Content = new StringContent(JsonSerializer.Serialize(payload));
|
||||
resp.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.SmRemote\StellaOps.Cryptography.Plugin.SmRemote.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\..\\SmRemote\\StellaOps.SmRemote.Service\\StellaOps.SmRemote.Service.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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>
|
||||
173
src/__Libraries/StellaOps.Provenance.Mongo/BsonStubs.cs
Normal file
173
src/__Libraries/StellaOps.Provenance.Mongo/BsonStubs.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Provenance.Mongo;
|
||||
|
||||
// Minimal stubs to avoid MongoDB.Bson dependency while keeping callers unchanged.
|
||||
public abstract class BsonValue
|
||||
{
|
||||
public virtual object? Value => null;
|
||||
|
||||
public virtual BsonDocument AsBsonDocument =>
|
||||
this as BsonDocument ?? throw new InvalidCastException("Value is not a BsonDocument.");
|
||||
|
||||
public virtual BsonArray AsBsonArray =>
|
||||
this as BsonArray ?? throw new InvalidCastException("Value is not a BsonArray.");
|
||||
|
||||
public virtual string AsString => Value?.ToString() ?? string.Empty;
|
||||
public virtual int AsInt32 => Convert.ToInt32(Value);
|
||||
public virtual long AsInt64 => Convert.ToInt64(Value);
|
||||
public virtual double AsDouble => Convert.ToDouble(Value);
|
||||
public virtual bool AsBoolean => Convert.ToBoolean(Value);
|
||||
|
||||
internal static BsonValue Wrap(object? value) =>
|
||||
value switch
|
||||
{
|
||||
null => BsonNull.Value,
|
||||
BsonValue bson => bson,
|
||||
string s => new BsonString(s),
|
||||
bool b => new BsonBoolean(b),
|
||||
int i => new BsonInt32(i),
|
||||
long l => new BsonInt64(l),
|
||||
double d => new BsonDouble(d),
|
||||
IEnumerable<BsonValue> bsonEnumerable => new BsonArray(bsonEnumerable),
|
||||
IEnumerable enumerable => new BsonArray(enumerable.Cast<object?>()),
|
||||
_ => new BsonRaw(value)
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class BsonNull : BsonValue
|
||||
{
|
||||
public static readonly BsonNull ValueInstance = new();
|
||||
public new static BsonNull Value => ValueInstance;
|
||||
}
|
||||
|
||||
public sealed class BsonString : BsonValue
|
||||
{
|
||||
public BsonString(string value) => Value = value;
|
||||
public override object? Value { get; }
|
||||
public override string ToString() => Value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
public sealed class BsonBoolean : BsonValue
|
||||
{
|
||||
public BsonBoolean(bool value) => Value = value;
|
||||
public override object? Value { get; }
|
||||
}
|
||||
|
||||
public sealed class BsonInt32 : BsonValue
|
||||
{
|
||||
public BsonInt32(int value) => Value = value;
|
||||
public override object? Value { get; }
|
||||
}
|
||||
|
||||
public sealed class BsonInt64 : BsonValue
|
||||
{
|
||||
public BsonInt64(long value) => Value = value;
|
||||
public override object? Value { get; }
|
||||
}
|
||||
|
||||
public sealed class BsonDouble : BsonValue
|
||||
{
|
||||
public BsonDouble(double value) => Value = value;
|
||||
public override object? Value { get; }
|
||||
}
|
||||
|
||||
public sealed class BsonRaw : BsonValue
|
||||
{
|
||||
public BsonRaw(object value) => Value = value;
|
||||
public override object? Value { get; }
|
||||
}
|
||||
|
||||
public record struct BsonElement(string Name, BsonValue Value);
|
||||
|
||||
public class BsonDocument : BsonValue, IEnumerable<KeyValuePair<string, BsonValue>>
|
||||
{
|
||||
private readonly Dictionary<string, object?> _values = new(StringComparer.Ordinal);
|
||||
|
||||
public BsonDocument()
|
||||
{
|
||||
}
|
||||
|
||||
public BsonDocument(string key, object? value)
|
||||
{
|
||||
_values[key] = value;
|
||||
}
|
||||
|
||||
public BsonValue this[string key]
|
||||
{
|
||||
get => BsonValue.Wrap(_values[key]);
|
||||
set => _values[key] = value;
|
||||
}
|
||||
|
||||
public void Add(string key, object? value) => _values.Add(key, value);
|
||||
|
||||
public IEnumerable<BsonElement> Elements => _values.Select(kvp => new BsonElement(kvp.Key, BsonValue.Wrap(kvp.Value)));
|
||||
|
||||
public bool Contains(string key) => ContainsKey(key);
|
||||
|
||||
public bool ContainsKey(string key) => _values.ContainsKey(key);
|
||||
|
||||
public override object? Value => this;
|
||||
|
||||
public override BsonDocument AsBsonDocument => this;
|
||||
|
||||
public IEnumerator<KeyValuePair<string, BsonValue>> GetEnumerator() =>
|
||||
_values.Select(kvp => new KeyValuePair<string, BsonValue>(kvp.Key, BsonValue.Wrap(kvp.Value))).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
public class BsonArray : BsonValue, IEnumerable<BsonValue>
|
||||
{
|
||||
private readonly List<object?> _items = new();
|
||||
|
||||
public BsonArray()
|
||||
{
|
||||
}
|
||||
|
||||
public BsonArray(IEnumerable items)
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public BsonArray(IEnumerable<object?> items)
|
||||
: this()
|
||||
{
|
||||
foreach (var item in items)
|
||||
{
|
||||
Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public BsonValue this[int index] => BsonValue.Wrap(_items[index]);
|
||||
|
||||
public void Add(BsonDocument doc) => _items.Add(doc);
|
||||
public void Add(object? value) => _items.Add(value);
|
||||
|
||||
public int Count => _items.Count;
|
||||
|
||||
public override object? Value => this;
|
||||
|
||||
public override BsonArray AsBsonArray => this;
|
||||
|
||||
public IEnumerator<BsonValue> GetEnumerator() => _items.Select(BsonValue.Wrap).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
public static class BsonValueExtensions
|
||||
{
|
||||
public static BsonDocument AsBsonDocument(this object? value) => BsonValue.Wrap(value).AsBsonDocument;
|
||||
public static BsonArray AsBsonArray(this object? value) => BsonValue.Wrap(value).AsBsonArray;
|
||||
public static string AsString(this object? value) => BsonValue.Wrap(value).AsString;
|
||||
public static int AsInt32(this object? value) => BsonValue.Wrap(value).AsInt32;
|
||||
public static long AsInt64(this object? value) => BsonValue.Wrap(value).AsInt64;
|
||||
public static double AsDouble(this object? value) => BsonValue.Wrap(value).AsDouble;
|
||||
public static bool AsBoolean(this object? value) => BsonValue.Wrap(value).AsBoolean;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace StellaOps.Provenance.Mongo;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace StellaOps.Provenance.Mongo;
|
||||
|
||||
public static class ProvenanceMongoExtensions
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v10.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v10.0": {
|
||||
"StellaOps.Provenance.Mongo/1.0.0": {
|
||||
"dependencies": {
|
||||
"MongoDB.Driver": "3.5.0",
|
||||
"SharpCompress": "0.41.0"
|
||||
},
|
||||
"runtime": {
|
||||
"StellaOps.Provenance.Mongo.dll": {}
|
||||
}
|
||||
},
|
||||
"DnsClient/1.6.1": {
|
||||
"runtime": {
|
||||
"lib/net5.0/DnsClient.dll": {
|
||||
"assemblyVersion": "1.6.1.0",
|
||||
"fileVersion": "1.6.1.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions/2.0.0": {
|
||||
"runtime": {
|
||||
"lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll": {
|
||||
"assemblyVersion": "2.0.0.0",
|
||||
"fileVersion": "2.0.0.17205"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MongoDB.Bson/3.5.0": {
|
||||
"runtime": {
|
||||
"lib/net6.0/MongoDB.Bson.dll": {
|
||||
"assemblyVersion": "3.5.0.0",
|
||||
"fileVersion": "3.5.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MongoDB.Driver/3.5.0": {
|
||||
"dependencies": {
|
||||
"DnsClient": "1.6.1",
|
||||
"Microsoft.Extensions.Logging.Abstractions": "2.0.0",
|
||||
"MongoDB.Bson": "3.5.0",
|
||||
"SharpCompress": "0.41.0",
|
||||
"Snappier": "1.0.0",
|
||||
"ZstdSharp.Port": "0.8.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net6.0/MongoDB.Driver.dll": {
|
||||
"assemblyVersion": "3.5.0.0",
|
||||
"fileVersion": "3.5.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SharpCompress/0.41.0": {
|
||||
"dependencies": {
|
||||
"ZstdSharp.Port": "0.8.6"
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/SharpCompress.dll": {
|
||||
"assemblyVersion": "0.41.0.0",
|
||||
"fileVersion": "0.41.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Snappier/1.0.0": {
|
||||
"runtime": {
|
||||
"lib/net5.0/Snappier.dll": {
|
||||
"assemblyVersion": "1.0.0.0",
|
||||
"fileVersion": "1.0.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ZstdSharp.Port/0.8.6": {
|
||||
"runtime": {
|
||||
"lib/net9.0/ZstdSharp.dll": {
|
||||
"assemblyVersion": "0.8.6.0",
|
||||
"fileVersion": "0.8.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"StellaOps.Provenance.Mongo/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"DnsClient/1.6.1": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-4H/f2uYJOZ+YObZjpY9ABrKZI+JNw3uizp6oMzTXwDw6F+2qIPhpRl/1t68O/6e98+vqNiYGu+lswmwdYUy3gg==",
|
||||
"path": "dnsclient/1.6.1",
|
||||
"hashPath": "dnsclient.1.6.1.nupkg.sha512"
|
||||
},
|
||||
"Microsoft.Extensions.Logging.Abstractions/2.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-6ZCllUYGFukkymSTx3Yr0G/ajRxoNJp7/FqSxSB4fGISST54ifBhgu4Nc0ItGi3i6DqwuNd8SUyObmiC++AO2Q==",
|
||||
"path": "microsoft.extensions.logging.abstractions/2.0.0",
|
||||
"hashPath": "microsoft.extensions.logging.abstractions.2.0.0.nupkg.sha512"
|
||||
},
|
||||
"MongoDB.Bson/3.5.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-JGNK6BanLDEifgkvPLqVFCPus5EDCy416pxf1dxUBRSVd3D9+NB3AvMVX190eXlk5/UXuCxpsQv7jWfNKvppBQ==",
|
||||
"path": "mongodb.bson/3.5.0",
|
||||
"hashPath": "mongodb.bson.3.5.0.nupkg.sha512"
|
||||
},
|
||||
"MongoDB.Driver/3.5.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-ST90u7psyMkNNOWFgSkexsrB3kPn7Ynl2DlMFj2rJyYuc6SIxjmzu4ufy51yzM+cPVE1SvVcdb5UFobrRw6cMg==",
|
||||
"path": "mongodb.driver/3.5.0",
|
||||
"hashPath": "mongodb.driver.3.5.0.nupkg.sha512"
|
||||
},
|
||||
"SharpCompress/0.41.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-z04dBVdTIAFTRKi38f0LkajaKA++bR+M8kYCbasXePILD2H+qs7CkLpyiippB24CSbTrWIgpBKm6BenZqkUwvw==",
|
||||
"path": "sharpcompress/0.41.0",
|
||||
"hashPath": "sharpcompress.0.41.0.nupkg.sha512"
|
||||
},
|
||||
"Snappier/1.0.0": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-rFtK2KEI9hIe8gtx3a0YDXdHOpedIf9wYCEYtBEmtlyiWVX3XlCNV03JrmmAi/Cdfn7dxK+k0sjjcLv4fpHnqA==",
|
||||
"path": "snappier/1.0.0",
|
||||
"hashPath": "snappier.1.0.0.nupkg.sha512"
|
||||
},
|
||||
"ZstdSharp.Port/0.8.6": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-iP4jVLQoQmUjMU88g1WObiNr6YKZGvh4aOXn3yOJsHqZsflwRsxZPcIBvNXgjXO3vQKSLctXGLTpcBPLnWPS8A==",
|
||||
"path": "zstdsharp.port/0.8.6",
|
||||
"hashPath": "zstdsharp.port.0.8.6.nupkg.sha512"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,7 @@ public class CryptoProviderRegistryTests
|
||||
{
|
||||
private readonly Dictionary<string, FakeSigner> signers = new(StringComparer.Ordinal);
|
||||
private readonly HashSet<(CryptoCapability Capability, string Algorithm)> supported;
|
||||
private readonly Dictionary<string, FakeHasher> hashers = new(StringComparer.Ordinal);
|
||||
|
||||
public FakeCryptoProvider(string name)
|
||||
{
|
||||
@@ -91,6 +92,10 @@ public class CryptoProviderRegistryTests
|
||||
public FakeCryptoProvider WithSupport(CryptoCapability capability, string algorithm)
|
||||
{
|
||||
supported.Add((capability, algorithm));
|
||||
if (capability == CryptoCapability.ContentHashing && !hashers.ContainsKey(algorithm))
|
||||
{
|
||||
hashers[algorithm] = new FakeHasher(algorithm);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -108,6 +113,16 @@ public class CryptoProviderRegistryTests
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
public ICryptoHasher GetHasher(string algorithmId)
|
||||
{
|
||||
if (!hashers.TryGetValue(algorithmId, out var hasher))
|
||||
{
|
||||
throw new InvalidOperationException($"Hasher '{algorithmId}' not registered.");
|
||||
}
|
||||
|
||||
return hasher;
|
||||
}
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
if (!signers.TryGetValue(keyReference.KeyId, out var signer))
|
||||
@@ -169,4 +184,15 @@ public class CryptoProviderRegistryTests
|
||||
Use = JsonWebKeyUseNames.Sig
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class FakeHasher : ICryptoHasher
|
||||
{
|
||||
public FakeHasher(string algorithmId) => AlgorithmId = algorithmId;
|
||||
|
||||
public string AlgorithmId { get; }
|
||||
|
||||
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
|
||||
|
||||
public string ComputeHashHex(ReadOnlySpan<byte> data) => Convert.ToHexStringLower(ComputeHash(data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Crypto.Prng;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.Asn1.Pkcs;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.SmSoft;
|
||||
using Xunit;
|
||||
|
||||
Reference in New Issue
Block a user