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,159 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.Logging;
using StellaOps.Authority.Plugin.Ldap.Connections;
using StellaOps.Authority.Plugin.Ldap.Security;
namespace StellaOps.Authority.Plugin.Ldap.ClientProvisioning;
internal sealed class LdapCapabilityProbe
{
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5);
private readonly string pluginName;
private readonly ILdapConnectionFactory connectionFactory;
private readonly ILogger logger;
public LdapCapabilityProbe(
string pluginName,
ILdapConnectionFactory connectionFactory,
ILogger logger)
{
this.pluginName = pluginName ?? throw new ArgumentNullException(nameof(pluginName));
this.connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public LdapCapabilitySnapshot Evaluate(LdapPluginOptions options, bool checkClientProvisioning, bool checkBootstrap)
{
if (!checkClientProvisioning && !checkBootstrap)
{
return new LdapCapabilitySnapshot(false, false);
}
var clientProvisioningWritable = false;
var bootstrapWritable = false;
try
{
using var timeoutCts = new CancellationTokenSource(DefaultTimeout);
var cancellationToken = timeoutCts.Token;
var connection = connectionFactory.CreateAsync(cancellationToken).GetAwaiter().GetResult();
try
{
MaybeBindServiceAccount(connection, options, cancellationToken);
if (checkClientProvisioning)
{
clientProvisioningWritable = TryProbeContainer(
connection,
options.ClientProvisioning.ContainerDn,
options.ClientProvisioning.RdnAttribute,
cancellationToken);
}
if (checkBootstrap)
{
bootstrapWritable = TryProbeContainer(
connection,
options.Bootstrap.ContainerDn,
options.Bootstrap.RdnAttribute,
cancellationToken);
}
}
finally
{
connection.DisposeAsync().GetAwaiter().GetResult();
}
}
catch (Exception ex) when (ex is LdapOperationException or LdapTransientException)
{
logger.LogWarning(
ex,
"LDAP plugin {Plugin} capability probe failed ({Message}). Capabilities will be downgraded.",
pluginName,
ex.Message);
}
catch (Exception ex)
{
logger.LogWarning(
ex,
"LDAP plugin {Plugin} encountered an unexpected capability probe error. Capabilities will be downgraded.",
pluginName);
}
return new LdapCapabilitySnapshot(clientProvisioningWritable, bootstrapWritable);
}
private void MaybeBindServiceAccount(ILdapConnectionHandle connection, LdapPluginOptions options, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(options.Connection.BindDn))
{
return;
}
var secret = LdapSecretResolver.Resolve(options.Connection.BindPasswordSecret);
connection.BindAsync(options.Connection.BindDn!, secret, cancellationToken).GetAwaiter().GetResult();
}
private bool TryProbeContainer(
ILdapConnectionHandle connection,
string? containerDn,
string rdnAttribute,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(containerDn))
{
logger.LogWarning(
"LDAP plugin {Plugin} cannot probe capability because container DN is not configured.",
pluginName);
return false;
}
var probeId = $"stellaops-probe-{Guid.NewGuid():N}";
var distinguishedName = $"{rdnAttribute}={LdapDistinguishedNameHelper.EscapeRdnValue(probeId)},{containerDn}";
var attributes = new Dictionary<string, IReadOnlyCollection<string>>(StringComparer.OrdinalIgnoreCase)
{
["objectClass"] = new[] { "top", "person", "organizationalPerson" },
[rdnAttribute] = new[] { probeId },
["cn"] = new[] { probeId },
["sn"] = new[] { probeId }
};
try
{
connection.AddEntryAsync(distinguishedName, attributes, cancellationToken).GetAwaiter().GetResult();
connection.DeleteEntryAsync(distinguishedName, cancellationToken).GetAwaiter().GetResult();
return true;
}
catch (LdapInsufficientAccessException ex)
{
logger.LogWarning(ex, "LDAP plugin {Plugin} lacks write permissions for container {Container}.", pluginName, containerDn);
return false;
}
catch (Exception ex) when (ex is LdapOperationException or LdapTransientException)
{
logger.LogWarning(ex, "LDAP plugin {Plugin} probe failed for container {Container}.", pluginName, containerDn);
return false;
}
finally
{
TryDeleteProbeEntry(connection, distinguishedName, cancellationToken);
}
}
private void TryDeleteProbeEntry(ILdapConnectionHandle connection, string dn, CancellationToken cancellationToken)
{
try
{
connection.DeleteEntryAsync(dn, cancellationToken).GetAwaiter().GetResult();
}
catch
{
// Best-effort cleanup.
}
}
}

View File

@@ -9,6 +9,7 @@ using Microsoft.Extensions.Options;
using MongoDB.Driver;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Authority.Plugin.Ldap.Bootstrap;
using StellaOps.Authority.Plugin.Ldap.ClientProvisioning;
using StellaOps.Authority.Plugin.Ldap.Connections;
using StellaOps.Authority.Plugin.Ldap.Monitoring;
using StellaOps.Authority.Plugin.Ldap.Security;
@@ -459,4 +460,170 @@ internal sealed class LdapCredentialStore : IUserCredentialStore
Array.Empty<string>(),
attributeSnapshot);
}
private async Task<AuthorityUserDescriptor> ProvisionBootstrapUserAsync(
AuthorityUserRegistration registration,
LdapPluginOptions pluginOptions,
LdapBootstrapOptions bootstrapOptions,
CancellationToken cancellationToken)
{
var normalizedUsername = NormalizeUsername(registration.Username);
await using var connection = await connectionFactory.CreateAsync(cancellationToken).ConfigureAwait(false);
await EnsureServiceBindAsync(connection, pluginOptions, cancellationToken).ConfigureAwait(false);
var distinguishedName = BuildBootstrapDistinguishedName(normalizedUsername, bootstrapOptions);
var attributes = BuildBootstrapAttributes(registration, normalizedUsername, bootstrapOptions);
var filter = $"({bootstrapOptions.UsernameAttribute}={LdapDistinguishedNameHelper.EscapeFilterValue(normalizedUsername)})";
var existing = await ExecuteWithRetryAsync(
"bootstrap_lookup",
ct => connection.FindEntryAsync(bootstrapOptions.ContainerDn!, filter, Array.Empty<string>(), ct),
cancellationToken).ConfigureAwait(false);
if (existing is null)
{
await connection.AddEntryAsync(distinguishedName, attributes, cancellationToken).ConfigureAwait(false);
}
else
{
await connection.ModifyEntryAsync(distinguishedName, attributes, cancellationToken).ConfigureAwait(false);
}
await WriteBootstrapAuditRecordAsync(registration, bootstrapOptions, distinguishedName, cancellationToken).ConfigureAwait(false);
var syntheticEntry = new LdapSearchEntry(
distinguishedName,
ConvertAttributes(attributes));
return BuildDescriptor(syntheticEntry, normalizedUsername, registration.RequirePasswordReset);
}
private async Task WriteBootstrapAuditRecordAsync(
AuthorityUserRegistration registration,
LdapBootstrapOptions options,
string distinguishedName,
CancellationToken cancellationToken)
{
if (!options.AuditMirror.Enabled)
{
return;
}
var collectionName = options.ResolveAuditCollectionName(pluginName);
var collection = mongoDatabase.GetCollection<LdapBootstrapAuditDocument>(collectionName);
var document = new LdapBootstrapAuditDocument
{
Plugin = pluginName,
Username = NormalizeUsername(registration.Username),
DistinguishedName = distinguishedName,
Operation = "upsert",
SecretHash = string.IsNullOrWhiteSpace(registration.Password)
? null
: AuthoritySecretHasher.ComputeHash(registration.Password!),
Timestamp = timeProvider.GetUtcNow(),
Metadata = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["requirePasswordReset"] = registration.RequirePasswordReset ? "true" : "false",
["email"] = registration.Email
}
};
foreach (var attribute in registration.Attributes)
{
document.Metadata[$"attr.{attribute.Key}"] = attribute.Value;
}
await collection.InsertOneAsync(document, cancellationToken: cancellationToken).ConfigureAwait(false);
}
private IReadOnlyDictionary<string, IReadOnlyCollection<string>> BuildBootstrapAttributes(
AuthorityUserRegistration registration,
string normalizedUsername,
LdapBootstrapOptions bootstrapOptions)
{
var attributes = new Dictionary<string, IReadOnlyCollection<string>>(StringComparer.OrdinalIgnoreCase)
{
["objectClass"] = bootstrapOptions.ObjectClasses,
[bootstrapOptions.RdnAttribute] = new[] { normalizedUsername }
};
if (!string.Equals(bootstrapOptions.UsernameAttribute, bootstrapOptions.RdnAttribute, StringComparison.OrdinalIgnoreCase))
{
attributes[bootstrapOptions.UsernameAttribute] = new[] { normalizedUsername };
}
var displayName = string.IsNullOrWhiteSpace(registration.DisplayName)
? normalizedUsername
: registration.DisplayName!.Trim();
attributes[bootstrapOptions.DisplayNameAttribute] = new[] { displayName };
var (givenName, surname) = DeriveNameParts(displayName, normalizedUsername);
attributes[bootstrapOptions.GivenNameAttribute] = new[] { givenName };
attributes[bootstrapOptions.SurnameAttribute] = new[] { surname };
if (!string.IsNullOrWhiteSpace(bootstrapOptions.EmailAttribute) && !string.IsNullOrWhiteSpace(registration.Email))
{
attributes[bootstrapOptions.EmailAttribute!] = new[] { registration.Email!.Trim() };
}
if (!string.IsNullOrWhiteSpace(bootstrapOptions.SecretAttribute) && !string.IsNullOrWhiteSpace(registration.Password))
{
attributes[bootstrapOptions.SecretAttribute!] = new[] { registration.Password! };
}
foreach (var staticAttribute in bootstrapOptions.StaticAttributes)
{
var resolved = ResolveBootstrapPlaceholder(staticAttribute.Value, normalizedUsername, displayName);
if (!string.IsNullOrWhiteSpace(resolved))
{
attributes[staticAttribute.Key] = new[] { resolved };
}
}
return attributes;
}
private static (string GivenName, string Surname) DeriveNameParts(string displayName, string fallback)
{
var parts = displayName
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 0)
{
return (fallback, fallback);
}
if (parts.Length == 1)
{
return (parts[0], parts[0]);
}
return (parts[0], parts[^1]);
}
private static string ResolveBootstrapPlaceholder(string value, string username, string displayName)
=> value
.Replace("{username}", username, StringComparison.OrdinalIgnoreCase)
.Replace("{displayName}", displayName, StringComparison.OrdinalIgnoreCase);
private static string BuildBootstrapDistinguishedName(string username, LdapBootstrapOptions options)
{
var escaped = LdapDistinguishedNameHelper.EscapeRdnValue(username);
return $"{options.RdnAttribute}={escaped},{options.ContainerDn}";
}
private static IReadOnlyDictionary<string, IReadOnlyList<string>> ConvertAttributes(
IReadOnlyDictionary<string, IReadOnlyCollection<string>> attributes)
{
var converted = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in attributes)
{
converted[pair.Key] = pair.Value.ToList();
}
return converted;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -22,7 +23,9 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
private readonly LdapClientProvisioningStore clientProvisioningStore;
private readonly ILogger<LdapIdentityProviderPlugin> logger;
private readonly AuthorityIdentityProviderCapabilities capabilities;
private readonly bool supportsClientProvisioning;
private readonly bool clientProvisioningActive;
private readonly bool bootstrapActive;
private readonly LdapCapabilityProbe capabilityProbe;
public LdapIdentityProviderPlugin(
AuthorityPluginContext pluginContext,
@@ -41,9 +44,12 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
this.clientProvisioningStore = clientProvisioningStore ?? throw new ArgumentNullException(nameof(clientProvisioningStore));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
capabilityProbe = new LdapCapabilityProbe(pluginContext.Manifest.Name, connectionFactory, logger);
var manifestCapabilities = AuthorityIdentityProviderCapabilities.FromCapabilities(pluginContext.Manifest.Capabilities);
var provisioningOptions = optionsMonitor.Get(pluginContext.Manifest.Name).ClientProvisioning;
supportsClientProvisioning = manifestCapabilities.SupportsClientProvisioning && provisioningOptions.Enabled;
var pluginOptions = optionsMonitor.Get(pluginContext.Manifest.Name);
var provisioningOptions = pluginOptions.ClientProvisioning;
var bootstrapOptions = pluginOptions.Bootstrap;
if (manifestCapabilities.SupportsClientProvisioning && !provisioningOptions.Enabled)
{
@@ -52,18 +58,47 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
pluginContext.Manifest.Name);
}
if (manifestCapabilities.SupportsBootstrap)
if (manifestCapabilities.SupportsBootstrap && !bootstrapOptions.Enabled)
{
this.logger.LogInformation(
"LDAP plugin '{PluginName}' manifest declares bootstrap capability, but it is not implemented yet. Capability will be advertised as false.",
this.logger.LogWarning(
"LDAP plugin '{PluginName}' manifest declares bootstrap capability, but configuration disabled it. Capability will be advertised as false.",
pluginContext.Manifest.Name);
}
var snapshot = LdapCapabilitySnapshotCache.GetOrAdd(
pluginContext.Manifest.Name,
() => capabilityProbe.Evaluate(
pluginOptions,
manifestCapabilities.SupportsClientProvisioning && provisioningOptions.Enabled,
manifestCapabilities.SupportsBootstrap && bootstrapOptions.Enabled));
clientProvisioningActive = manifestCapabilities.SupportsClientProvisioning
&& provisioningOptions.Enabled
&& snapshot.ClientProvisioningWritable;
bootstrapActive = manifestCapabilities.SupportsBootstrap
&& bootstrapOptions.Enabled
&& snapshot.BootstrapWritable;
if (manifestCapabilities.SupportsClientProvisioning && provisioningOptions.Enabled && !clientProvisioningActive)
{
this.logger.LogWarning(
"LDAP plugin '{PluginName}' degraded client provisioning capability because LDAP write permissions could not be validated.",
pluginContext.Manifest.Name);
}
if (manifestCapabilities.SupportsBootstrap && bootstrapOptions.Enabled && !bootstrapActive)
{
this.logger.LogWarning(
"LDAP plugin '{PluginName}' degraded bootstrap capability because LDAP write permissions could not be validated.",
pluginContext.Manifest.Name);
}
capabilities = new AuthorityIdentityProviderCapabilities(
SupportsPassword: true,
SupportsMfa: manifestCapabilities.SupportsMfa,
SupportsClientProvisioning: supportsClientProvisioning,
SupportsBootstrap: false);
SupportsClientProvisioning: clientProvisioningActive,
SupportsBootstrap: bootstrapActive);
}
public string Name => pluginContext.Manifest.Name;
@@ -76,7 +111,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
public IClaimsEnricher ClaimsEnricher => claimsEnricher;
public IClientProvisioningStore? ClientProvisioning => supportsClientProvisioning ? clientProvisioningStore : null;
public IClientProvisioningStore? ClientProvisioning => clientProvisioningActive ? clientProvisioningStore : null;
public AuthorityIdentityProviderCapabilities Capabilities => capabilities;
@@ -93,6 +128,29 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
await connection.BindAsync(options.Connection.BindDn!, secret, cancellationToken).ConfigureAwait(false);
}
var degradeReasons = new List<string>();
var latestOptions = optionsMonitor.Get(Name);
if (latestOptions.ClientProvisioning.Enabled && !clientProvisioningActive)
{
degradeReasons.Add("clientProvisioningDisabled");
}
if (latestOptions.Bootstrap.Enabled && !bootstrapActive)
{
degradeReasons.Add("bootstrapDisabled");
}
if (degradeReasons.Count > 0)
{
return AuthorityPluginHealthResult.Degraded(
"One or more LDAP write capabilities are unavailable.",
new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["capabilities"] = string.Join(',', degradeReasons)
});
}
return AuthorityPluginHealthResult.Healthy();
}
catch (LdapAuthenticationException ex)

View File

@@ -50,7 +50,9 @@ internal sealed class LdapPluginRegistrar : IAuthorityPluginRegistrar
sp.GetRequiredService<IOptionsMonitor<LdapPluginOptions>>(),
sp.GetRequiredService<ILdapConnectionFactory>(),
sp.GetRequiredService<ILogger<LdapCredentialStore>>(),
sp.GetRequiredService<LdapMetrics>()));
sp.GetRequiredService<LdapMetrics>(),
sp.GetRequiredService<IMongoDatabase>(),
ResolveTimeProvider(sp)));
context.Services.AddScoped(sp => new LdapClientProvisioningStore(
pluginName,