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

View File

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

View File

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

View File

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

View File

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

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>

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

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using MongoDB.Bson;
namespace StellaOps.Provenance.Mongo;

View File

@@ -1,5 +1,3 @@
using MongoDB.Bson;
namespace StellaOps.Provenance.Mongo;
public static class ProvenanceMongoExtensions

View File

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

View File

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

View File

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