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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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