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