Add support for ГОСТ Р 34.10 digital signatures
- Implemented the GostKeyValue class for handling public key parameters in ГОСТ Р 34.10 digital signatures. - Created the GostSignedXml class to manage XML signatures using ГОСТ 34.10, including methods for computing and checking signatures. - Developed the GostSignedXmlImpl class to encapsulate the signature computation logic and public key retrieval. - Added specific key value classes for ГОСТ Р 34.10-2001, ГОСТ Р 34.10-2012/256, and ГОСТ Р 34.10-2012/512 to support different signature algorithms. - Ensured compatibility with existing XML signature standards while integrating ГОСТ cryptography.
This commit is contained in:
@@ -20,9 +20,12 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../Authority/StellaOps.Authority/StellaOps.Authority.Plugins.Abstractions/StellaOps.Authority.Plugins.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.CryptoPro\StellaOps.Cryptography.Plugin.CryptoPro.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.DependencyInjection\StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.CryptoPro\StellaOps.Cryptography.Plugin.CryptoPro.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
#endif
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -13,8 +15,9 @@ public sealed class StellaOpsCryptoOptions
|
||||
public CryptoProviderRegistryOptions Registry { get; } = new();
|
||||
|
||||
public Pkcs11GostProviderOptions Pkcs11 { get; } = new();
|
||||
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
public CryptoProGostProviderOptions CryptoPro { get; } = new();
|
||||
#endif
|
||||
|
||||
public string DefaultHashAlgorithm { get; set; } = HashAlgorithms.Sha256;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.DependencyInjection;
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
#endif
|
||||
|
||||
namespace StellaOps.Configuration;
|
||||
|
||||
@@ -30,11 +32,13 @@ public static class StellaOpsCryptoServiceCollectionExtensions
|
||||
CopyPkcs11Options(target, resolved.Pkcs11);
|
||||
});
|
||||
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
services.AddCryptoProGostProvider();
|
||||
services.Configure<CryptoProGostProviderOptions>(target =>
|
||||
{
|
||||
CopyCryptoProOptions(target, resolved.CryptoPro);
|
||||
});
|
||||
#endif
|
||||
|
||||
services.Configure<CryptoHashOptions>(hash =>
|
||||
{
|
||||
@@ -90,6 +94,7 @@ public static class StellaOpsCryptoServiceCollectionExtensions
|
||||
}
|
||||
}
|
||||
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
private static void CopyCryptoProOptions(CryptoProGostProviderOptions target, CryptoProGostProviderOptions source)
|
||||
{
|
||||
target.Keys.Clear();
|
||||
@@ -98,4 +103,5 @@ public static class StellaOpsCryptoServiceCollectionExtensions
|
||||
target.Keys.Add(key.Clone());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Cryptography;
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
#endif
|
||||
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
|
||||
using StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
namespace StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -72,12 +75,21 @@ public static class CryptoServiceCollectionExtensions
|
||||
var baseSection = configuration.GetSection("StellaOps:Crypto");
|
||||
services.Configure<StellaOpsCryptoOptions>(baseSection);
|
||||
services.Configure<CryptoProviderRegistryOptions>(baseSection.GetSection("Registry"));
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
services.Configure<CryptoProGostProviderOptions>(baseSection.GetSection("CryptoPro"));
|
||||
#endif
|
||||
services.Configure<Pkcs11GostProviderOptions>(baseSection.GetSection("Pkcs11"));
|
||||
services.Configure<OpenSslGostProviderOptions>(baseSection.GetSection("OpenSsl"));
|
||||
|
||||
services.AddStellaOpsCrypto(configureRegistry);
|
||||
services.AddCryptoProGostProvider();
|
||||
services.AddOpenSslGostProvider();
|
||||
services.AddPkcs11GostProvider();
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
services.AddCryptoProGostProvider();
|
||||
}
|
||||
#endif
|
||||
|
||||
services.PostConfigure<CryptoProviderRegistryOptions>(options =>
|
||||
{
|
||||
@@ -93,7 +105,13 @@ public static class CryptoServiceCollectionExtensions
|
||||
static void EnsurePreferred(IList<string> providers)
|
||||
{
|
||||
InsertIfMissing(providers, "ru.pkcs11");
|
||||
InsertIfMissing(providers, "ru.cryptopro.csp");
|
||||
InsertIfMissing(providers, "ru.openssl.gost");
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
InsertIfMissing(providers, "ru.cryptopro.csp");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static void InsertIfMissing(IList<string> providers, string name)
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsNotAsErrors>NU1701;NU1902;NU1903</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.Pkcs11Gost\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.OpenSslGost\StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
|
||||
<ProjectReference Include="..\StellaOps.Cryptography.Plugin.CryptoPro\StellaOps.Cryptography.Plugin.CryptoPro.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
using StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
#endif
|
||||
|
||||
namespace StellaOps.Cryptography.DependencyInjection;
|
||||
|
||||
@@ -7,7 +10,11 @@ public sealed class StellaOpsCryptoOptions
|
||||
{
|
||||
public CryptoProviderRegistryOptions Registry { get; set; } = new();
|
||||
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
public CryptoProGostProviderOptions CryptoPro { get; set; } = new();
|
||||
#endif
|
||||
|
||||
public Pkcs11GostProviderOptions Pkcs11 { get; set; } = new();
|
||||
|
||||
public OpenSslGostProviderOptions OpenSsl { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -66,5 +66,5 @@ internal static class CryptoProCertificateResolver
|
||||
}
|
||||
|
||||
private static string Normalize(string value)
|
||||
=> value.Replace(" ", string.Empty, StringComparison.Ordinal).ToUpperInvariant(CultureInfo.InvariantCulture);
|
||||
=> value.Replace(" ", string.Empty, StringComparison.Ordinal).ToUpperInvariant();
|
||||
}
|
||||
|
||||
@@ -17,9 +17,13 @@ public static class CryptoProCryptoServiceCollectionExtensions
|
||||
services.Configure(configure);
|
||||
}
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return services;
|
||||
}
|
||||
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<StellaOps.Cryptography.ICryptoProvider, CryptoProGostCryptoProvider>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Runtime.Versioning;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class CryptoProGostCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
|
||||
{
|
||||
private readonly ILogger<CryptoProGostCryptoProvider>? logger;
|
||||
@@ -26,7 +28,9 @@ public sealed class CryptoProGostCryptoProvider : ICryptoProvider, ICryptoProvid
|
||||
key.Algorithm,
|
||||
certificate,
|
||||
key.ProviderName,
|
||||
key.ContainerName);
|
||||
key.ContainerName,
|
||||
key.UseMachineKeyStore,
|
||||
key.SignatureFormat);
|
||||
|
||||
map[key.KeyId] = entry;
|
||||
}
|
||||
|
||||
@@ -9,13 +9,17 @@ internal sealed class CryptoProGostKeyEntry
|
||||
string algorithmId,
|
||||
X509Certificate2 certificate,
|
||||
string providerName,
|
||||
string? containerName)
|
||||
string? containerName,
|
||||
bool useMachineKeyStore,
|
||||
GostSignatureFormat signatureFormat)
|
||||
{
|
||||
KeyId = keyId;
|
||||
AlgorithmId = algorithmId;
|
||||
Certificate = certificate;
|
||||
ProviderName = providerName;
|
||||
ContainerName = containerName;
|
||||
UseMachineKeyStore = useMachineKeyStore;
|
||||
SignatureFormat = signatureFormat;
|
||||
}
|
||||
|
||||
public string KeyId { get; }
|
||||
@@ -28,5 +32,11 @@ internal sealed class CryptoProGostKeyEntry
|
||||
|
||||
public string? ContainerName { get; }
|
||||
|
||||
public bool UseMachineKeyStore { get; }
|
||||
|
||||
public GostSignatureFormat SignatureFormat { get; }
|
||||
|
||||
public bool Use256 => string.Equals(AlgorithmId, SignatureAlgorithms.GostR3410_2012_256, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public int CoordinateSize => Use256 ? 32 : 64;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,11 @@ public sealed class CryptoProGostKeyOptions
|
||||
|
||||
public string Algorithm { get; set; } = SignatureAlgorithms.GostR3410_2012_256;
|
||||
|
||||
/// <summary>
|
||||
/// Wire format emitted by the signer. Defaults to DER (ASN.1 sequence). Set to Raw for (s || r).
|
||||
/// </summary>
|
||||
public GostSignatureFormat SignatureFormat { get; set; } = GostSignatureFormat.Der;
|
||||
|
||||
/// <summary>
|
||||
/// Optional CryptoPro provider name (defaults to standard CSP).
|
||||
/// </summary>
|
||||
@@ -21,6 +26,11 @@ public sealed class CryptoProGostKeyOptions
|
||||
/// </summary>
|
||||
public string? ContainerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true when the container lives in the machine key store.
|
||||
/// </summary>
|
||||
public bool UseMachineKeyStore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Thumbprint of the certificate that owns the CryptoPro private key.
|
||||
/// </summary>
|
||||
@@ -45,6 +55,8 @@ public sealed class CryptoProGostKeyOptions
|
||||
CertificateThumbprint = CertificateThumbprint,
|
||||
SubjectName = SubjectName,
|
||||
CertificateStoreLocation = CertificateStoreLocation,
|
||||
CertificateStoreName = CertificateStoreName
|
||||
CertificateStoreName = CertificateStoreName,
|
||||
UseMachineKeyStore = UseMachineKeyStore,
|
||||
SignatureFormat = SignatureFormat
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
using System;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GostCryptography.Gost_3410;
|
||||
using GostCryptography.Base;
|
||||
using GostCryptography.Config;
|
||||
using GostCryptography.Gost_R3410;
|
||||
using GostCryptography.Reflection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal sealed class CryptoProGostSigner : ICryptoSigner
|
||||
{
|
||||
private readonly CryptoProGostKeyEntry entry;
|
||||
@@ -27,9 +35,11 @@ internal sealed class CryptoProGostSigner : ICryptoSigner
|
||||
? GostDigestUtilities.ComputeDigest(data.Span, use256: true)
|
||||
: GostDigestUtilities.ComputeDigest(data.Span, use256: false);
|
||||
|
||||
using var provider = CreateProvider();
|
||||
var signature = provider.SignHash(digest);
|
||||
return ValueTask.FromResult(signature);
|
||||
using var algorithm = CreateAlgorithm(forVerification: false);
|
||||
var formatter = new GostSignatureFormatter(algorithm);
|
||||
var signature = formatter.CreateSignature(digest);
|
||||
var normalized = NormalizeSignatureForOutput(signature);
|
||||
return ValueTask.FromResult(normalized);
|
||||
}
|
||||
|
||||
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
|
||||
@@ -40,8 +50,10 @@ internal sealed class CryptoProGostSigner : ICryptoSigner
|
||||
? GostDigestUtilities.ComputeDigest(data.Span, use256: true)
|
||||
: GostDigestUtilities.ComputeDigest(data.Span, use256: false);
|
||||
|
||||
using var provider = CreateProvider();
|
||||
var valid = provider.VerifyHash(digest, signature.ToArray());
|
||||
using var algorithm = CreateAlgorithm(forVerification: true);
|
||||
var deformatter = new GostSignatureDeformatter(algorithm);
|
||||
var derSignature = EnsureDerSignature(signature.Span);
|
||||
var valid = deformatter.VerifySignature(digest, derSignature);
|
||||
return ValueTask.FromResult(valid);
|
||||
}
|
||||
|
||||
@@ -63,13 +75,89 @@ internal sealed class CryptoProGostSigner : ICryptoSigner
|
||||
return jwk;
|
||||
}
|
||||
|
||||
private Gost3410CryptoServiceProvider CreateProvider()
|
||||
private GostAsymmetricAlgorithm CreateAlgorithm(bool forVerification)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry.ContainerName))
|
||||
if (!forVerification && !string.IsNullOrWhiteSpace(entry.ContainerName))
|
||||
{
|
||||
return new Gost3410CryptoServiceProvider(entry.ProviderName, entry.ContainerName);
|
||||
return entry.Use256
|
||||
? new Gost_R3410_2012_256_AsymmetricAlgorithm(CreateCspParameters())
|
||||
: new Gost_R3410_2012_512_AsymmetricAlgorithm(CreateCspParameters());
|
||||
}
|
||||
|
||||
return new Gost3410CryptoServiceProvider(entry.Certificate);
|
||||
var algorithm = forVerification
|
||||
? entry.Certificate.GetPublicKeyAlgorithm()
|
||||
: entry.Certificate.GetPrivateKeyAlgorithm();
|
||||
|
||||
if (algorithm is GostAsymmetricAlgorithm gost)
|
||||
{
|
||||
return gost;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Certificate does not expose a GOST private key.");
|
||||
}
|
||||
|
||||
private CspParameters CreateCspParameters()
|
||||
{
|
||||
var providerType = entry.Use256 ? ProviderType.CryptoPro_2012_512 : ProviderType.CryptoPro_2012_1024;
|
||||
var flags = CspProviderFlags.UseExistingKey;
|
||||
if (entry.UseMachineKeyStore)
|
||||
{
|
||||
flags |= CspProviderFlags.UseMachineKeyStore;
|
||||
}
|
||||
|
||||
return new CspParameters(providerType.ToInt(), entry.ProviderName, entry.ContainerName)
|
||||
{
|
||||
Flags = flags,
|
||||
KeyNumber = (int)KeyNumber.Signature
|
||||
};
|
||||
}
|
||||
|
||||
private byte[] NormalizeSignatureForOutput(byte[] signature)
|
||||
{
|
||||
var coordinateLength = entry.CoordinateSize;
|
||||
|
||||
if (entry.SignatureFormat == GostSignatureFormat.Raw)
|
||||
{
|
||||
if (GostSignatureEncoding.IsDer(signature))
|
||||
{
|
||||
return GostSignatureEncoding.ToRaw(signature, coordinateLength);
|
||||
}
|
||||
|
||||
if (signature.Length == coordinateLength * 2)
|
||||
{
|
||||
return signature;
|
||||
}
|
||||
|
||||
throw new CryptographicException("Unexpected signature format returned by CryptoPro.");
|
||||
}
|
||||
|
||||
if (GostSignatureEncoding.IsDer(signature))
|
||||
{
|
||||
return signature;
|
||||
}
|
||||
|
||||
if (signature.Length == coordinateLength * 2)
|
||||
{
|
||||
return GostSignatureEncoding.ToDer(signature, coordinateLength);
|
||||
}
|
||||
|
||||
throw new CryptographicException("Unexpected signature format returned by CryptoPro.");
|
||||
}
|
||||
|
||||
private byte[] EnsureDerSignature(ReadOnlySpan<byte> signature)
|
||||
{
|
||||
var coordinateLength = entry.CoordinateSize;
|
||||
|
||||
if (GostSignatureEncoding.IsDer(signature))
|
||||
{
|
||||
return signature.ToArray();
|
||||
}
|
||||
|
||||
if (signature.Length == coordinateLength * 2)
|
||||
{
|
||||
return GostSignatureEncoding.ToDer(signature, coordinateLength);
|
||||
}
|
||||
|
||||
throw new CryptographicException("Signature payload is neither DER nor raw GOST format.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Cryptography.Tests")]
|
||||
@@ -9,7 +9,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GostCryptography" Version="2.0.11" />
|
||||
<PackageReference Include="IT.GostCryptography" Version="6.0.0.1" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
public static class OpenSslCryptoServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddOpenSslGostProvider(
|
||||
this IServiceCollection services,
|
||||
Action<OpenSslGostProviderOptions>? configure = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
if (configure is not null)
|
||||
{
|
||||
services.Configure(configure);
|
||||
}
|
||||
|
||||
services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<ICryptoProvider, OpenSslGostProvider>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
internal sealed class OpenSslGostKeyEntry
|
||||
{
|
||||
public OpenSslGostKeyEntry(
|
||||
string keyId,
|
||||
string algorithmId,
|
||||
ECPrivateKeyParameters privateKey,
|
||||
ECPublicKeyParameters publicKey,
|
||||
X509Certificate2? certificate,
|
||||
GostSignatureFormat signatureFormat)
|
||||
{
|
||||
KeyId = keyId;
|
||||
AlgorithmId = algorithmId;
|
||||
PrivateKey = privateKey;
|
||||
PublicKey = publicKey;
|
||||
Certificate = certificate;
|
||||
SignatureFormat = signatureFormat;
|
||||
}
|
||||
|
||||
public string KeyId { get; }
|
||||
|
||||
public string AlgorithmId { get; }
|
||||
|
||||
public ECPrivateKeyParameters PrivateKey { get; }
|
||||
|
||||
public ECPublicKeyParameters PublicKey { get; }
|
||||
|
||||
public X509Certificate2? Certificate { get; }
|
||||
|
||||
public GostSignatureFormat SignatureFormat { get; }
|
||||
|
||||
public bool Use256 => string.Equals(AlgorithmId, SignatureAlgorithms.GostR3410_2012_256, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public int CoordinateSize => Use256 ? 32 : 64;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
public sealed class OpenSslGostKeyOptions
|
||||
{
|
||||
[Required]
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Algorithm { get; set; } = SignatureAlgorithms.GostR3410_2012_256;
|
||||
|
||||
[Required]
|
||||
public string PrivateKeyPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Optional environment variable containing the passphrase for the private key PEM (avoids inline secrets).
|
||||
/// </summary>
|
||||
public string? PrivateKeyPassphraseEnvVar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional certificate (PEM/DER/PFX) to populate JWK x5c entries.
|
||||
/// </summary>
|
||||
public string? CertificatePath { get; set; }
|
||||
|
||||
public string? CertificatePasswordEnvVar { get; set; }
|
||||
|
||||
public GostSignatureFormat SignatureFormat { get; set; } = GostSignatureFormat.Der;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
public sealed class OpenSslGostProvider : ICryptoProvider, ICryptoProviderDiagnostics
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, OpenSslGostKeyEntry> entries;
|
||||
private readonly ILogger<OpenSslGostProvider>? logger;
|
||||
|
||||
public OpenSslGostProvider(
|
||||
IOptions<OpenSslGostProviderOptions>? optionsAccessor = null,
|
||||
ILogger<OpenSslGostProvider>? logger = null)
|
||||
{
|
||||
this.logger = logger;
|
||||
var options = optionsAccessor?.Value ?? new OpenSslGostProviderOptions();
|
||||
entries = LoadEntries(options);
|
||||
}
|
||||
|
||||
public string Name => "ru.openssl.gost";
|
||||
|
||||
public bool Supports(CryptoCapability capability, string algorithmId)
|
||||
=> (capability is CryptoCapability.Signing or CryptoCapability.Verification)
|
||||
&& (string.Equals(algorithmId, SignatureAlgorithms.GostR3410_2012_256, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(algorithmId, SignatureAlgorithms.GostR3410_2012_512, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public IPasswordHasher GetPasswordHasher(string algorithmId)
|
||||
=> throw new NotSupportedException("OpenSSL GOST provider does not expose password hashing.");
|
||||
|
||||
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(keyReference);
|
||||
if (string.IsNullOrEmpty(keyReference.KeyId))
|
||||
{
|
||||
throw new ArgumentException("Crypto key reference must include KeyId.", nameof(keyReference));
|
||||
}
|
||||
|
||||
if (!entries.TryGetValue(keyReference.KeyId, out var entry))
|
||||
{
|
||||
throw new KeyNotFoundException($"OpenSSL GOST key '{keyReference.KeyId}' is not registered.");
|
||||
}
|
||||
|
||||
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("Using OpenSSL GOST key {Key} ({Algorithm})", entry.KeyId, entry.AlgorithmId);
|
||||
return new OpenSslGostSigner(entry);
|
||||
}
|
||||
|
||||
public void UpsertSigningKey(CryptoSigningKey signingKey)
|
||||
=> throw new NotSupportedException("OpenSSL GOST provider uses external key material.");
|
||||
|
||||
public bool RemoveSigningKey(string keyId) => false;
|
||||
|
||||
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys()
|
||||
=> 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)
|
||||
{
|
||||
["certificate"] = entry.Certificate?.Subject,
|
||||
["curve"] = entry.PrivateKey.Parameters.Curve.FieldSize.ToString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, OpenSslGostKeyEntry> LoadEntries(OpenSslGostProviderOptions options)
|
||||
{
|
||||
var map = new Dictionary<string, OpenSslGostKeyEntry>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var key in options.Keys)
|
||||
{
|
||||
ValidateKeyOptions(key);
|
||||
var passphrase = ResolveSecret(key.PrivateKeyPassphraseEnvVar);
|
||||
var privateKey = OpenSslPemLoader.LoadPrivateKey(key.PrivateKeyPath, passphrase);
|
||||
|
||||
var certPassword = ResolveSecret(key.CertificatePasswordEnvVar);
|
||||
X509Certificate2? certificate;
|
||||
try
|
||||
{
|
||||
certificate = OpenSslPemLoader.LoadCertificate(key.CertificatePath, certPassword);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to load certificate for key '{key.KeyId}'.", ex);
|
||||
}
|
||||
|
||||
var publicKey = OpenSslPemLoader.LoadPublicKey(privateKey, certificate);
|
||||
var entry = new OpenSslGostKeyEntry(
|
||||
key.KeyId,
|
||||
key.Algorithm,
|
||||
privateKey,
|
||||
publicKey,
|
||||
certificate,
|
||||
key.SignatureFormat);
|
||||
|
||||
map[key.KeyId] = entry;
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void ValidateKeyOptions(OpenSslGostKeyOptions key)
|
||||
{
|
||||
if (!File.Exists(key.PrivateKeyPath))
|
||||
{
|
||||
throw new InvalidOperationException($"Private key '{key.PrivateKeyPath}' does not exist.");
|
||||
}
|
||||
|
||||
if (!string.Equals(key.Algorithm, SignatureAlgorithms.GostR3410_2012_256, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(key.Algorithm, SignatureAlgorithms.GostR3410_2012_512, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Unsupported GOST algorithm '{key.Algorithm}' for key '{key.KeyId}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveSecret(string? envVar)
|
||||
=> string.IsNullOrEmpty(envVar) ? null : Environment.GetEnvironmentVariable(envVar);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
public sealed class OpenSslGostProviderOptions
|
||||
{
|
||||
private readonly IList<OpenSslGostKeyOptions> keys = new List<OpenSslGostKeyOptions>();
|
||||
|
||||
public IList<OpenSslGostKeyOptions> Keys => keys;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Crypto.Signers;
|
||||
using Org.BouncyCastle.Math;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Cryptography;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
internal sealed class OpenSslGostSigner : ICryptoSigner
|
||||
{
|
||||
private static readonly SecureRandom SecureRandom = new();
|
||||
private readonly OpenSslGostKeyEntry entry;
|
||||
|
||||
public OpenSslGostSigner(OpenSslGostKeyEntry entry)
|
||||
=> this.entry = entry ?? throw new ArgumentNullException(nameof(entry));
|
||||
|
||||
public string KeyId => entry.KeyId;
|
||||
|
||||
public string AlgorithmId => entry.AlgorithmId;
|
||||
|
||||
public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var digest = entry.Use256
|
||||
? GostDigestUtilities.ComputeDigest(data.Span, use256: true)
|
||||
: GostDigestUtilities.ComputeDigest(data.Span, use256: false);
|
||||
|
||||
var signer = new ECGost3410Signer();
|
||||
signer.Init(true, new ParametersWithRandom(entry.PrivateKey, SecureRandom));
|
||||
var components = signer.GenerateSignature(digest);
|
||||
var encoded = EncodeSignature(components, entry.CoordinateSize, entry.SignatureFormat);
|
||||
return ValueTask.FromResult(encoded);
|
||||
}
|
||||
|
||||
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var digest = entry.Use256
|
||||
? GostDigestUtilities.ComputeDigest(data.Span, use256: true)
|
||||
: GostDigestUtilities.ComputeDigest(data.Span, use256: false);
|
||||
|
||||
var (r, s) = GostSignatureEncoding.DecodeComponents(signature.Span, entry.CoordinateSize);
|
||||
var verifier = new ECGost3410Signer();
|
||||
verifier.Init(false, entry.PublicKey);
|
||||
var valid = verifier.VerifySignature(digest, r, s);
|
||||
return ValueTask.FromResult(valid);
|
||||
}
|
||||
|
||||
public JsonWebKey ExportPublicJsonWebKey()
|
||||
{
|
||||
var jwk = new JsonWebKey
|
||||
{
|
||||
Kid = KeyId,
|
||||
Alg = AlgorithmId,
|
||||
Kty = "EC",
|
||||
Crv = entry.Use256 ? "GOST3410-2012-256" : "GOST3410-2012-512",
|
||||
Use = JsonWebKeyUseNames.Sig
|
||||
};
|
||||
|
||||
jwk.KeyOps.Add("sign");
|
||||
jwk.KeyOps.Add("verify");
|
||||
|
||||
if (entry.Certificate is not null)
|
||||
{
|
||||
jwk.X5c.Add(Convert.ToBase64String(entry.Certificate.RawData));
|
||||
}
|
||||
|
||||
return jwk;
|
||||
}
|
||||
|
||||
private static byte[] EncodeSignature(BigInteger[] components, int coordinateLength, GostSignatureFormat format)
|
||||
{
|
||||
var r = ToFixedLength(components[0], coordinateLength);
|
||||
var s = ToFixedLength(components[1], coordinateLength);
|
||||
|
||||
var raw = new byte[coordinateLength * 2];
|
||||
s.CopyTo(raw.AsSpan(0, coordinateLength));
|
||||
r.CopyTo(raw.AsSpan(coordinateLength));
|
||||
|
||||
return format == GostSignatureFormat.Raw
|
||||
? raw
|
||||
: GostSignatureEncoding.ToDer(raw, coordinateLength);
|
||||
}
|
||||
|
||||
private static byte[] ToFixedLength(BigInteger value, int coordinateLength)
|
||||
{
|
||||
var bytes = value.ToByteArrayUnsigned();
|
||||
if (bytes.Length > coordinateLength)
|
||||
{
|
||||
throw new CryptographicException("GOST signature component exceeds expected length.");
|
||||
}
|
||||
|
||||
if (bytes.Length == coordinateLength)
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
|
||||
var padded = new byte[coordinateLength];
|
||||
bytes.CopyTo(padded.AsSpan(coordinateLength - bytes.Length));
|
||||
return padded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
namespace StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
|
||||
internal static class OpenSslPemLoader
|
||||
{
|
||||
public static ECPrivateKeyParameters LoadPrivateKey(string path, string? passphrase)
|
||||
{
|
||||
using var reader = File.OpenText(path);
|
||||
var pemReader = string.IsNullOrEmpty(passphrase)
|
||||
? new PemReader(reader)
|
||||
: new PemReader(reader, new StaticPasswordFinder(passphrase));
|
||||
|
||||
var pemObject = pemReader.ReadObject();
|
||||
|
||||
return pemObject switch
|
||||
{
|
||||
AsymmetricCipherKeyPair pair when pair.Private is ECPrivateKeyParameters ecPrivate => ecPrivate,
|
||||
ECPrivateKeyParameters ecPrivate => ecPrivate,
|
||||
_ => throw new InvalidOperationException($"Unsupported private key content in '{path}'.")
|
||||
};
|
||||
}
|
||||
|
||||
public static ECPublicKeyParameters LoadPublicKey(ECPrivateKeyParameters privateKey, X509Certificate2? certificate)
|
||||
{
|
||||
if (certificate is not null)
|
||||
{
|
||||
var bouncyCert = DotNetUtilities.FromX509Certificate(certificate);
|
||||
var keyParam = bouncyCert.GetPublicKey();
|
||||
if (keyParam is ECPublicKeyParameters ecPublic)
|
||||
{
|
||||
return ecPublic;
|
||||
}
|
||||
}
|
||||
|
||||
var q = privateKey.Parameters.G.Multiply(privateKey.D).Normalize();
|
||||
return new ECPublicKeyParameters(q, privateKey.Parameters);
|
||||
}
|
||||
|
||||
public static X509Certificate2? LoadCertificate(string? path, string? passphrase)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(Path.GetExtension(path), ".pem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return X509Certificate2.CreateFromPemFile(path);
|
||||
}
|
||||
|
||||
var password = string.IsNullOrEmpty(passphrase) ? null : passphrase;
|
||||
return string.IsNullOrEmpty(password)
|
||||
? X509CertificateLoader.LoadPkcs12FromFile(path, ReadOnlySpan<char>.Empty)
|
||||
: X509CertificateLoader.LoadPkcs12FromFile(path, password.AsSpan());
|
||||
}
|
||||
|
||||
private sealed class StaticPasswordFinder : IPasswordFinder
|
||||
{
|
||||
private readonly char[] password;
|
||||
|
||||
public StaticPasswordFinder(string passphrase)
|
||||
=> password = passphrase.ToCharArray();
|
||||
|
||||
public char[] GetPassword() => password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("StellaOps.Cryptography.Tests")]
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.5.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
126
src/__Libraries/StellaOps.Cryptography/GostSignatureEncoding.cs
Normal file
126
src/__Libraries/StellaOps.Cryptography/GostSignatureEncoding.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Math;
|
||||
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
public static class GostSignatureEncoding
|
||||
{
|
||||
public static bool IsDer(ReadOnlySpan<byte> signature)
|
||||
{
|
||||
if (signature.Length < 2 || signature[0] != 0x30)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lengthByte = signature[1];
|
||||
if ((lengthByte & 0x80) == 0)
|
||||
{
|
||||
var total = lengthByte + 2;
|
||||
return total == signature.Length;
|
||||
}
|
||||
|
||||
var lengthBytes = lengthByte & 0x7F;
|
||||
if (lengthBytes is 0 or > 4 || signature.Length < 2 + lengthBytes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var totalLength = 0;
|
||||
for (var i = 0; i < lengthBytes; i++)
|
||||
{
|
||||
totalLength = (totalLength << 8) | signature[2 + i];
|
||||
}
|
||||
|
||||
return totalLength + 2 + lengthBytes == signature.Length;
|
||||
}
|
||||
|
||||
public static byte[] ToRaw(ReadOnlySpan<byte> der, int coordinateLength)
|
||||
{
|
||||
if (!IsDer(der))
|
||||
{
|
||||
throw new CryptographicException("Signature is not DER encoded.");
|
||||
}
|
||||
|
||||
var sequence = Asn1Sequence.GetInstance(Asn1Object.FromByteArray(der.ToArray()));
|
||||
if (sequence.Count != 2)
|
||||
{
|
||||
throw new CryptographicException("Invalid DER structure for GOST signature.");
|
||||
}
|
||||
|
||||
var r = NormalizeCoordinate(((DerInteger)sequence[0]).PositiveValue.ToByteArrayUnsigned(), coordinateLength);
|
||||
var s = NormalizeCoordinate(((DerInteger)sequence[1]).PositiveValue.ToByteArrayUnsigned(), coordinateLength);
|
||||
|
||||
var raw = new byte[coordinateLength * 2];
|
||||
s.CopyTo(raw.AsSpan(0, coordinateLength));
|
||||
r.CopyTo(raw.AsSpan(coordinateLength));
|
||||
return raw;
|
||||
}
|
||||
|
||||
public static byte[] ToDer(ReadOnlySpan<byte> raw, int coordinateLength)
|
||||
{
|
||||
if (raw.Length != coordinateLength * 2)
|
||||
{
|
||||
throw new CryptographicException($"Raw GOST signature must be {coordinateLength * 2} bytes.");
|
||||
}
|
||||
|
||||
var s = raw[..coordinateLength].ToArray();
|
||||
var r = raw[coordinateLength..].ToArray();
|
||||
|
||||
var derSequence = new DerSequence(
|
||||
new DerInteger(new BigInteger(1, r)),
|
||||
new DerInteger(new BigInteger(1, s)));
|
||||
|
||||
return derSequence.GetDerEncoded();
|
||||
}
|
||||
|
||||
public static (BigInteger r, BigInteger s) DecodeComponents(ReadOnlySpan<byte> signature, int coordinateLength)
|
||||
{
|
||||
if (IsDer(signature))
|
||||
{
|
||||
var sequence = Asn1Sequence.GetInstance(Asn1Object.FromByteArray(signature.ToArray()));
|
||||
if (sequence.Count != 2)
|
||||
{
|
||||
throw new CryptographicException("Invalid DER structure for GOST signature.");
|
||||
}
|
||||
|
||||
return (((DerInteger)sequence[0]).PositiveValue, ((DerInteger)sequence[1]).PositiveValue);
|
||||
}
|
||||
|
||||
if (signature.Length == coordinateLength * 2)
|
||||
{
|
||||
var s = new byte[coordinateLength];
|
||||
var r = new byte[coordinateLength];
|
||||
signature[..coordinateLength].CopyTo(s);
|
||||
signature[coordinateLength..].CopyTo(r);
|
||||
return (new BigInteger(1, r), new BigInteger(1, s));
|
||||
}
|
||||
|
||||
throw new CryptographicException("Signature payload is neither DER nor raw GOST format.");
|
||||
}
|
||||
|
||||
private static byte[] NormalizeCoordinate(ReadOnlySpan<byte> value, int coordinateLength)
|
||||
{
|
||||
var trimmed = TrimLeadingZeros(value);
|
||||
if (trimmed.Length > coordinateLength)
|
||||
{
|
||||
throw new CryptographicException("Coordinate exceeds expected length.");
|
||||
}
|
||||
|
||||
var output = new byte[coordinateLength];
|
||||
trimmed.CopyTo(output.AsSpan(coordinateLength - trimmed.Length));
|
||||
return output;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> TrimLeadingZeros(ReadOnlySpan<byte> value)
|
||||
{
|
||||
var index = 0;
|
||||
while (index < value.Length - 1 && value[index] == 0)
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
return value[index..];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Cryptography;
|
||||
|
||||
/// <summary>
|
||||
/// Desired wire format for GOST 34.10 signatures.
|
||||
/// </summary>
|
||||
public enum GostSignatureFormat
|
||||
{
|
||||
/// <summary>DER-encoded ASN.1 sequence (default).</summary>
|
||||
Der = 0,
|
||||
|
||||
/// <summary>Concatenated (s || r) raw bytes per RFC 9215.</summary>
|
||||
Raw = 1
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# Active Tasks
|
||||
|
||||
| ID | Status | Owner | Description | Dependencies | Exit Criteria |
|
||||
|----|--------|-------|-------------|--------------|---------------|
|
||||
| SEC-CRYPTO-90-009 | TODO | Security Guild | Replace the placeholder CryptoPro plug-in with a true CryptoPro CSP implementation (GostCryptography driver, X509 store lookups, DER/raw normalization, deterministic logging). | SPRINT_514 | ✅ CryptoPro provider no longer depends on PKCS#11 core; ✅ Certificates resolved via thumbprint/container; ✅ Sign/verify + JWK export exercised in tests/harness. |
|
||||
| SEC-CRYPTO-90-010 | TODO | Security Guild | Introduce `StellaOpsCryptoOptions` + configuration binding for registry profiles/keys and expose `AddStellaOpsCryptoRu(IConfiguration, …)` so hosts can enable `ru-offline` without handwritten wiring. | SPRINT_514 | ✅ Options bind from `StellaOps:Crypto` (registry, CryptoPro, PKCS#11); ✅ New DI helper registers providers + preferred order; ✅ Sample config (`etc/rootpack/ru/crypto.profile.yaml`) loads without custom code. |
|
||||
| SEC-CRYPTO-90-011 | TODO | Security & Ops Guilds | Build the sovereign crypto CLI (`StellaOps.CryptoRu.Cli`) to list keys, perform test-sign operations, and emit determinism/audit payloads referenced by RootPack docs. | SPRINT_514 | ✅ CLI project under `src/Tools/`; ✅ `list` & `sign` commands hit provider registry; ✅ README/runbooks updated with usage examples. |
|
||||
| SEC-CRYPTO-90-012 | TODO | Security Guild | Add CryptoPro + PKCS#11 integration tests (env/pin gated) and wire them into `scripts/crypto/run-rootpack-ru-tests.sh`, covering Streebog vectors and DER/raw signatures. | SPRINT_514 | ✅ New tests skip gracefully when env vars absent; ✅ Test harness logs include RU cases; ✅ CI instructions documented. |
|
||||
| SEC-CRYPTO-90-013 | TODO | Security Guild | Extend the shared crypto stack with sovereign symmetric algorithms (Magma/Kuznyechik) so exports/data-at-rest can request Russian ciphers via the provider registry. | SPRINT_514 | ✅ Hash/service interfaces support symmetric operations; ✅ CryptoPro/PKCS#11 providers implement Magma/Kuznyechik; ✅ Sample usage documented/tests added. |
|
||||
| SEC-CRYPTO-90-014 | TODO | Security Guild + Service Guilds | Update runtime hosts (Authority, Scanner WebService/Worker, Concelier, etc.) to register RU providers, bind `StellaOps:Crypto` profiles, and expose operator-facing configuration toggles. | SPRINT_514 | ✅ Each host calls the new DI helper/config binding; ✅ Default configs document `ru-offline`; ✅ Sovereign bundles verified end-to-end. |
|
||||
| SEC-CRYPTO-90-015 | TODO | Security & Docs Guilds | Refresh RootPack/validation documentation once CLI/config/tests exist (remove TODO callouts, document final workflows). | SPRINT_514 | ✅ TODO sections removed from `rootpack_ru_*` docs; ✅ Final CLI/test steps published; ✅ Release checklist updated. |
|
||||
@@ -0,0 +1,41 @@
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class CryptoProGostSignerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExportPublicJsonWebKey_ContainsCertificateChain()
|
||||
{
|
||||
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var request = new CertificateRequest("CN=stellaops.test", ecdsa, HashAlgorithmName.SHA256);
|
||||
using var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
|
||||
|
||||
var entry = new CryptoProGostKeyEntry(
|
||||
"test-key",
|
||||
SignatureAlgorithms.GostR3410_2012_256,
|
||||
cert,
|
||||
"provider",
|
||||
containerName: null,
|
||||
useMachineKeyStore: false,
|
||||
signatureFormat: GostSignatureFormat.Der);
|
||||
|
||||
var signer = new CryptoProGostSigner(entry);
|
||||
|
||||
var jwk = signer.ExportPublicJsonWebKey();
|
||||
|
||||
Assert.Equal("test-key", jwk.Kid);
|
||||
Assert.Equal(SignatureAlgorithms.GostR3410_2012_256, jwk.Alg);
|
||||
Assert.Equal(JsonWebKeyUseNames.Sig, jwk.Use);
|
||||
Assert.Single(jwk.X5c);
|
||||
Assert.Equal("EC", jwk.Kty);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
#if STELLAOPS_CRYPTO_PRO
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.CryptoPro;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class GostSignatureEncodingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(32)]
|
||||
[InlineData(64)]
|
||||
public void RawAndDer_RoundTrip(int coordinateLength)
|
||||
{
|
||||
var raw = Enumerable.Range(1, coordinateLength * 2)
|
||||
.Select(i => (byte)(i & 0xFF))
|
||||
.ToArray();
|
||||
|
||||
var der = GostSignatureEncoding.ToDer(raw, coordinateLength);
|
||||
Assert.True(GostSignatureEncoding.IsDer(der));
|
||||
|
||||
var roundTrip = GostSignatureEncoding.ToRaw(der, coordinateLength);
|
||||
Assert.Equal(raw, roundTrip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToDer_Throws_When_Length_Invalid()
|
||||
{
|
||||
var raw = new byte[10];
|
||||
Assert.Throws<CryptographicException>(() => GostSignatureEncoding.ToDer(raw, coordinateLength: 32));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRaw_Throws_When_Not_Der()
|
||||
{
|
||||
var raw = new byte[64];
|
||||
Assert.Throws<CryptographicException>(() => GostSignatureEncoding.ToRaw(raw, coordinateLength: 32));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Org.BouncyCastle.Asn1.CryptoPro;
|
||||
using Org.BouncyCastle.Asn1.Rosstandart;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Crypto.Prng;
|
||||
using Org.BouncyCastle.Security;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Cryptography.Plugin.OpenSslGost;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Cryptography.Tests;
|
||||
|
||||
public class OpenSslGostSignerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SignAndVerify_WithManagedProvider_Succeeds()
|
||||
{
|
||||
var keyPair = GenerateKeyPair();
|
||||
var entry = new OpenSslGostKeyEntry(
|
||||
"ru-openssl-test",
|
||||
SignatureAlgorithms.GostR3410_2012_256,
|
||||
(ECPrivateKeyParameters)keyPair.Private,
|
||||
(ECPublicKeyParameters)keyPair.Public,
|
||||
certificate: null,
|
||||
signatureFormat: GostSignatureFormat.Raw);
|
||||
|
||||
var signer = new OpenSslGostSigner(entry);
|
||||
var payload = Encoding.UTF8.GetBytes("open-ssl-gost");
|
||||
|
||||
var signature = await signer.SignAsync(payload);
|
||||
Assert.True(await signer.VerifyAsync(payload, signature));
|
||||
|
||||
var jwk = signer.ExportPublicJsonWebKey();
|
||||
Assert.Equal("ru-openssl-test", jwk.Kid);
|
||||
Assert.Equal(SignatureAlgorithms.GostR3410_2012_256, jwk.Alg);
|
||||
Assert.Empty(jwk.X5c);
|
||||
}
|
||||
|
||||
private static AsymmetricCipherKeyPair GenerateKeyPair()
|
||||
{
|
||||
var generator = new ECKeyPairGenerator("ECGOST3410");
|
||||
var parameters = ECGost3410NamedCurves.GetByOid(RosstandartObjectIdentifiers.id_tc26_gost_3410_12_256_paramSetA);
|
||||
var domain = new ECDomainParameters(parameters.Curve, parameters.G, parameters.N, parameters.H);
|
||||
generator.Init(new ECKeyGenerationParameters(domain, new SecureRandom(new CryptoApiRandomGenerator())));
|
||||
return generator.GenerateKeyPair();
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,9 @@
|
||||
<ProjectReference Include="../../StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.Cryptography.DependencyInjection/StellaOps.Cryptography.DependencyInjection.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.BouncyCastle/StellaOps.Cryptography.Plugin.BouncyCastle.csproj" />
|
||||
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.OpenSslGost/StellaOps.Cryptography.Plugin.OpenSslGost.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<ItemGroup Condition="'$(StellaOpsEnableCryptoPro)' == 'true'">
|
||||
<ProjectReference Include="../../StellaOps.Cryptography.Plugin.CryptoPro/StellaOps.Cryptography.Plugin.CryptoPro.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user