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

@@ -1,22 +1,25 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Authentication;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Configuration;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Events;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Metadata;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.State;
using StellaOps.Excititor.Core;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Authentication;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Configuration;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Events;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.Metadata;
using StellaOps.Excititor.Connectors.SUSE.RancherVEXHub.State;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
namespace StellaOps.Excititor.Connectors.SUSE.RancherVEXHub;
@@ -88,12 +91,14 @@ public sealed class RancherHubConnector : VexConnectorBase
throw new InvalidOperationException("Connector must be validated before fetch operations.");
}
if (_metadata is null)
{
_metadata = await _metadataLoader.LoadAsync(_options, cancellationToken).ConfigureAwait(false);
}
var checkpoint = await _checkpointManager.LoadAsync(Descriptor.Id, context, cancellationToken).ConfigureAwait(false);
if (_metadata is null)
{
_metadata = await _metadataLoader.LoadAsync(_options, cancellationToken).ConfigureAwait(false);
}
await UpsertProviderAsync(context.Services, _metadata.Metadata.Provider, cancellationToken).ConfigureAwait(false);
var checkpoint = await _checkpointManager.LoadAsync(Descriptor.Id, context, cancellationToken).ConfigureAwait(false);
var digestHistory = checkpoint.Digests.ToList();
var dedupeSet = new HashSet<string>(checkpoint.Digests, StringComparer.OrdinalIgnoreCase);
var latestCursor = checkpoint.Cursor;
@@ -210,14 +215,19 @@ public sealed class RancherHubConnector : VexConnectorBase
var contentBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
var publishedAt = record.PublishedAt ?? UtcNow();
var metadata = BuildMetadata(builder => builder
.Add("rancher.event.id", record.Id)
.Add("rancher.event.type", record.Type)
.Add("rancher.event.channel", record.Channel)
.Add("rancher.event.published", publishedAt)
.Add("rancher.event.cursor", batch.NextCursor ?? batch.Cursor)
.Add("rancher.event.offline", batch.FromOfflineSnapshot ? "true" : "false")
.Add("rancher.event.declaredDigest", record.DocumentDigest));
var metadata = BuildMetadata(builder =>
{
builder
.Add("rancher.event.id", record.Id)
.Add("rancher.event.type", record.Type)
.Add("rancher.event.channel", record.Channel)
.Add("rancher.event.published", publishedAt)
.Add("rancher.event.cursor", batch.NextCursor ?? batch.Cursor)
.Add("rancher.event.offline", batch.FromOfflineSnapshot ? "true" : "false")
.Add("rancher.event.declaredDigest", record.DocumentDigest);
AddProvenanceMetadata(builder);
});
var format = ResolveFormat(record.DocumentFormat);
var document = CreateRawDocument(format, record.DocumentUri, contentBytes, metadata);
@@ -240,14 +250,48 @@ public sealed class RancherHubConnector : VexConnectorBase
}
digestHistory.Add(document.Digest);
await context.RawSink.StoreAsync(document, cancellationToken).ConfigureAwait(false);
return new EventProcessingResult(document, false, publishedAt);
}
private static bool TrimHistory(List<string> digestHistory)
{
if (digestHistory.Count <= MaxDigestHistory)
{
await context.RawSink.StoreAsync(document, cancellationToken).ConfigureAwait(false);
return new EventProcessingResult(document, false, publishedAt);
}
private void AddProvenanceMetadata(VexConnectorMetadataBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
var provider = _metadata?.Metadata.Provider;
if (provider is null)
{
return;
}
builder
.Add("vex.provenance.provider", provider.Id)
.Add("vex.provenance.providerName", provider.DisplayName)
.Add("vex.provenance.providerKind", provider.Kind.ToString().ToLowerInvariant(CultureInfo.InvariantCulture))
.Add("vex.provenance.trust.weight", provider.Trust.Weight.ToString("0.###", CultureInfo.InvariantCulture));
if (provider.Trust.Cosign is { } cosign)
{
builder
.Add("vex.provenance.cosign.issuer", cosign.Issuer)
.Add("vex.provenance.cosign.identityPattern", cosign.IdentityPattern);
}
if (!provider.Trust.PgpFingerprints.IsDefaultOrEmpty && provider.Trust.PgpFingerprints.Length > 0)
{
builder.Add("vex.provenance.pgp.fingerprints", string.Join(',', provider.Trust.PgpFingerprints));
}
var tier = provider.Kind.ToString().ToLowerInvariant(CultureInfo.InvariantCulture);
builder
.Add("vex.provenance.trust.tier", tier)
.Add("vex.provenance.trust.note", $"tier={tier};weight={provider.Trust.Weight.ToString("0.###", CultureInfo.InvariantCulture)}");
}
private static bool TrimHistory(List<string> digestHistory)
{
if (digestHistory.Count <= MaxDigestHistory)
{
return false;
}
@@ -259,34 +303,55 @@ public sealed class RancherHubConnector : VexConnectorBase
private async Task<HttpRequestMessage> CreateDocumentRequestAsync(Uri documentUri, CancellationToken cancellationToken)
{
var request = new HttpRequestMessage(HttpMethod.Get, documentUri);
if (_metadata?.Metadata.Subscription.RequiresAuthentication ?? false)
{
var token = await _tokenProvider.GetAccessTokenAsync(_options!, cancellationToken).ConfigureAwait(false);
if (token is not null)
{
var scheme = string.IsNullOrWhiteSpace(token.TokenType) ? "Bearer" : token.TokenType;
request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token.Value);
}
}
return request;
}
private async Task QuarantineAsync(
RancherHubEventRecord record,
RancherHubEventBatch batch,
string reason,
if (_metadata?.Metadata.Subscription.RequiresAuthentication ?? false)
{
var token = await _tokenProvider.GetAccessTokenAsync(_options!, cancellationToken).ConfigureAwait(false);
if (token is not null)
{
var scheme = string.IsNullOrWhiteSpace(token.TokenType) ? "Bearer" : token.TokenType;
request.Headers.Authorization = new AuthenticationHeaderValue(scheme, token.Value);
}
}
return request;
}
private static async ValueTask UpsertProviderAsync(IServiceProvider services, VexProvider provider, CancellationToken cancellationToken)
{
if (services is null)
{
return;
}
var store = services.GetService<IVexProviderStore>();
if (store is null)
{
return;
}
await store.SaveAsync(provider, cancellationToken).ConfigureAwait(false);
}
private async Task QuarantineAsync(
RancherHubEventRecord record,
RancherHubEventBatch batch,
string reason,
VexConnectorContext context,
CancellationToken cancellationToken)
{
var metadata = BuildMetadata(builder => builder
.Add("rancher.event.id", record.Id)
.Add("rancher.event.type", record.Type)
.Add("rancher.event.channel", record.Channel)
.Add("rancher.event.quarantine", "true")
.Add("rancher.event.error", reason)
.Add("rancher.event.cursor", batch.NextCursor ?? batch.Cursor)
.Add("rancher.event.offline", batch.FromOfflineSnapshot ? "true" : "false"));
var metadata = BuildMetadata(builder =>
{
builder
.Add("rancher.event.id", record.Id)
.Add("rancher.event.type", record.Type)
.Add("rancher.event.channel", record.Channel)
.Add("rancher.event.quarantine", "true")
.Add("rancher.event.error", reason)
.Add("rancher.event.cursor", batch.NextCursor ?? batch.Cursor)
.Add("rancher.event.offline", batch.FromOfflineSnapshot ? "true" : "false");
AddProvenanceMetadata(builder);
});
var sourceUri = record.DocumentUri ?? _metadata?.Metadata.Subscription.EventsUri ?? _options!.DiscoveryUri;
var payload = Encoding.UTF8.GetBytes(record.RawJson);

View File

@@ -32,10 +32,35 @@ public sealed class UbuntuConnectorOptions
/// Optional file path for offline index snapshot.
/// </summary>
public string? OfflineSnapshotPath { get; set; }
/// <summary>
/// Controls persistence of network responses to <see cref="OfflineSnapshotPath"/>.
/// </summary>
public bool PersistOfflineSnapshot { get; set; } = true;
/// <summary>
/// Controls persistence of network responses to <see cref="OfflineSnapshotPath"/>.
/// </summary>
public bool PersistOfflineSnapshot { get; set; } = true;
/// <summary>
/// Weight applied to Ubuntu-sourced statements during trust evaluation.
/// </summary>
public double TrustWeight { get; set; } = 0.75;
/// <summary>
/// Optional cosign issuer enforcing Ubuntu CSAF signatures.
/// </summary>
public string? CosignIssuer { get; set; }
/// <summary>
/// Cosign identity pattern matching Ubuntu CSAF log entries.
/// </summary>
public string? CosignIdentityPattern { get; set; }
/// <summary>
/// Trusted Ubuntu CSAF GPG fingerprints.
/// </summary>
public IList<string> PgpFingerprints { get; } = new List<string>();
/// <summary>
/// Friendly trust tier label surfaced in provenance metadata.
/// </summary>
public string TrustTier { get; set; } = "distro";
public void Validate(IFileSystem? fileSystem = null)
{
@@ -77,14 +102,45 @@ public sealed class UbuntuConnectorOptions
throw new InvalidOperationException("OfflineSnapshotPath must be provided when PreferOfflineSnapshot is enabled.");
}
if (!string.IsNullOrWhiteSpace(OfflineSnapshotPath))
{
var fs = fileSystem ?? new FileSystem();
var directory = Path.GetDirectoryName(OfflineSnapshotPath);
if (!string.IsNullOrWhiteSpace(directory) && !fs.Directory.Exists(directory))
{
fs.Directory.CreateDirectory(directory);
}
}
}
}
if (!string.IsNullOrWhiteSpace(OfflineSnapshotPath))
{
var fs = fileSystem ?? new FileSystem();
var directory = Path.GetDirectoryName(OfflineSnapshotPath);
if (!string.IsNullOrWhiteSpace(directory) && !fs.Directory.Exists(directory))
{
fs.Directory.CreateDirectory(directory);
}
}
if (double.IsNaN(TrustWeight) || double.IsInfinity(TrustWeight))
{
TrustWeight = 0.75;
}
else if (TrustWeight <= 0)
{
TrustWeight = 0.1;
}
else if (TrustWeight > 1.0)
{
TrustWeight = 1.0;
}
if (!string.IsNullOrWhiteSpace(CosignIssuer) && string.IsNullOrWhiteSpace(CosignIdentityPattern))
{
throw new InvalidOperationException("CosignIdentityPattern must be provided when CosignIssuer is specified.");
}
for (var i = PgpFingerprints.Count - 1; i >= 0; i--)
{
if (string.IsNullOrWhiteSpace(PgpFingerprints[i]))
{
PgpFingerprints.RemoveAt(i);
}
}
if (string.IsNullOrWhiteSpace(TrustTier))
{
TrustTier = "distro";
}
}
}

View File

@@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.Ubuntu.CSAF.Configuration;
@@ -34,6 +36,7 @@ public sealed class UbuntuCsafConnector : VexConnectorBase
private UbuntuConnectorOptions? _options;
private UbuntuCatalogResult? _catalog;
private VexProvider? _provider;
public UbuntuCsafConnector(
UbuntuCatalogLoader catalogLoader,
@@ -58,6 +61,8 @@ public sealed class UbuntuCsafConnector : VexConnectorBase
validators: _validators);
_catalog = await _catalogLoader.LoadAsync(_options, cancellationToken).ConfigureAwait(false);
_provider = BuildProvider(_options, _catalog);
LogConnectorEvent(LogLevel.Information, "validate", "Ubuntu CSAF index loaded.", new Dictionary<string, object?>
{
["channelCount"] = _catalog.Metadata.Channels.Length,
@@ -79,6 +84,13 @@ public sealed class UbuntuCsafConnector : VexConnectorBase
_catalog = await _catalogLoader.LoadAsync(_options, cancellationToken).ConfigureAwait(false);
}
if (_provider is null)
{
_provider = BuildProvider(_options!, _catalog);
}
await UpsertProviderAsync(context.Services, _provider, cancellationToken).ConfigureAwait(false);
var state = await _stateRepository.GetAsync(Descriptor.Id, cancellationToken).ConfigureAwait(false);
var knownTokens = state?.DocumentDigests ?? ImmutableArray<string>.Empty;
var digestSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
@@ -334,42 +346,44 @@ public sealed class UbuntuCsafConnector : VexConnectorBase
? Unquote(etagHeader!)
: entry.ETag is null ? null : Unquote(entry.ETag);
var metadata = BuildMetadata(builder =>
{
builder.Add("ubuntu.channel", entry.Channel);
builder.Add("ubuntu.uri", entry.DocumentUri.ToString());
if (!string.IsNullOrWhiteSpace(entry.AdvisoryId))
var metadata = BuildMetadata(builder =>
{
builder.Add("ubuntu.advisoryId", entry.AdvisoryId);
}
builder.Add("ubuntu.channel", entry.Channel);
builder.Add("ubuntu.uri", entry.DocumentUri.ToString());
if (!string.IsNullOrWhiteSpace(entry.AdvisoryId))
{
builder.Add("ubuntu.advisoryId", entry.AdvisoryId);
}
if (!string.IsNullOrWhiteSpace(entry.Title))
{
builder.Add("ubuntu.title", entry.Title!);
}
if (!string.IsNullOrWhiteSpace(entry.Title))
{
builder.Add("ubuntu.title", entry.Title!);
}
if (!string.IsNullOrWhiteSpace(entry.Version))
{
builder.Add("ubuntu.version", entry.Version!);
}
if (!string.IsNullOrWhiteSpace(entry.Version))
{
builder.Add("ubuntu.version", entry.Version!);
}
if (entry.LastModified is { } modified)
{
builder.Add("ubuntu.lastModified", modified.ToString("O"));
}
if (entry.LastModified is { } modified)
{
builder.Add("ubuntu.lastModified", modified.ToString("O"));
}
if (entry.Sha256 is not null)
{
builder.Add("ubuntu.sha256", NormalizeDigest(entry.Sha256));
}
if (entry.Sha256 is not null)
{
builder.Add("ubuntu.sha256", NormalizeDigest(entry.Sha256));
}
if (!string.IsNullOrWhiteSpace(etagValue))
{
builder.Add("ubuntu.etag", etagValue!);
}
});
if (!string.IsNullOrWhiteSpace(etagValue))
{
builder.Add("ubuntu.etag", etagValue!);
}
var document = CreateRawDocument(VexDocumentFormat.Csaf, entry.DocumentUri, payload, metadata);
AddProvenanceMetadata(builder);
});
var document = CreateRawDocument(VexDocumentFormat.Csaf, entry.DocumentUri, payload, metadata);
return new DownloadResult(document, etagValue);
}
catch (Exception ex) when (ex is not OperationCanceledException)
@@ -386,6 +400,83 @@ public sealed class UbuntuCsafConnector : VexConnectorBase
}
}
private VexProvider BuildProvider(UbuntuConnectorOptions options, UbuntuCatalogResult? catalog)
{
var baseUris = new List<Uri> { options.IndexUri };
if (catalog?.Metadata.Channels is { Length: > 0 })
{
baseUris.AddRange(catalog.Metadata.Channels.Select(channel => channel.CatalogUri));
}
VexCosignTrust? cosign = null;
if (!string.IsNullOrWhiteSpace(options.CosignIssuer) && !string.IsNullOrWhiteSpace(options.CosignIdentityPattern))
{
cosign = new VexCosignTrust(options.CosignIssuer!, options.CosignIdentityPattern!);
}
var trust = new VexProviderTrust(options.TrustWeight, cosign, options.PgpFingerprints);
return new VexProvider(
Descriptor.Id,
Descriptor.DisplayName,
Descriptor.Kind,
baseUris,
new VexProviderDiscovery(options.IndexUri, null),
trust);
}
private void AddProvenanceMetadata(VexConnectorMetadataBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
var provider = _provider;
if (provider is null)
{
return;
}
builder
.Add("vex.provenance.provider", provider.Id)
.Add("vex.provenance.providerName", provider.DisplayName)
.Add("vex.provenance.providerKind", provider.Kind.ToString().ToLowerInvariant(CultureInfo.InvariantCulture))
.Add("vex.provenance.trust.weight", provider.Trust.Weight.ToString("0.###", CultureInfo.InvariantCulture));
if (provider.Trust.Cosign is { } cosign)
{
builder
.Add("vex.provenance.cosign.issuer", cosign.Issuer)
.Add("vex.provenance.cosign.identityPattern", cosign.IdentityPattern);
}
if (!provider.Trust.PgpFingerprints.IsDefaultOrEmpty && provider.Trust.PgpFingerprints.Length > 0)
{
builder.Add("vex.provenance.pgp.fingerprints", string.Join(',', provider.Trust.PgpFingerprints));
}
var tier = !string.IsNullOrWhiteSpace(_options?.TrustTier)
? _options!.TrustTier!
: provider.Kind.ToString().ToLowerInvariant(CultureInfo.InvariantCulture);
builder
.Add("vex.provenance.trust.tier", tier)
.Add("vex.provenance.trust.note", $"tier={tier};weight={provider.Trust.Weight.ToString("0.###", CultureInfo.InvariantCulture)}");
}
private static async ValueTask UpsertProviderAsync(IServiceProvider services, VexProvider provider, CancellationToken cancellationToken)
{
if (services is null)
{
return;
}
var store = services.GetService<IVexProviderStore>();
if (store is null)
{
return;
}
await store.SaveAsync(provider, cancellationToken).ConfigureAwait(false);
}
private static string NormalizeDigest(string value)
{
var trimmed = value.Trim();