up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Findings Ledger CI / build-test (push) Has been cancelled
Findings Ledger CI / migration-validation (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
cryptopro-linux-csp / build-and-test (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
sm-remote-ci / build-and-test (push) Has been cancelled
Findings Ledger CI / generate-manifest (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-09 09:38:09 +02:00
parent bc0762e97d
commit 108d1c64b3
193 changed files with 7265 additions and 13029 deletions

View File

@@ -16,6 +16,7 @@
<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.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />

View File

@@ -1,26 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Wine CSP HTTP Provider - remote GOST operations via Wine CSP service -->
<AssemblyName>StellaOps.Cryptography.Plugin.WineCsp</AssemblyName>
<RootNamespace>StellaOps.Cryptography.Plugin.WineCsp</RootNamespace>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0" />
<PackageReference Include="System.Text.Json" Version="10.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.15.0" />
</ItemGroup>
</Project>

View File

@@ -1,90 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;
namespace StellaOps.Cryptography.Plugin.WineCsp;
/// <summary>
/// Extension methods for registering the Wine CSP HTTP provider.
/// </summary>
public static class WineCspCryptoServiceCollectionExtensions
{
/// <summary>
/// Registers the Wine CSP HTTP provider for GOST operations via Wine-hosted CryptoPro CSP.
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="configureOptions">Optional options configuration.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddWineCspProvider(
this IServiceCollection services,
Action<WineCspProviderOptions>? configureOptions = null)
{
// Configure options
if (configureOptions != null)
{
services.Configure(configureOptions);
}
// Register HTTP client with retry policy
services.AddHttpClient<WineCspHttpClient>((sp, client) =>
{
var options = sp.GetService<IOptions<WineCspProviderOptions>>()?.Value
?? new WineCspProviderOptions();
client.BaseAddress = new Uri(options.ServiceUrl);
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
})
.AddPolicyHandler((sp, _) =>
{
var options = sp.GetService<IOptions<WineCspProviderOptions>>()?.Value
?? new WineCspProviderOptions();
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
options.MaxRetries,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt - 1)));
});
// Register provider
services.TryAddSingleton<WineCspHttpProvider>();
services.AddSingleton<ICryptoProvider>(sp => sp.GetRequiredService<WineCspHttpProvider>());
return services;
}
/// <summary>
/// Registers the Wine CSP HTTP provider with custom HTTP client configuration.
/// </summary>
/// <param name="services">Service collection.</param>
/// <param name="configureOptions">Options configuration.</param>
/// <param name="configureClient">HTTP client configuration.</param>
/// <returns>Service collection for chaining.</returns>
public static IServiceCollection AddWineCspProvider(
this IServiceCollection services,
Action<WineCspProviderOptions> configureOptions,
Action<HttpClient> configureClient)
{
services.Configure(configureOptions);
services.AddHttpClient<WineCspHttpClient>(configureClient)
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
MaxConnectionsPerServer = 10
});
services.TryAddSingleton<WineCspHttpProvider>();
services.AddSingleton<ICryptoProvider>(sp => sp.GetRequiredService<WineCspHttpProvider>());
return services;
}
}

View File

@@ -1,236 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Cryptography.Plugin.WineCsp;
/// <summary>
/// HTTP client for communicating with the Wine CSP service.
/// </summary>
public sealed class WineCspHttpClient : IDisposable
{
private readonly HttpClient httpClient;
private readonly ILogger<WineCspHttpClient>? logger;
private readonly JsonSerializerOptions jsonOptions;
public WineCspHttpClient(
HttpClient httpClient,
IOptions<WineCspProviderOptions>? optionsAccessor = null,
ILogger<WineCspHttpClient>? logger = null)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
this.logger = logger;
this.jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var options = optionsAccessor?.Value ?? new WineCspProviderOptions();
if (httpClient.BaseAddress == null)
{
httpClient.BaseAddress = new Uri(options.ServiceUrl);
}
}
/// <summary>
/// Gets the CSP status from the Wine service.
/// </summary>
public async Task<WineCspStatus> GetStatusAsync(CancellationToken ct = default)
{
logger?.LogDebug("Checking Wine CSP service status");
var response = await httpClient.GetAsync("/status", ct);
response.EnsureSuccessStatusCode();
var status = await response.Content.ReadFromJsonAsync<WineCspStatus>(jsonOptions, ct);
return status ?? throw new InvalidOperationException("Invalid status response from Wine CSP service");
}
/// <summary>
/// Lists available keys from the Wine CSP service.
/// </summary>
public async Task<IReadOnlyList<WineCspKeyInfo>> ListKeysAsync(CancellationToken ct = default)
{
logger?.LogDebug("Listing keys from Wine CSP service");
var response = await httpClient.GetAsync("/keys", ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<WineCspKeysResponse>(jsonOptions, ct);
return result?.Keys ?? Array.Empty<WineCspKeyInfo>();
}
/// <summary>
/// Signs data using the Wine CSP service.
/// </summary>
public async Task<WineCspSignResponse> SignAsync(
byte[] data,
string algorithm,
string? keyId,
CancellationToken ct = default)
{
logger?.LogDebug("Signing {ByteCount} bytes with algorithm {Algorithm}, keyId: {KeyId}",
data.Length, algorithm, keyId ?? "(default)");
var request = new WineCspSignRequest
{
DataBase64 = Convert.ToBase64String(data),
Algorithm = algorithm,
KeyId = keyId
};
var response = await httpClient.PostAsJsonAsync("/sign", request, jsonOptions, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<WineCspSignResponse>(jsonOptions, ct);
return result ?? throw new InvalidOperationException("Invalid sign response from Wine CSP service");
}
/// <summary>
/// Verifies a signature using the Wine CSP service.
/// </summary>
public async Task<bool> VerifyAsync(
byte[] data,
byte[] signature,
string algorithm,
string? keyId,
CancellationToken ct = default)
{
logger?.LogDebug("Verifying signature with algorithm {Algorithm}, keyId: {KeyId}",
algorithm, keyId ?? "(default)");
var request = new WineCspVerifyRequest
{
DataBase64 = Convert.ToBase64String(data),
SignatureBase64 = Convert.ToBase64String(signature),
Algorithm = algorithm,
KeyId = keyId
};
var response = await httpClient.PostAsJsonAsync("/verify", request, jsonOptions, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<WineCspVerifyResponse>(jsonOptions, ct);
return result?.IsValid ?? false;
}
/// <summary>
/// Computes a GOST hash using the Wine CSP service.
/// </summary>
public async Task<byte[]> HashAsync(
byte[] data,
string algorithm,
CancellationToken ct = default)
{
logger?.LogDebug("Hashing {ByteCount} bytes with algorithm {Algorithm}", data.Length, algorithm);
var request = new WineCspHashRequest
{
DataBase64 = Convert.ToBase64String(data),
Algorithm = algorithm
};
var response = await httpClient.PostAsJsonAsync("/hash", request, jsonOptions, ct);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<WineCspHashResponse>(jsonOptions, ct);
if (result == null || string.IsNullOrEmpty(result.HashBase64))
{
throw new InvalidOperationException("Invalid hash response from Wine CSP service");
}
return Convert.FromBase64String(result.HashBase64);
}
/// <summary>
/// Checks if the Wine CSP service is healthy.
/// </summary>
public async Task<bool> IsHealthyAsync(CancellationToken ct = default)
{
try
{
var response = await httpClient.GetAsync("/health", ct);
return response.IsSuccessStatusCode;
}
catch
{
return false;
}
}
public void Dispose()
{
// HttpClient is managed by HttpClientFactory, don't dispose
}
}
// Request/Response DTOs matching Wine CSP Service
#region DTOs
public sealed record WineCspSignRequest
{
public required string DataBase64 { get; init; }
public string? Algorithm { get; init; }
public string? KeyId { get; init; }
}
public sealed record WineCspSignResponse
{
public required string SignatureBase64 { get; init; }
public required string Algorithm { get; init; }
public string? KeyId { get; init; }
public DateTimeOffset Timestamp { get; init; }
public string? ProviderName { get; init; }
}
public sealed record WineCspVerifyRequest
{
public required string DataBase64 { get; init; }
public required string SignatureBase64 { get; init; }
public string? Algorithm { get; init; }
public string? KeyId { get; init; }
}
public sealed record WineCspVerifyResponse
{
public bool IsValid { get; init; }
}
public sealed record WineCspHashRequest
{
public required string DataBase64 { get; init; }
public string? Algorithm { get; init; }
}
public sealed record WineCspHashResponse
{
public required string HashBase64 { get; init; }
public required string HashHex { get; init; }
}
public sealed record WineCspStatus
{
public bool IsAvailable { get; init; }
public string? ProviderName { get; init; }
public string? ProviderVersion { get; init; }
public IReadOnlyList<string> SupportedAlgorithms { get; init; } = Array.Empty<string>();
public string? Error { get; init; }
}
public sealed record WineCspKeysResponse
{
public IReadOnlyList<WineCspKeyInfo> Keys { get; init; } = Array.Empty<WineCspKeyInfo>();
}
public sealed record WineCspKeyInfo
{
public required string KeyId { get; init; }
public required string Algorithm { get; init; }
public string? ContainerName { get; init; }
public bool IsAvailable { get; init; }
}
#endregion

View File

@@ -1,271 +0,0 @@
using System.Collections.Concurrent;
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.Cryptography.Plugin.WineCsp;
/// <summary>
/// ICryptoProvider implementation that delegates to the Wine CSP HTTP service.
/// Enables GOST cryptographic operations on Linux via Wine-hosted CryptoPro CSP.
/// </summary>
public sealed class WineCspHttpProvider : ICryptoProvider, ICryptoProviderDiagnostics, IDisposable
{
private readonly WineCspHttpClient client;
private readonly ILogger<WineCspHttpProvider>? logger;
private readonly ILoggerFactory? loggerFactory;
private readonly ConcurrentDictionary<string, WineCspKeyEntry> entries;
private readonly WineCspStatus? cachedStatus;
public WineCspHttpProvider(
WineCspHttpClient client,
IOptions<WineCspProviderOptions>? optionsAccessor = null,
ILogger<WineCspHttpProvider>? logger = null,
ILoggerFactory? loggerFactory = null)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.logger = logger;
this.loggerFactory = loggerFactory;
this.entries = new ConcurrentDictionary<string, WineCspKeyEntry>(StringComparer.OrdinalIgnoreCase);
var options = optionsAccessor?.Value ?? new WineCspProviderOptions();
// Load configured keys
foreach (var key in options.Keys)
{
var entry = new WineCspKeyEntry(
key.KeyId,
key.Algorithm,
key.RemoteKeyId ?? key.KeyId,
key.Description);
entries[key.KeyId] = entry;
}
// Try to probe service status
try
{
cachedStatus = client.GetStatusAsync().GetAwaiter().GetResult();
logger?.LogInformation(
"Wine CSP service available: {Available}, provider: {Provider}, algorithms: {Algorithms}",
cachedStatus.IsAvailable,
cachedStatus.ProviderName,
string.Join(", ", cachedStatus.SupportedAlgorithms));
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Wine CSP service probe failed, provider will be unavailable");
cachedStatus = new WineCspStatus
{
IsAvailable = false,
Error = $"Service probe failed: {ex.Message}"
};
}
}
public string Name => "ru.winecsp.http";
public bool Supports(CryptoCapability capability, string algorithmId)
{
if (cachedStatus?.IsAvailable != true)
{
return false;
}
return capability switch
{
CryptoCapability.Signing or CryptoCapability.Verification =>
IsGostSigningAlgorithm(algorithmId),
CryptoCapability.ContentHashing =>
IsGostHashAlgorithm(algorithmId),
_ => false
};
}
public IPasswordHasher GetPasswordHasher(string algorithmId)
=> throw new NotSupportedException("Wine CSP provider does not expose password hashing.");
public ICryptoHasher GetHasher(string algorithmId)
{
if (!IsGostHashAlgorithm(algorithmId))
{
throw new NotSupportedException($"Algorithm '{algorithmId}' is not a supported GOST hash algorithm.");
}
return new WineCspHttpHasher(client, algorithmId, loggerFactory?.CreateLogger<WineCspHttpHasher>());
}
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
ArgumentNullException.ThrowIfNull(keyReference);
if (!entries.TryGetValue(keyReference.KeyId, out var entry))
{
// Create ad-hoc entry for unregistered keys
entry = new WineCspKeyEntry(
keyReference.KeyId,
algorithmId,
keyReference.KeyId,
null);
}
else if (!string.Equals(entry.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.AlgorithmId}', not '{algorithmId}'.");
}
logger?.LogDebug("Creating Wine CSP signer for key {KeyId} ({Algorithm})", entry.KeyId, entry.AlgorithmId);
return new WineCspHttpSigner(client, entry, loggerFactory?.CreateLogger<WineCspHttpSigner>());
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
{
ArgumentNullException.ThrowIfNull(signingKey);
var entry = new WineCspKeyEntry(
signingKey.Reference.KeyId,
signingKey.AlgorithmId,
signingKey.Reference.KeyId,
null);
entries[signingKey.Reference.KeyId] = entry;
logger?.LogDebug("Registered Wine CSP key reference: {KeyId}", signingKey.Reference.KeyId);
}
public bool RemoveSigningKey(string keyId)
{
var removed = entries.TryRemove(keyId, out _);
if (removed)
{
logger?.LogDebug("Removed Wine CSP key reference: {KeyId}", keyId);
}
return removed;
}
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
{
// Wine CSP keys don't contain exportable key material
return Array.Empty<CryptoSigningKey>();
}
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys()
{
foreach (var entry in entries.Values)
{
yield return new CryptoProviderKeyDescriptor(
Name,
entry.KeyId,
entry.AlgorithmId,
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["remoteKeyId"] = entry.RemoteKeyId,
["description"] = entry.Description,
["serviceStatus"] = cachedStatus?.IsAvailable == true ? "available" : "unavailable"
});
}
}
/// <summary>
/// Gets the cached status of the Wine CSP service.
/// </summary>
public WineCspStatus? ServiceStatus => cachedStatus;
/// <summary>
/// Checks if the Wine CSP service is currently healthy.
/// </summary>
public async Task<bool> IsServiceHealthyAsync(CancellationToken ct = default)
{
return await client.IsHealthyAsync(ct);
}
/// <summary>
/// Refreshes the list of available keys from the Wine CSP service.
/// </summary>
public async Task<IReadOnlyList<WineCspKeyInfo>> RefreshKeysAsync(CancellationToken ct = default)
{
var keys = await client.ListKeysAsync(ct);
// Optionally register discovered keys
foreach (var key in keys.Where(k => k.IsAvailable))
{
if (!entries.ContainsKey(key.KeyId))
{
var entry = new WineCspKeyEntry(
key.KeyId,
key.Algorithm,
key.KeyId,
key.ContainerName);
entries[key.KeyId] = entry;
logger?.LogInformation("Discovered Wine CSP key: {KeyId} ({Algorithm})", key.KeyId, key.Algorithm);
}
}
return keys;
}
public void Dispose()
{
client.Dispose();
}
private static bool IsGostSigningAlgorithm(string algorithmId)
{
var normalized = algorithmId.ToUpperInvariant();
return normalized.Contains("GOST") &&
(normalized.Contains("3410") || normalized.Contains("34.10"));
}
private static bool IsGostHashAlgorithm(string algorithmId)
{
var normalized = algorithmId.ToUpperInvariant();
return normalized.Contains("GOST") &&
(normalized.Contains("3411") || normalized.Contains("34.11"));
}
}
/// <summary>
/// ICryptoHasher implementation that delegates to the Wine CSP HTTP service.
/// </summary>
internal sealed class WineCspHttpHasher : ICryptoHasher
{
private readonly WineCspHttpClient client;
private readonly ILogger<WineCspHttpHasher>? logger;
public WineCspHttpHasher(WineCspHttpClient client, string algorithmId, ILogger<WineCspHttpHasher>? logger = null)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.AlgorithmId = algorithmId;
this.logger = logger;
}
public string AlgorithmId { get; }
public byte[] ComputeHash(ReadOnlySpan<byte> data)
{
logger?.LogDebug("Computing GOST hash via Wine CSP service, {ByteCount} bytes", data.Length);
var result = client.HashAsync(
data.ToArray(),
MapAlgorithmToWineCsp(AlgorithmId),
CancellationToken.None).GetAwaiter().GetResult();
return result;
}
public string ComputeHashHex(ReadOnlySpan<byte> data)
{
var hash = ComputeHash(data);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string MapAlgorithmToWineCsp(string algorithmId)
{
return algorithmId.ToUpperInvariant() switch
{
"GOST-R-34.11-2012-256" or "GOSTR3411-2012-256" => "GOST12-256",
"GOST-R-34.11-2012-512" or "GOSTR3411-2012-512" => "GOST12-512",
_ => algorithmId
};
}
}

View File

@@ -1,122 +0,0 @@
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
namespace StellaOps.Cryptography.Plugin.WineCsp;
/// <summary>
/// ICryptoSigner implementation that delegates to the Wine CSP HTTP service.
/// </summary>
internal sealed class WineCspHttpSigner : ICryptoSigner
{
private readonly WineCspHttpClient client;
private readonly WineCspKeyEntry entry;
private readonly ILogger<WineCspHttpSigner>? logger;
public WineCspHttpSigner(
WineCspHttpClient client,
WineCspKeyEntry entry,
ILogger<WineCspHttpSigner>? logger = null)
{
this.client = client ?? throw new ArgumentNullException(nameof(client));
this.entry = entry ?? throw new ArgumentNullException(nameof(entry));
this.logger = logger;
}
public string KeyId => entry.KeyId;
public string AlgorithmId => entry.AlgorithmId;
public async ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
logger?.LogDebug("Signing {ByteCount} bytes via Wine CSP service, key: {KeyId}",
data.Length, entry.KeyId);
var response = await client.SignAsync(
data.ToArray(),
MapAlgorithmToWineCsp(entry.AlgorithmId),
entry.RemoteKeyId,
cancellationToken);
var signature = Convert.FromBase64String(response.SignatureBase64);
logger?.LogDebug("Signature received: {SignatureBytes} bytes from provider {Provider}",
signature.Length, response.ProviderName);
return signature;
}
catch (HttpRequestException ex)
{
logger?.LogError(ex, "Wine CSP service communication failed during signing");
throw new CryptographicException("Wine CSP service unavailable for signing", ex);
}
}
public async ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
logger?.LogDebug("Verifying signature via Wine CSP service, key: {KeyId}", entry.KeyId);
return await client.VerifyAsync(
data.ToArray(),
signature.ToArray(),
MapAlgorithmToWineCsp(entry.AlgorithmId),
entry.RemoteKeyId,
cancellationToken);
}
catch (HttpRequestException ex)
{
logger?.LogError(ex, "Wine CSP service communication failed during verification");
throw new CryptographicException("Wine CSP service unavailable for verification", ex);
}
}
public JsonWebKey ExportPublicJsonWebKey()
{
// Generate a JWK stub for the GOST key
// Full public key export would require additional certificate data from the service
var jwk = new JsonWebKey
{
Kid = KeyId,
Alg = AlgorithmId,
Kty = "EC",
Crv = entry.AlgorithmId.Contains("512", StringComparison.OrdinalIgnoreCase)
? "GOST3410-2012-512"
: "GOST3410-2012-256",
Use = JsonWebKeyUseNames.Sig
};
jwk.KeyOps.Add("sign");
jwk.KeyOps.Add("verify");
return jwk;
}
private static string MapAlgorithmToWineCsp(string algorithmId)
{
return algorithmId.ToUpperInvariant() switch
{
"GOST-R-34.10-2012-256" or "GOSTR3410-2012-256" => "GOST12-256",
"GOST-R-34.10-2012-512" or "GOSTR3410-2012-512" => "GOST12-512",
"GOST-R-34.11-2012-256" => "GOST12-256",
"GOST-R-34.11-2012-512" => "GOST12-512",
_ => algorithmId // Pass through if already in Wine CSP format
};
}
}
/// <summary>
/// Internal representation of a key accessible via Wine CSP service.
/// </summary>
internal sealed record WineCspKeyEntry(
string KeyId,
string AlgorithmId,
string? RemoteKeyId,
string? Description);

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Plugin.WineCsp;
/// <summary>
/// Options for configuring the WineCSP provider shim.
/// </summary>
public sealed record WineCspProviderOptions
{
/// <summary>
/// Optional base address for a WineCSP sidecar (HTTP) endpoint.
/// </summary>
public string BaseAddress { get; init; } = string.Empty;
/// <summary>
/// Optional request timeout when calling a WineCSP sidecar.
/// </summary>
public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(30);
/// <summary>
/// Provider identifier injected into the registry.
/// </summary>
public string ProviderName { get; init; } = "ru.winecsp.http";
}
/// <summary>
/// Minimal shim provider to keep registry wiring stable when WineCSP binaries are absent.
/// Delegates to the default crypto provider so callers receive deterministic behaviour.
/// </summary>
public sealed class WineCspProvider : ICryptoProvider
{
public const string DefaultProviderName = "ru.winecsp.http";
private readonly DefaultCryptoProvider _fallback = new();
private readonly WineCspProviderOptions _options;
private readonly ILogger<WineCspProvider>? _logger;
public WineCspProvider(IOptions<WineCspProviderOptions>? options = null, ILogger<WineCspProvider>? logger = null)
{
_options = options?.Value ?? new WineCspProviderOptions();
_logger = logger;
}
public string Name => _options.ProviderName;
public bool Supports(CryptoCapability capability, string algorithmId)
=> _fallback.Supports(capability, algorithmId);
public IPasswordHasher GetPasswordHasher(string algorithmId)
=> _fallback.GetPasswordHasher(algorithmId);
public ICryptoHasher GetHasher(string algorithmId)
=> _fallback.GetHasher(algorithmId);
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
LogIfInvoked();
return _fallback.GetSigner(algorithmId, keyReference);
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
{
LogIfInvoked();
_fallback.UpsertSigningKey(signingKey);
}
public bool RemoveSigningKey(string keyId)
{
LogIfInvoked();
return _fallback.RemoveSigningKey(keyId);
}
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
=> _fallback.GetSigningKeys();
private void LogIfInvoked()
{
_logger?.LogWarning("WineCSP provider invoked using fallback implementation; WineCSP sidecar not present (BaseAddress: {BaseAddress}).", _options.BaseAddress);
}
}
/// <summary>
/// Registration helpers for the WineCSP provider shim.
/// </summary>
public static class WineCspServiceCollectionExtensions
{
public static IServiceCollection AddWineCspProvider(
this IServiceCollection services,
Action<WineCspProviderOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(services);
services.AddOptions<WineCspProviderOptions>();
if (configure is not null)
{
services.Configure(configure);
}
services.TryAddSingleton<WineCspProvider>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<ICryptoProvider, WineCspProvider>());
return services;
}
}

View File

@@ -1,65 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace StellaOps.Cryptography.Plugin.WineCsp;
/// <summary>
/// Configuration options for the Wine CSP HTTP provider.
/// </summary>
public sealed class WineCspProviderOptions
{
/// <summary>
/// Base URL for the Wine CSP service (default: http://localhost:5099).
/// </summary>
[Required]
public string ServiceUrl { get; set; } = "http://localhost:5099";
/// <summary>
/// HTTP request timeout in seconds (default: 30).
/// </summary>
public int TimeoutSeconds { get; set; } = 30;
/// <summary>
/// Whether to enable HTTP connection pooling (default: true).
/// </summary>
public bool EnableConnectionPooling { get; set; } = true;
/// <summary>
/// Maximum number of retries for transient failures (default: 2).
/// </summary>
public int MaxRetries { get; set; } = 2;
/// <summary>
/// Pre-configured key references for signing.
/// </summary>
public List<WineCspKeyOptions> Keys { get; set; } = new();
}
/// <summary>
/// Configuration for a key accessible via the Wine CSP service.
/// </summary>
public sealed class WineCspKeyOptions
{
/// <summary>
/// Unique identifier for the key (used as reference in ICryptoSigner).
/// </summary>
[Required]
public required string KeyId { get; set; }
/// <summary>
/// Algorithm identifier (e.g., GOST-R-34.10-2012-256).
/// </summary>
[Required]
public required string Algorithm { get; set; }
/// <summary>
/// Remote key ID on the Wine CSP service (certificate thumbprint or container name).
/// If null, uses KeyId.
/// </summary>
public string? RemoteKeyId { get; set; }
/// <summary>
/// Description of the key for diagnostics.
/// </summary>
public string? Description { get; set; }
}