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
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:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user