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

@@ -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;
}