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>