Add LDAP Distinguished Name Helper and Credential Audit Context
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implemented LdapDistinguishedNameHelper for escaping RDN and filter values.
- Created AuthorityCredentialAuditContext and IAuthorityCredentialAuditContextAccessor for managing credential audit context.
- Developed StandardCredentialAuditLogger with tests for success, failure, and lockout events.
- Introduced AuthorityAuditSink for persisting audit records with structured logging.
- Added CryptoPro related classes for certificate resolution and signing operations.
This commit is contained in:
master
2025-11-09 12:21:38 +02:00
parent ba4c935182
commit 75c2bcafce
385 changed files with 7354 additions and 7344 deletions

View File

@@ -0,0 +1,70 @@
using System;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
namespace StellaOps.Cryptography.Plugin.CryptoPro;
internal static class CryptoProCertificateResolver
{
public static X509Certificate2 Resolve(CryptoProGostKeyOptions options)
{
if (!string.IsNullOrWhiteSpace(options.CertificateThumbprint))
{
var cert = FindByThumbprint(options.CertificateThumbprint!, options.CertificateStoreName, options.CertificateStoreLocation);
if (cert is not null)
{
return cert;
}
throw new InvalidOperationException(
$"Certificate with thumbprint '{options.CertificateThumbprint}' was not found in {options.CertificateStoreLocation}/{options.CertificateStoreName}.");
}
if (!string.IsNullOrWhiteSpace(options.SubjectName))
{
var cert = FindBySubject(options.SubjectName!, options.CertificateStoreName, options.CertificateStoreLocation);
if (cert is not null)
{
return cert;
}
throw new InvalidOperationException(
$"Certificate with subject containing '{options.SubjectName}' was not found in {options.CertificateStoreLocation}/{options.CertificateStoreName}.");
}
throw new InvalidOperationException($"CryptoPro key '{options.KeyId}' must specify either CertificateThumbprint or SubjectName.");
}
private static X509Certificate2? FindByThumbprint(string thumbprint, StoreName storeName, StoreLocation storeLocation)
{
using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
foreach (var cert in store.Certificates)
{
if (string.Equals(Normalize(thumbprint), Normalize(cert.Thumbprint ?? string.Empty), StringComparison.OrdinalIgnoreCase))
{
return new X509Certificate2(cert);
}
}
return null;
}
private static X509Certificate2? FindBySubject(string subjectFragment, StoreName storeName, StoreLocation storeLocation)
{
using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
foreach (var cert in store.Certificates)
{
if (cert.Subject?.IndexOf(subjectFragment, StringComparison.OrdinalIgnoreCase) >= 0)
{
return new X509Certificate2(cert);
}
}
return null;
}
private static string Normalize(string value)
=> value.Replace(" ", string.Empty, StringComparison.Ordinal).ToUpperInvariant(CultureInfo.InvariantCulture);
}

View File

@@ -3,39 +3,43 @@ using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Cryptography;
using StellaOps.Cryptography.Plugin.Pkcs11Gost;
namespace StellaOps.Cryptography.Plugin.CryptoPro;
public sealed class CryptoProGostCryptoProvider : ICryptoProvider, ICryptoProviderDiagnostics
{
private readonly Pkcs11GostProviderCore core;
private readonly ILogger<CryptoProGostCryptoProvider>? logger;
private readonly IReadOnlyDictionary<string, CryptoProGostKeyEntry> entries;
public CryptoProGostCryptoProvider(
IOptions<CryptoProGostProviderOptions>? optionsAccessor = null,
ILogger<CryptoProGostCryptoProvider>? logger = null)
{
this.logger = logger;
var options = optionsAccessor?.Value ?? new CryptoProGostProviderOptions();
var mappedKeys = new List<Pkcs11GostKeyOptions>(options.Keys.Count);
var map = new Dictionary<string, CryptoProGostKeyEntry>(StringComparer.OrdinalIgnoreCase);
foreach (var key in options.Keys)
{
mappedKeys.Add(MapToPkcs11Options(key));
var certificate = CryptoProCertificateResolver.Resolve(key);
var entry = new CryptoProGostKeyEntry(
key.KeyId,
key.Algorithm,
certificate,
key.ProviderName,
key.ContainerName);
map[key.KeyId] = entry;
}
core = new Pkcs11GostProviderCore("ru.cryptopro.csp", mappedKeys, logger);
entries = map;
}
public string Name => core.ProviderName;
public string Name => "ru.cryptopro.csp";
public bool Supports(CryptoCapability capability, string algorithmId)
{
if (capability is CryptoCapability.Signing or CryptoCapability.Verification)
{
return core.SupportsAlgorithm(algorithmId);
}
return false;
}
=> 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("CryptoPro provider does not expose password hashing.");
@@ -43,14 +47,15 @@ public sealed class CryptoProGostCryptoProvider : ICryptoProvider, ICryptoProvid
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
{
ArgumentNullException.ThrowIfNull(keyReference);
var entry = core.Resolve(keyReference.KeyId);
var entry = ResolveKey(keyReference.KeyId);
if (!string.Equals(entry.AlgorithmId, algorithmId, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
$"Signing key '{keyReference.KeyId}' is registered for algorithm '{entry.AlgorithmId}', not '{algorithmId}'.");
}
return new Pkcs11GostSigner(entry);
logger?.LogDebug("Using CryptoPro key {Key} ({Algorithm})", entry.KeyId, entry.AlgorithmId);
return new CryptoProGostSigner(entry);
}
public void UpsertSigningKey(CryptoSigningKey signingKey)
@@ -62,26 +67,35 @@ public sealed class CryptoProGostCryptoProvider : ICryptoProvider, ICryptoProvid
=> Array.Empty<CryptoSigningKey>();
public IEnumerable<CryptoProviderKeyDescriptor> DescribeKeys()
=> core.DescribeKeys(Name);
private static Pkcs11GostKeyOptions MapToPkcs11Options(CryptoProGostKeyOptions source)
{
ArgumentNullException.ThrowIfNull(source);
return new Pkcs11GostKeyOptions
foreach (var entry in entries.Values)
{
KeyId = source.KeyId,
Algorithm = source.Algorithm,
LibraryPath = source.LibraryPath,
SlotId = source.SlotId,
TokenLabel = source.TokenLabel,
PrivateKeyLabel = source.ContainerLabel,
UserPin = source.UserPin,
UserPinEnvironmentVariable = source.UserPinEnvironmentVariable,
SignMechanismId = source.SignMechanismId,
CertificateThumbprint = source.CertificateThumbprint,
CertificateStoreLocation = source.CertificateStoreLocation.ToString(),
CertificateStoreName = source.CertificateStoreName.ToString()
};
yield return new CryptoProviderKeyDescriptor(
Name,
entry.KeyId,
entry.AlgorithmId,
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["provider"] = entry.ProviderName,
["container"] = entry.ContainerName,
["thumbprint"] = entry.Certificate.Thumbprint,
["subject"] = entry.Certificate.Subject
});
}
}
private CryptoProGostKeyEntry ResolveKey(string? keyId)
{
if (string.IsNullOrWhiteSpace(keyId))
{
throw new ArgumentException("Crypto key reference must include KeyId.", nameof(keyId));
}
if (entries.TryGetValue(keyId, out var entry))
{
return entry;
}
throw new KeyNotFoundException($"CryptoPro key '{keyId}' is not registered.");
}
}

View File

@@ -0,0 +1,32 @@
using System.Security.Cryptography.X509Certificates;
namespace StellaOps.Cryptography.Plugin.CryptoPro;
internal sealed class CryptoProGostKeyEntry
{
public CryptoProGostKeyEntry(
string keyId,
string algorithmId,
X509Certificate2 certificate,
string providerName,
string? containerName)
{
KeyId = keyId;
AlgorithmId = algorithmId;
Certificate = certificate;
ProviderName = providerName;
ContainerName = containerName;
}
public string KeyId { get; }
public string AlgorithmId { get; }
public X509Certificate2 Certificate { get; }
public string ProviderName { get; }
public string? ContainerName { get; }
public bool Use256 => string.Equals(AlgorithmId, SignatureAlgorithms.GostR3410_2012_256, StringComparison.OrdinalIgnoreCase);
}

View File

@@ -12,24 +12,24 @@ public sealed class CryptoProGostKeyOptions
public string Algorithm { get; set; } = SignatureAlgorithms.GostR3410_2012_256;
/// <summary>
/// PKCS#11 library path (typically cprocsp-pkcs11*.dll/so).
/// Optional CryptoPro provider name (defaults to standard CSP).
/// </summary>
[Required]
public string LibraryPath { get; set; } = string.Empty;
public string ProviderName { get; set; } = "Crypto-Pro GOST R 34.10-2012 Cryptographic Service Provider";
public string? SlotId { get; set; }
/// <summary>
/// Optional container name. If omitted, the certificate private key association is used.
/// </summary>
public string? ContainerName { get; set; }
public string? TokenLabel { get; set; }
/// <summary>
/// Thumbprint of the certificate that owns the CryptoPro private key.
/// </summary>
public string? CertificateThumbprint { get; set; }
public string? ContainerLabel { get; set; }
public string? UserPin { get; set; }
public string? UserPinEnvironmentVariable { get; set; }
public uint? SignMechanismId { get; set; }
public string CertificateThumbprint { get; set; } = string.Empty;
/// <summary>
/// Alternative lookup if thumbprint is not supplied.
/// </summary>
public string? SubjectName { get; set; }
public StoreLocation CertificateStoreLocation { get; set; } = StoreLocation.CurrentUser;
@@ -40,14 +40,10 @@ public sealed class CryptoProGostKeyOptions
{
KeyId = KeyId,
Algorithm = Algorithm,
LibraryPath = LibraryPath,
SlotId = SlotId,
TokenLabel = TokenLabel,
ContainerLabel = ContainerLabel,
UserPin = UserPin,
UserPinEnvironmentVariable = UserPinEnvironmentVariable,
SignMechanismId = SignMechanismId,
ProviderName = ProviderName,
ContainerName = ContainerName,
CertificateThumbprint = CertificateThumbprint,
SubjectName = SubjectName,
CertificateStoreLocation = CertificateStoreLocation,
CertificateStoreName = CertificateStoreName
};

View File

@@ -0,0 +1,75 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GostCryptography.Gost_3410;
using StellaOps.Cryptography;
namespace StellaOps.Cryptography.Plugin.CryptoPro;
internal sealed class CryptoProGostSigner : ICryptoSigner
{
private readonly CryptoProGostKeyEntry entry;
public CryptoProGostSigner(CryptoProGostKeyEntry 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);
using var provider = CreateProvider();
var signature = provider.SignHash(digest);
return ValueTask.FromResult(signature);
}
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);
using var provider = CreateProvider();
var valid = provider.VerifyHash(digest, signature.ToArray());
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");
jwk.X5c.Add(Convert.ToBase64String(entry.Certificate.RawData));
return jwk;
}
private Gost3410CryptoServiceProvider CreateProvider()
{
if (!string.IsNullOrWhiteSpace(entry.ContainerName))
{
return new Gost3410CryptoServiceProvider(entry.ProviderName, entry.ContainerName);
}
return new Gost3410CryptoServiceProvider(entry.Certificate);
}
}

View File

@@ -5,17 +5,18 @@
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GostCryptography" Version="2.0.11" />
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\\StellaOps.Cryptography.Plugin.Pkcs11Gost\\StellaOps.Cryptography.Plugin.Pkcs11Gost.csproj" />
<ProjectReference Include="..\\StellaOps.Cryptography\\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>