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