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();

View File

@@ -30,14 +30,25 @@ public sealed class RancherHubConnectorTests
var sink = new InMemoryRawSink();
var context = fixture.CreateContext(sink);
var documents = await CollectAsync(fixture.Connector.FetchAsync(context, CancellationToken.None));
documents.Should().HaveCount(1);
var document = documents[0];
document.Digest.Should().Be(fixture.ExpectedDocumentDigest);
document.Metadata.Should().ContainKey("rancher.event.id").WhoseValue.Should().Be("evt-1");
document.Metadata.Should().ContainKey("rancher.event.cursor").WhoseValue.Should().Be("cursor-2");
sink.Documents.Should().HaveCount(1);
var documents = await CollectAsync(fixture.Connector.FetchAsync(context, CancellationToken.None));
documents.Should().HaveCount(1);
var document = documents[0];
document.Digest.Should().Be(fixture.ExpectedDocumentDigest);
document.Metadata.Should().ContainKey("rancher.event.id").WhoseValue.Should().Be("evt-1");
document.Metadata.Should().ContainKey("rancher.event.cursor").WhoseValue.Should().Be("cursor-2");
document.Metadata.Should().Contain("vex.provenance.provider", "excititor:suse.rancher");
document.Metadata.Should().Contain("vex.provenance.providerName", "SUSE Rancher VEX Hub");
document.Metadata.Should().Contain("vex.provenance.providerKind", "hub");
document.Metadata.Should().Contain("vex.provenance.trust.weight", "0.42");
document.Metadata.Should().Contain("vex.provenance.trust.tier", "hub");
document.Metadata.Should().Contain("vex.provenance.trust.note", "tier=hub;weight=0.42");
document.Metadata.Should().Contain("vex.provenance.cosign.issuer", "https://issuer.testsuse.example");
document.Metadata.Should().Contain("vex.provenance.cosign.identityPattern", "spiffe://rancher-vex/*");
document.Metadata.Should().Contain(
"vex.provenance.pgp.fingerprints",
"11223344556677889900AABBCCDDEEFF00112233,AABBCCDDEEFF00112233445566778899AABBCCDD");
sink.Documents.Should().HaveCount(1);
var state = fixture.StateRepository.State;
state.Should().NotBeNull();
@@ -60,12 +71,15 @@ public sealed class RancherHubConnectorTests
var documents = await CollectAsync(fixture.Connector.FetchAsync(context, CancellationToken.None));
documents.Should().BeEmpty();
sink.Documents.Should().HaveCount(1);
var quarantined = sink.Documents[0];
quarantined.Metadata.Should().Contain("rancher.event.quarantine", "true");
quarantined.Metadata.Should().ContainKey("rancher.event.error").WhoseValue.Should().Contain("document fetch failed");
var state = fixture.StateRepository.State;
sink.Documents.Should().HaveCount(1);
var quarantined = sink.Documents[0];
quarantined.Metadata.Should().Contain("rancher.event.quarantine", "true");
quarantined.Metadata.Should().ContainKey("rancher.event.error").WhoseValue.Should().Contain("document fetch failed");
quarantined.Metadata.Should().Contain("vex.provenance.provider", "excititor:suse.rancher");
quarantined.Metadata.Should().Contain("vex.provenance.trust.weight", "0.42");
quarantined.Metadata.Should().Contain("vex.provenance.trust.tier", "hub");
var state = fixture.StateRepository.State;
state.Should().NotBeNull();
state!.DocumentDigests.Should().Contain(d => d.StartsWith("quarantine:", StringComparison.Ordinal));
}
@@ -265,11 +279,16 @@ public sealed class RancherHubConnectorTests
TimeProvider.System,
validators);
var settingsValues = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.OrdinalIgnoreCase);
settingsValues["DiscoveryUri"] = "https://hub.test/.well-known/rancher-hub.json";
settingsValues["OfflineSnapshotPath"] = discoveryPath;
settingsValues["PreferOfflineSnapshot"] = "true";
var settings = new VexConnectorSettings(settingsValues.ToImmutable());
var settingsValues = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.OrdinalIgnoreCase);
settingsValues["DiscoveryUri"] = "https://hub.test/.well-known/rancher-hub.json";
settingsValues["OfflineSnapshotPath"] = discoveryPath;
settingsValues["PreferOfflineSnapshot"] = "true";
settingsValues["TrustWeight"] = "0.42";
settingsValues["CosignIssuer"] = "https://issuer.testsuse.example";
settingsValues["CosignIdentityPattern"] = "spiffe://rancher-vex/*";
settingsValues["PgpFingerprints:0"] = "AABBCCDDEEFF00112233445566778899AABBCCDD";
settingsValues["PgpFingerprints:1"] = "11223344556677889900AABBCCDDEEFF00112233";
var settings = new VexConnectorSettings(settingsValues.ToImmutable());
await connector.ValidateAsync(settings, CancellationToken.None).ConfigureAwait(false);
var services = new ServiceCollection().BuildServiceProvider();

View File

@@ -57,11 +57,21 @@ public sealed class UbuntuCsafConnectorTests
NullLogger<UbuntuCsafConnector>.Instance,
TimeProvider.System);
var settings = new VexConnectorSettings(ImmutableDictionary<string, string>.Empty);
await connector.ValidateAsync(settings, CancellationToken.None);
var sink = new InMemoryRawSink();
var context = new VexConnectorContext(null, VexConnectorSettings.Empty, sink, new NoopSignatureVerifier(), new NoopNormalizerRouter(), new ServiceCollection().BuildServiceProvider(), ImmutableDictionary<string, string>.Empty);
var settings = BuildConnectorSettings(indexUri, trustWeight: 0.63, trustTier: "distro-trusted",
fingerprints: new[]
{
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
});
await connector.ValidateAsync(settings, CancellationToken.None);
var providerStore = new InMemoryProviderStore();
var services = new ServiceCollection()
.AddSingleton<IVexProviderStore>(providerStore)
.BuildServiceProvider();
var sink = new InMemoryRawSink();
var context = new VexConnectorContext(null, VexConnectorSettings.Empty, sink, new NoopSignatureVerifier(), new NoopNormalizerRouter(), services, ImmutableDictionary<string, string>.Empty);
var documents = new List<VexRawDocument>();
await foreach (var doc in connector.FetchAsync(context, CancellationToken.None))
@@ -71,10 +81,18 @@ public sealed class UbuntuCsafConnectorTests
documents.Should().HaveCount(1);
sink.Documents.Should().HaveCount(1);
var stored = sink.Documents.Single();
stored.Digest.Should().Be($"sha256:{documentSha}");
stored.Metadata.TryGetValue("ubuntu.etag", out var storedEtag).Should().BeTrue();
storedEtag.Should().Be("etag-123");
var stored = sink.Documents.Single();
stored.Digest.Should().Be($"sha256:{documentSha}");
stored.Metadata.Should().Contain("ubuntu.etag", "etag-123");
stored.Metadata.Should().Contain("vex.provenance.provider", "excititor:ubuntu");
stored.Metadata.Should().Contain("vex.provenance.providerName", "Ubuntu CSAF");
stored.Metadata.Should().Contain("vex.provenance.providerKind", "distro");
stored.Metadata.Should().Contain("vex.provenance.trust.weight", "0.63");
stored.Metadata.Should().Contain("vex.provenance.trust.tier", "distro-trusted");
stored.Metadata.Should().Contain("vex.provenance.trust.note", "tier=distro-trusted;weight=0.63");
stored.Metadata.Should().Contain(
"vex.provenance.pgp.fingerprints",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
stateRepository.CurrentState.Should().NotBeNull();
stateRepository.CurrentState!.DocumentDigests.Should().Contain($"sha256:{documentSha}");
@@ -94,8 +112,17 @@ public sealed class UbuntuCsafConnectorTests
documents.Should().BeEmpty();
sink.Documents.Should().BeEmpty();
handler.DocumentRequestCount.Should().Be(2);
handler.SeenIfNoneMatch.Should().Contain("\"etag-123\"");
handler.DocumentRequestCount.Should().Be(2);
handler.SeenIfNoneMatch.Should().Contain("\"etag-123\"");
providerStore.SavedProviders.Should().ContainSingle();
var savedProvider = providerStore.SavedProviders.Single();
savedProvider.Trust.Weight.Should().Be(0.63);
savedProvider.Trust.PgpFingerprints.Should().Contain(new[]
{
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
});
}
[Fact]
@@ -127,10 +154,16 @@ public sealed class UbuntuCsafConnectorTests
NullLogger<UbuntuCsafConnector>.Instance,
TimeProvider.System);
await connector.ValidateAsync(new VexConnectorSettings(ImmutableDictionary<string, string>.Empty), CancellationToken.None);
var sink = new InMemoryRawSink();
var context = new VexConnectorContext(null, VexConnectorSettings.Empty, sink, new NoopSignatureVerifier(), new NoopNormalizerRouter(), new ServiceCollection().BuildServiceProvider(), ImmutableDictionary<string, string>.Empty);
var settings = BuildConnectorSettings(indexUri);
await connector.ValidateAsync(settings, CancellationToken.None);
var providerStore = new InMemoryProviderStore();
var services = new ServiceCollection()
.AddSingleton<IVexProviderStore>(providerStore)
.BuildServiceProvider();
var sink = new InMemoryRawSink();
var context = new VexConnectorContext(null, VexConnectorSettings.Empty, sink, new NoopSignatureVerifier(), new NoopNormalizerRouter(), services, ImmutableDictionary<string, string>.Empty);
var documents = new List<VexRawDocument>();
await foreach (var doc in connector.FetchAsync(context, CancellationToken.None))
@@ -142,9 +175,29 @@ public sealed class UbuntuCsafConnectorTests
sink.Documents.Should().BeEmpty();
stateRepository.CurrentState.Should().NotBeNull();
stateRepository.CurrentState!.DocumentDigests.Should().BeEmpty();
handler.DocumentRequestCount.Should().Be(1);
}
handler.DocumentRequestCount.Should().Be(1);
providerStore.SavedProviders.Should().ContainSingle();
}
private static VexConnectorSettings BuildConnectorSettings(Uri indexUri, double trustWeight = 0.75, string trustTier = "distro", string[]? fingerprints = null)
{
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.OrdinalIgnoreCase);
builder["IndexUri"] = indexUri.ToString();
builder["Channels:0"] = "stable";
builder["TrustWeight"] = trustWeight.ToString(CultureInfo.InvariantCulture);
builder["TrustTier"] = trustTier;
if (fingerprints is not null)
{
for (var i = 0; i < fingerprints.Length; i++)
{
builder[$"PgpFingerprints:{i}"] = fingerprints[i];
}
}
return new VexConnectorSettings(builder.ToImmutable());
}
private static (string IndexJson, string CatalogJson) CreateTestManifest(Uri advisoryUri, string advisoryId, string timestamp)
{
var indexJson = """
@@ -285,16 +338,42 @@ public sealed class UbuntuCsafConnectorTests
}
}
private sealed class InMemoryRawSink : IVexRawDocumentSink
{
public List<VexRawDocument> Documents { get; } = new();
public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken)
{
Documents.Add(document);
return ValueTask.CompletedTask;
}
}
private sealed class InMemoryRawSink : IVexRawDocumentSink
{
public List<VexRawDocument> Documents { get; } = new();
public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken)
{
Documents.Add(document);
return ValueTask.CompletedTask;
}
}
private sealed class InMemoryProviderStore : IVexProviderStore
{
public List<VexProvider> SavedProviders { get; } = new();
public ValueTask<VexProvider?> FindAsync(string id, CancellationToken cancellationToken, IClientSessionHandle? session = null)
=> ValueTask.FromResult(SavedProviders.LastOrDefault(provider => provider.Id == id));
public ValueTask<IReadOnlyCollection<VexProvider>> ListAsync(CancellationToken cancellationToken, IClientSessionHandle? session = null)
=> ValueTask.FromResult<IReadOnlyCollection<VexProvider>>(SavedProviders.ToList());
public ValueTask SaveAsync(VexProvider provider, CancellationToken cancellationToken, IClientSessionHandle? session = null)
{
var existingIndex = SavedProviders.FindIndex(p => p.Id == provider.Id);
if (existingIndex >= 0)
{
SavedProviders[existingIndex] = provider;
}
else
{
SavedProviders.Add(provider);
}
return ValueTask.CompletedTask;
}
}
private sealed class NoopSignatureVerifier : IVexSignatureVerifier
{