Add Authority Advisory AI and API Lifecycle Configuration

- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings.
- Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations.
- Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration.
- Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options.
- Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations.
- Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client.
- Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
master
2025-11-02 13:40:38 +02:00
parent 66cb6c4b8a
commit f98cea3bcf
516 changed files with 68157 additions and 24754 deletions

View File

@@ -17,6 +17,7 @@ using StellaOps.Excititor.Worker.Scheduling;
using StellaOps.Excititor.Worker.Signature;
using StellaOps.Excititor.Attestation.Extensions;
using StellaOps.Excititor.Attestation.Verification;
using StellaOps.IssuerDirectory.Client;
var builder = Host.CreateApplicationBuilder(args);
var services = builder.Services;
@@ -39,6 +40,15 @@ services.AddOpenVexNormalizer();
services.AddSingleton<IVexSignatureVerifier, WorkerSignatureVerifier>();
services.AddVexAttestation();
services.Configure<VexAttestationVerificationOptions>(configuration.GetSection("Excititor:Attestation:Verification"));
var issuerDirectorySection = configuration.GetSection("Excititor:IssuerDirectory");
if (issuerDirectorySection.Exists())
{
services.AddIssuerDirectoryClient(issuerDirectorySection);
}
else
{
services.AddIssuerDirectoryClient(configuration);
}
services.PostConfigure<VexAttestationVerificationOptions>(options =>
{
// Workers operate in offline-first environments; allow verification to succeed without Rekor.

View File

@@ -1,8 +1,9 @@
using System.Collections.Immutable;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
namespace StellaOps.Excititor.Worker.Signature;
using System.Collections.Immutable;
using System.Globalization;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Storage.Mongo;
namespace StellaOps.Excititor.Worker.Signature;
internal sealed class VerifyingVexRawDocumentSink : IVexRawDocumentSink
{
@@ -59,11 +60,20 @@ internal sealed class VerifyingVexRawDocumentSink : IVexRawDocumentSink
builder["vex.signature.verifiedAt"] = signature.VerifiedAt.Value.ToString("O");
}
if (!string.IsNullOrWhiteSpace(signature.TransparencyLogReference))
{
builder["vex.signature.transparencyLogReference"] = signature.TransparencyLogReference!;
}
return builder.ToImmutable();
}
}
if (!string.IsNullOrWhiteSpace(signature.TransparencyLogReference))
{
builder["vex.signature.transparencyLogReference"] = signature.TransparencyLogReference!;
}
if (signature.Trust is not null)
{
builder["vex.signature.trust.weight"] = signature.Trust.EffectiveWeight.ToString(CultureInfo.InvariantCulture);
builder["vex.signature.trust.tenantId"] = signature.Trust.TenantId;
builder["vex.signature.trust.issuerId"] = signature.Trust.IssuerId;
builder["vex.signature.trust.tenantOverrideApplied"] = signature.Trust.TenantOverrideApplied ? "true" : "false";
builder["vex.signature.trust.retrievedAtUtc"] = signature.Trust.RetrievedAtUtc.ToString("O");
}
return builder.ToImmutable();
}
}

View File

@@ -9,12 +9,13 @@ using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using StellaOps.Aoc;
using StellaOps.Excititor.Attestation.Dsse;
using StellaOps.Excititor.Attestation.Models;
using StellaOps.Excititor.Attestation.Verification;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Aoc;
namespace StellaOps.Excititor.Worker.Signature;
using StellaOps.Excititor.Attestation.Models;
using StellaOps.Excititor.Attestation.Verification;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Aoc;
using StellaOps.IssuerDirectory.Client;
namespace StellaOps.Excititor.Worker.Signature;
/// <summary>
/// Enforces checksum validation and records signature verification metadata.
@@ -26,9 +27,10 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
"ingestion_signature_verified_total",
description: "Counts signature and checksum verification results for Excititor worker ingestion.");
private readonly ILogger<WorkerSignatureVerifier> _logger;
private readonly IVexAttestationVerifier? _attestationVerifier;
private readonly TimeProvider _timeProvider;
private readonly ILogger<WorkerSignatureVerifier> _logger;
private readonly IVexAttestationVerifier? _attestationVerifier;
private readonly TimeProvider _timeProvider;
private readonly IIssuerDirectoryClient? _issuerDirectoryClient;
private static readonly JsonSerializerOptions EnvelopeSerializerOptions = new()
{
@@ -43,15 +45,17 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
};
public WorkerSignatureVerifier(
ILogger<WorkerSignatureVerifier> logger,
IVexAttestationVerifier? attestationVerifier = null,
TimeProvider? timeProvider = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_attestationVerifier = attestationVerifier;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public WorkerSignatureVerifier(
ILogger<WorkerSignatureVerifier> logger,
IVexAttestationVerifier? attestationVerifier = null,
TimeProvider? timeProvider = null,
IIssuerDirectoryClient? issuerDirectoryClient = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_attestationVerifier = attestationVerifier;
_timeProvider = timeProvider ?? TimeProvider.System;
_issuerDirectoryClient = issuerDirectoryClient;
}
public async ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken cancellationToken)
{
@@ -82,13 +86,17 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
VexSignatureMetadata? signatureMetadata = null;
if (document.Format == VexDocumentFormat.OciAttestation && _attestationVerifier is not null)
{
signatureMetadata = await VerifyAttestationAsync(document, metadata, cancellationToken).ConfigureAwait(false);
}
signatureMetadata ??= ExtractSignatureMetadata(metadata);
var resultLabel = signatureMetadata is null ? "skipped" : "ok";
RecordVerification(document.ProviderId, metadata, resultLabel);
{
signatureMetadata = await VerifyAttestationAsync(document, metadata, cancellationToken).ConfigureAwait(false);
}
signatureMetadata ??= ExtractSignatureMetadata(metadata);
if (signatureMetadata is not null)
{
signatureMetadata = await AttachIssuerTrustAsync(signatureMetadata, metadata, cancellationToken).ConfigureAwait(false);
}
var resultLabel = signatureMetadata is null ? "skipped" : "ok";
RecordVerification(document.ProviderId, metadata, resultLabel);
if (resultLabel == "skipped")
{
@@ -322,11 +330,11 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
return "sha256:" + Convert.ToHexString(buffer).ToLowerInvariant();
}
private static VexSignatureMetadata? ExtractSignatureMetadata(ImmutableDictionary<string, string> metadata)
{
if (!metadata.TryGetValue("vex.signature.type", out var type) || string.IsNullOrWhiteSpace(type))
{
return null;
private static VexSignatureMetadata? ExtractSignatureMetadata(ImmutableDictionary<string, string> metadata)
{
if (!metadata.TryGetValue("vex.signature.type", out var type) || string.IsNullOrWhiteSpace(type))
{
return null;
}
metadata.TryGetValue("vex.signature.subject", out var subject);
@@ -341,11 +349,11 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
verifiedAt = parsed;
}
return new VexSignatureMetadata(type, subject, issuer, keyId, verifiedAt, tlog);
}
private static void RecordVerification(string providerId, ImmutableDictionary<string, string> metadata, string result)
{
return new VexSignatureMetadata(type, subject, issuer, keyId, verifiedAt, tlog);
}
private static void RecordVerification(string providerId, ImmutableDictionary<string, string> metadata, string result)
{
var tags = new List<KeyValuePair<string, object?>>(3)
{
new("source", providerId),
@@ -359,6 +367,143 @@ internal sealed class WorkerSignatureVerifier : IVexSignatureVerifier
tags.Add(new KeyValuePair<string, object?>("tenant", tenant));
SignatureVerificationCounter.Add(1, tags.ToArray());
}
}
SignatureVerificationCounter.Add(1, tags.ToArray());
}
private async ValueTask<VexSignatureMetadata> AttachIssuerTrustAsync(
VexSignatureMetadata signature,
ImmutableDictionary<string, string> metadata,
CancellationToken cancellationToken)
{
if (_issuerDirectoryClient is null)
{
return signature;
}
var tenantId = ResolveTenantId(metadata);
var issuerId = ResolveIssuerId(signature, metadata);
var keyId = signature.KeyId;
if (string.IsNullOrWhiteSpace(tenantId) ||
string.IsNullOrWhiteSpace(issuerId) ||
string.IsNullOrWhiteSpace(keyId))
{
return signature;
}
IReadOnlyList<IssuerKeyModel> keys;
try
{
keys = await _issuerDirectoryClient
.GetIssuerKeysAsync(tenantId, issuerId, includeGlobal: true, cancellationToken)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Issuer Directory key lookup failed for issuer {IssuerId} (tenant={TenantId}).",
issuerId,
tenantId);
return signature;
}
var key = keys.FirstOrDefault(k => string.Equals(k.Id, keyId, StringComparison.OrdinalIgnoreCase));
if (key is null)
{
_logger.LogWarning(
"Issuer Directory has no key {KeyId} for issuer {IssuerId} (tenant={TenantId}).",
keyId,
issuerId,
tenantId);
return signature;
}
if (!string.Equals(key.Status, "Active", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning(
"Issuer Directory key {KeyId} for issuer {IssuerId} (tenant={TenantId}) is {Status}; skipping trust enrichment.",
keyId,
issuerId,
tenantId,
key.Status);
return signature;
}
IssuerTrustResponseModel trustResponse;
try
{
trustResponse = await _issuerDirectoryClient
.GetIssuerTrustAsync(tenantId, issuerId, includeGlobal: true, cancellationToken)
.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogWarning(
ex,
"Issuer Directory trust lookup failed for issuer {IssuerId} (tenant={TenantId}).",
issuerId,
tenantId);
return signature;
}
var trust = new VexSignatureTrustMetadata(
trustResponse.EffectiveWeight,
tenantId,
issuerId,
trustResponse.TenantOverride is not null,
_timeProvider.GetUtcNow());
return new VexSignatureMetadata(
signature.Type,
signature.Subject,
signature.Issuer,
signature.KeyId,
signature.VerifiedAt,
signature.TransparencyLogReference,
trust);
}
private static string? ResolveTenantId(ImmutableDictionary<string, string> metadata)
{
if (metadata.TryGetValue("tenant", out var tenant) && !string.IsNullOrWhiteSpace(tenant))
{
return tenant.Trim();
}
if (metadata.TryGetValue("tenantId", out var tenantId) && !string.IsNullOrWhiteSpace(tenantId))
{
return tenantId.Trim();
}
return null;
}
private static string? ResolveIssuerId(VexSignatureMetadata signature, ImmutableDictionary<string, string> metadata)
{
if (!string.IsNullOrWhiteSpace(signature.Issuer))
{
return signature.Issuer;
}
if (metadata.TryGetValue("vex.signature.issuer", out var issuer) && !string.IsNullOrWhiteSpace(issuer))
{
return issuer.Trim();
}
if (metadata.TryGetValue("verification.issuer", out var diagIssuer) && !string.IsNullOrWhiteSpace(diagIssuer))
{
return diagIssuer.Trim();
}
return null;
}
}

View File

@@ -21,5 +21,6 @@
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Formats.CycloneDX/StellaOps.Excititor.Formats.CycloneDX.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Formats.OpenVEX/StellaOps.Excititor.Formats.OpenVEX.csproj" />
<ProjectReference Include="../__Libraries/StellaOps.Excititor.Attestation/StellaOps.Excititor.Attestation.csproj" />
<ProjectReference Include="../../__Libraries/StellaOps.IssuerDirectory.Client/StellaOps.IssuerDirectory.Client.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -99,6 +99,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.WebServ
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Worker.Tests", "__Tests\StellaOps.Excititor.Worker.Tests\StellaOps.Excititor.Worker.Tests.csproj", "{3F51027B-F194-4321-AC7B-E00DA5CD47E3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.IssuerDirectory.Client", "..\__Libraries\StellaOps.IssuerDirectory.Client\StellaOps.IssuerDirectory.Client.csproj", "{E1558326-7169-467B-BB8C-498ACA5DF579}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -661,6 +663,18 @@ Global
{3F51027B-F194-4321-AC7B-E00DA5CD47E3}.Release|x64.Build.0 = Release|Any CPU
{3F51027B-F194-4321-AC7B-E00DA5CD47E3}.Release|x86.ActiveCfg = Release|Any CPU
{3F51027B-F194-4321-AC7B-E00DA5CD47E3}.Release|x86.Build.0 = Release|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Debug|x64.ActiveCfg = Debug|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Debug|x64.Build.0 = Debug|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Debug|x86.ActiveCfg = Debug|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Debug|x86.Build.0 = Debug|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Release|Any CPU.Build.0 = Release|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Release|x64.ActiveCfg = Release|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Release|x64.Build.0 = Release|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Release|x86.ActiveCfg = Release|Any CPU
{E1558326-7169-467B-BB8C-498ACA5DF579}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -1,17 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
</ItemGroup>
</Project>

View File

@@ -184,43 +184,74 @@ public sealed record VexClaimDocument
public VexSignatureMetadata? Signature { get; }
}
public sealed record VexSignatureMetadata
{
public VexSignatureMetadata(
string type,
string? subject = null,
string? issuer = null,
string? keyId = null,
DateTimeOffset? verifiedAt = null,
string? transparencyLogReference = null)
{
if (string.IsNullOrWhiteSpace(type))
{
throw new ArgumentException("Signature type must be provided.", nameof(type));
}
Type = type.Trim();
Subject = string.IsNullOrWhiteSpace(subject) ? null : subject.Trim();
Issuer = string.IsNullOrWhiteSpace(issuer) ? null : issuer.Trim();
KeyId = string.IsNullOrWhiteSpace(keyId) ? null : keyId.Trim();
VerifiedAt = verifiedAt;
TransparencyLogReference = string.IsNullOrWhiteSpace(transparencyLogReference)
? null
: transparencyLogReference.Trim();
}
public string Type { get; }
public string? Subject { get; }
public string? Issuer { get; }
public string? KeyId { get; }
public DateTimeOffset? VerifiedAt { get; }
public string? TransparencyLogReference { get; }
}
public sealed record VexSignatureMetadata
{
public VexSignatureMetadata(
string type,
string? subject = null,
string? issuer = null,
string? keyId = null,
DateTimeOffset? verifiedAt = null,
string? transparencyLogReference = null,
VexSignatureTrustMetadata? trust = null)
{
if (string.IsNullOrWhiteSpace(type))
{
throw new ArgumentException("Signature type must be provided.", nameof(type));
}
Type = type.Trim();
Subject = string.IsNullOrWhiteSpace(subject) ? null : subject.Trim();
Issuer = string.IsNullOrWhiteSpace(issuer) ? null : issuer.Trim();
KeyId = string.IsNullOrWhiteSpace(keyId) ? null : keyId.Trim();
VerifiedAt = verifiedAt;
TransparencyLogReference = string.IsNullOrWhiteSpace(transparencyLogReference)
? null
: transparencyLogReference.Trim();
Trust = trust;
}
public string Type { get; }
public string? Subject { get; }
public string? Issuer { get; }
public string? KeyId { get; }
public DateTimeOffset? VerifiedAt { get; }
public string? TransparencyLogReference { get; }
public VexSignatureTrustMetadata? Trust { get; }
}
public sealed record VexSignatureTrustMetadata
{
public VexSignatureTrustMetadata(
decimal effectiveWeight,
string tenantId,
string issuerId,
bool tenantOverrideApplied,
DateTimeOffset retrievedAtUtc)
{
EffectiveWeight = effectiveWeight;
TenantId = string.IsNullOrWhiteSpace(tenantId) ? "@unknown" : tenantId.Trim();
IssuerId = string.IsNullOrWhiteSpace(issuerId) ? "unknown" : issuerId.Trim();
TenantOverrideApplied = tenantOverrideApplied;
RetrievedAtUtc = retrievedAtUtc.ToUniversalTime();
}
public decimal EffectiveWeight { get; }
public string TenantId { get; }
public string IssuerId { get; }
public bool TenantOverrideApplied { get; }
public DateTimeOffset RetrievedAtUtc { get; }
}
public sealed record VexConfidence
{

View File

@@ -783,43 +783,76 @@ internal sealed class VexSignatureMetadataDocument
public string? Issuer { get; set; }
= null;
public string? KeyId { get; set; }
= null;
public DateTime? VerifiedAt { get; set; }
= null;
public string? TransparencyLogReference { get; set; }
= null;
public static VexSignatureMetadataDocument? FromDomain(VexSignatureMetadata? signature)
=> signature is null
? null
: new VexSignatureMetadataDocument
{
Type = signature.Type,
Subject = signature.Subject,
Issuer = signature.Issuer,
KeyId = signature.KeyId,
VerifiedAt = signature.VerifiedAt?.UtcDateTime,
TransparencyLogReference = signature.TransparencyLogReference,
};
public VexSignatureMetadata ToDomain()
{
var verifiedAt = VerifiedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(VerifiedAt.Value, DateTimeKind.Utc))
: (DateTimeOffset?)null;
return new VexSignatureMetadata(
Type,
Subject,
Issuer,
KeyId,
verifiedAt,
TransparencyLogReference);
}
}
public string? KeyId { get; set; }
= null;
public DateTime? VerifiedAt { get; set; }
= null;
public string? TransparencyLogReference { get; set; }
= null;
public decimal? TrustWeight { get; set; }
= null;
public string? TrustTenantId { get; set; }
= null;
public string? TrustIssuerId { get; set; }
= null;
public bool? TrustTenantOverrideApplied { get; set; }
= null;
public DateTime? TrustRetrievedAtUtc { get; set; }
= null;
public static VexSignatureMetadataDocument? FromDomain(VexSignatureMetadata? signature)
=> signature is null
? null
: new VexSignatureMetadataDocument
{
Type = signature.Type,
Subject = signature.Subject,
Issuer = signature.Issuer,
KeyId = signature.KeyId,
VerifiedAt = signature.VerifiedAt?.UtcDateTime,
TransparencyLogReference = signature.TransparencyLogReference,
TrustWeight = signature.Trust?.EffectiveWeight,
TrustTenantId = signature.Trust?.TenantId,
TrustIssuerId = signature.Trust?.IssuerId,
TrustTenantOverrideApplied = signature.Trust?.TenantOverrideApplied,
TrustRetrievedAtUtc = signature.Trust?.RetrievedAtUtc.UtcDateTime
};
public VexSignatureMetadata ToDomain()
{
var verifiedAt = VerifiedAt.HasValue
? new DateTimeOffset(DateTime.SpecifyKind(VerifiedAt.Value, DateTimeKind.Utc))
: (DateTimeOffset?)null;
VexSignatureTrustMetadata? trust = null;
if (TrustWeight is not null && TrustRetrievedAtUtc is not null)
{
var retrievedOffset = new DateTimeOffset(DateTime.SpecifyKind(TrustRetrievedAtUtc.Value, DateTimeKind.Utc));
trust = new VexSignatureTrustMetadata(
TrustWeight.Value,
TrustTenantId ?? "@unknown",
TrustIssuerId ?? "unknown",
TrustTenantOverrideApplied ?? false,
retrievedOffset);
}
return new VexSignatureMetadata(
Type,
Subject,
Issuer,
KeyId,
verifiedAt,
TransparencyLogReference,
trust);
}
}
[BsonIgnoreExtraElements]
internal sealed class VexClaimDocumentRecord

View File

@@ -19,12 +19,13 @@ using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Aoc;
using StellaOps.Excititor.Storage.Mongo;
using StellaOps.Excititor.Worker.Options;
using StellaOps.Excititor.Worker.Scheduling;
using StellaOps.Excititor.Worker.Signature;
using StellaOps.Aoc;
using Xunit;
using System.Runtime.CompilerServices;
using StellaOps.Excititor.Worker.Options;
using StellaOps.Excititor.Worker.Scheduling;
using StellaOps.Excititor.Worker.Signature;
using StellaOps.Aoc;
using Xunit;
using System.Runtime.CompilerServices;
using StellaOps.IssuerDirectory.Client;
namespace StellaOps.Excititor.Worker.Tests;
@@ -285,11 +286,12 @@ public sealed class DefaultVexProviderRunnerTests
.Add("verification.issuer", "issuer-from-verifier")
.Add("verification.keyId", "key-from-verifier");
var attestationVerifier = new StubAttestationVerifier(true, diagnostics);
var signatureVerifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
attestationVerifier,
time);
var attestationVerifier = new StubAttestationVerifier(true, diagnostics);
var signatureVerifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
attestationVerifier,
time,
TestIssuerDirectoryClient.Instance);
var connector = TestConnector.WithDocuments("excititor:test", document);
var stateRepository = new InMemoryStateRepository();
@@ -465,28 +467,49 @@ public sealed class DefaultVexProviderRunnerTests
=> ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray<VexClaim>.Empty, ImmutableDictionary<string, string>.Empty));
}
private sealed class StubNormalizerRouter : IVexNormalizerRouter
{
private readonly ImmutableArray<VexClaim> _claims;
public StubNormalizerRouter(IEnumerable<VexClaim> claims)
{
_claims = claims.ToImmutableArray();
}
public int CallCount { get; private set; }
public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken)
{
CallCount++;
return ValueTask.FromResult(new VexClaimBatch(document, _claims, ImmutableDictionary<string, string>.Empty));
}
}
private sealed class NoopSignatureVerifier : IVexSignatureVerifier
{
public ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken cancellationToken)
=> ValueTask.FromResult<VexSignatureMetadata?>(null);
private sealed class StubNormalizerRouter : IVexNormalizerRouter
{
private readonly ImmutableArray<VexClaim> _claims;
public StubNormalizerRouter(IEnumerable<VexClaim> claims)
{
_claims = claims.ToImmutableArray();
}
public int CallCount { get; private set; }
public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken)
{
CallCount++;
return ValueTask.FromResult(new VexClaimBatch(document, _claims, ImmutableDictionary<string, string>.Empty));
}
}
private sealed class TestIssuerDirectoryClient : IIssuerDirectoryClient
{
public static TestIssuerDirectoryClient Instance { get; } = new();
private static readonly IssuerTrustResponseModel DefaultTrust = new(null, null, 1m);
public ValueTask<IReadOnlyList<IssuerKeyModel>> GetIssuerKeysAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
=> ValueTask.FromResult<IReadOnlyList<IssuerKeyModel>>(Array.Empty<IssuerKeyModel>());
public ValueTask<IssuerTrustResponseModel> GetIssuerTrustAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
=> ValueTask.FromResult(DefaultTrust);
}
private sealed class NoopSignatureVerifier : IVexSignatureVerifier
{
public ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken cancellationToken)
=> ValueTask.FromResult<VexSignatureMetadata?>(null);
}
private sealed class InMemoryStateRepository : IVexConnectorStateRepository

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System;
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@@ -6,13 +7,14 @@ using System.Text.Json.Serialization;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Aoc;
using StellaOps.Excititor.Attestation.Dsse;
using StellaOps.Excititor.Attestation.Models;
using StellaOps.Excititor.Attestation.Verification;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Aoc;
using StellaOps.Excititor.Worker.Signature;
using Xunit;
using StellaOps.Excititor.Attestation.Dsse;
using StellaOps.Excititor.Attestation.Models;
using StellaOps.Excititor.Attestation.Verification;
using StellaOps.Excititor.Core;
using StellaOps.Excititor.Core.Aoc;
using StellaOps.Excititor.Worker.Signature;
using StellaOps.IssuerDirectory.Client;
using Xunit;
namespace StellaOps.Excititor.Worker.Tests.Signature;
@@ -41,7 +43,9 @@ public sealed class WorkerSignatureVerifierTests
content,
metadata);
var verifier = new WorkerSignatureVerifier(NullLogger<WorkerSignatureVerifier>.Instance);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
issuerDirectoryClient: StubIssuerDirectoryClient.DefaultFor("tenant-a", "issuer-a", "kid"));
var result = await verifier.VerifyAsync(document, CancellationToken.None);
@@ -67,7 +71,9 @@ public sealed class WorkerSignatureVerifierTests
content,
metadata);
var verifier = new WorkerSignatureVerifier(NullLogger<WorkerSignatureVerifier>.Instance);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
issuerDirectoryClient: StubIssuerDirectoryClient.Empty());
var exception = await Assert.ThrowsAsync<ExcititorAocGuardException>(() => verifier.VerifyAsync(document, CancellationToken.None).AsTask());
exception.PrimaryErrorCode.Should().Be("ERR_AOC_005");
@@ -79,8 +85,12 @@ public sealed class WorkerSignatureVerifierTests
var now = DateTimeOffset.UtcNow;
var (document, metadata) = CreateAttestationDocument(now, subject: "export-1", includeRekor: true);
var attestationVerifier = new StubAttestationVerifier(true);
var verifier = new WorkerSignatureVerifier(NullLogger<WorkerSignatureVerifier>.Instance, attestationVerifier, TimeProvider.System);
var attestationVerifier = new StubAttestationVerifier(true);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
attestationVerifier,
TimeProvider.System,
StubIssuerDirectoryClient.Empty());
var result = await verifier.VerifyAsync(document with { Metadata = metadata }, CancellationToken.None);
@@ -96,8 +106,12 @@ public sealed class WorkerSignatureVerifierTests
var now = DateTimeOffset.UtcNow;
var (document, metadata) = CreateAttestationDocument(now, subject: "export-2", includeRekor: true);
var attestationVerifier = new StubAttestationVerifier(false);
var verifier = new WorkerSignatureVerifier(NullLogger<WorkerSignatureVerifier>.Instance, attestationVerifier, TimeProvider.System);
var attestationVerifier = new StubAttestationVerifier(false);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
attestationVerifier,
TimeProvider.System,
StubIssuerDirectoryClient.Empty());
await Assert.ThrowsAsync<ExcititorAocGuardException>(() => verifier.VerifyAsync(document with { Metadata = metadata }, CancellationToken.None).AsTask());
attestationVerifier.Invocations.Should().Be(1);
@@ -113,27 +127,64 @@ public sealed class WorkerSignatureVerifierTests
.Add("verification.issuer", "issuer-from-attestation")
.Add("verification.keyId", "kid-from-attestation");
var attestationVerifier = new StubAttestationVerifier(true, diagnostics);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
attestationVerifier,
new FixedTimeProvider(now));
var attestationVerifier = new StubAttestationVerifier(true, diagnostics);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
attestationVerifier,
new FixedTimeProvider(now),
StubIssuerDirectoryClient.DefaultFor("tenant-a", "issuer-from-attestation", "kid-from-attestation"));
var result = await verifier.VerifyAsync(document, CancellationToken.None);
result.Should().NotBeNull();
result!.Issuer.Should().Be("issuer-from-attestation");
result.KeyId.Should().Be("kid-from-attestation");
result.TransparencyLogReference.Should().BeNull();
result.VerifiedAt.Should().Be(now);
attestationVerifier.Invocations.Should().Be(1);
}
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
Span<byte> buffer = stackalloc byte[32];
return SHA256.TryHashData(payload, buffer, out _)
? "sha256:" + Convert.ToHexString(buffer).ToLowerInvariant()
result.TransparencyLogReference.Should().BeNull();
result.VerifiedAt.Should().Be(now);
attestationVerifier.Invocations.Should().Be(1);
}
[Fact]
public async Task VerifyAsync_AttachesIssuerTrustMetadata()
{
var now = DateTimeOffset.UtcNow;
var content = Encoding.UTF8.GetBytes("{\"id\":\"trust\"}");
var digest = ComputeDigest(content);
var metadata = ImmutableDictionary<string, string>.Empty
.Add("tenant", "tenant-a")
.Add("vex.signature.type", "cosign")
.Add("vex.signature.issuer", "issuer-a")
.Add("vex.signature.keyId", "key-1")
.Add("vex.signature.verifiedAt", now.ToString("O"));
var document = new VexRawDocument(
"provider-a",
VexDocumentFormat.Csaf,
new Uri("https://example.org/vex-trust.json"),
now,
digest,
content,
metadata);
var issuerClient = StubIssuerDirectoryClient.DefaultFor("tenant-a", "issuer-a", "key-1", 0.85m);
var verifier = new WorkerSignatureVerifier(
NullLogger<WorkerSignatureVerifier>.Instance,
issuerDirectoryClient: issuerClient);
var result = await verifier.VerifyAsync(document, CancellationToken.None);
result.Should().NotBeNull();
result!.Trust.Should().NotBeNull();
result.Trust!.EffectiveWeight.Should().Be(0.85m);
result.Trust!.TenantId.Should().Be("tenant-a");
result.Trust!.IssuerId.Should().Be("issuer-a");
}
private static string ComputeDigest(ReadOnlySpan<byte> payload)
{
Span<byte> buffer = stackalloc byte[32];
return SHA256.TryHashData(payload, buffer, out _)
? "sha256:" + Convert.ToHexString(buffer).ToLowerInvariant()
: "sha256:" + Convert.ToHexString(SHA256.HashData(payload.ToArray())).ToLowerInvariant();
}
@@ -195,12 +246,12 @@ public sealed class WorkerSignatureVerifierTests
return (document, metadataBuilder.ToImmutable());
}
private sealed class StubAttestationVerifier : IVexAttestationVerifier
{
private readonly bool _isValid;
private readonly ImmutableDictionary<string, string> _diagnostics;
public StubAttestationVerifier(bool isValid, ImmutableDictionary<string, string>? diagnostics = null)
private sealed class StubAttestationVerifier : IVexAttestationVerifier
{
private readonly bool _isValid;
private readonly ImmutableDictionary<string, string> _diagnostics;
public StubAttestationVerifier(bool isValid, ImmutableDictionary<string, string>? diagnostics = null)
{
_isValid = isValid;
_diagnostics = diagnostics ?? ImmutableDictionary<string, string>.Empty;
@@ -211,15 +262,73 @@ public sealed class WorkerSignatureVerifierTests
public ValueTask<VexAttestationVerification> VerifyAsync(VexAttestationVerificationRequest request, CancellationToken cancellationToken)
{
Invocations++;
return ValueTask.FromResult(new VexAttestationVerification(_isValid, _diagnostics));
}
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FixedTimeProvider(DateTimeOffset utcNow)
return ValueTask.FromResult(new VexAttestationVerification(_isValid, _diagnostics));
}
}
private sealed class StubIssuerDirectoryClient : IIssuerDirectoryClient
{
private readonly IReadOnlyList<IssuerKeyModel> _keys;
private readonly IssuerTrustResponseModel _trust;
private StubIssuerDirectoryClient(
IReadOnlyList<IssuerKeyModel> keys,
IssuerTrustResponseModel trust)
{
_keys = keys;
_trust = trust;
}
public static StubIssuerDirectoryClient Empty()
=> new(Array.Empty<IssuerKeyModel>(), new IssuerTrustResponseModel(null, null, 0m));
public static StubIssuerDirectoryClient DefaultFor(
string tenantId,
string issuerId,
string keyId,
decimal weight = 1m)
{
var key = new IssuerKeyModel(
keyId,
issuerId,
tenantId,
"Ed25519PublicKey",
"Active",
"base64",
Convert.ToBase64String(new byte[32]),
"fingerprint-" + keyId,
null,
null,
null,
null);
var now = DateTimeOffset.UtcNow;
var overrideModel = new IssuerTrustOverrideModel(weight, "stub", now, "test", now, "test");
return new StubIssuerDirectoryClient(
new[] { key },
new IssuerTrustResponseModel(overrideModel, null, weight));
}
public ValueTask<IReadOnlyList<IssuerKeyModel>> GetIssuerKeysAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
=> ValueTask.FromResult(_keys);
public ValueTask<IssuerTrustResponseModel> GetIssuerTrustAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
=> ValueTask.FromResult(_trust);
}
private sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FixedTimeProvider(DateTimeOffset utcNow)
{
_utcNow = utcNow;
}