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:
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user