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:
master
2025-11-09 21:59:57 +02:00
parent 75c2bcafce
commit cef4cb2c5a
486 changed files with 32952 additions and 801 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StellaOps.Cryptography.Tests")]

View File

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