consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -25,7 +25,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Cryptography.Kms\StellaOps.Cryptography.Kms.csproj" />
|
||||
<ProjectReference Include="..\..\..\Signer\StellaOps.Signer\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Signer\StellaOps.Signer.Core\StellaOps.Signer.Core.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| ATTESTOR-225-002 | DOING | Sprint 225 endpoint tests for trusted/revoked/unknown key scenarios. |
|
||||
| ATTESTOR-225-003 | DOING | Sprint 225 tenant isolation and claim-derived tenant tests. |
|
||||
| ATTESTOR-225-004 | DOING | Sprint 225 verdict-by-hash retrieval tests with authorization checks. |
|
||||
| AUDIT-0066-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
|
||||
| AUDIT-0066-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
|
||||
| AUDIT-0066-A | DONE | Waived (test project; revalidated 2026-01-06). |
|
||||
|
||||
@@ -11,6 +11,7 @@ using OpenTelemetry.Trace;
|
||||
using Serilog;
|
||||
using Serilog.Context;
|
||||
using Serilog.Events;
|
||||
using StellaOps.Attestation;
|
||||
using StellaOps.Attestor.Core.Bulk;
|
||||
using StellaOps.Attestor.Core.Observability;
|
||||
using StellaOps.Attestor.Core.Options;
|
||||
@@ -130,6 +131,8 @@ internal static class AttestorWebServiceComposition
|
||||
builder.Services.AddOptions<AttestorWebServiceFeatures>()
|
||||
.Bind(builder.Configuration.GetSection($"{configurationSection}:features"))
|
||||
.ValidateOnStart();
|
||||
builder.Services.AddOptions<VerdictAuthorityRosterOptions>()
|
||||
.Bind(builder.Configuration.GetSection($"{configurationSection}:verdictTrust"));
|
||||
|
||||
var featureOptions = builder.Configuration.GetSection($"{configurationSection}:features")
|
||||
.Get<AttestorWebServiceFeatures>() ?? new AttestorWebServiceFeatures();
|
||||
@@ -141,6 +144,7 @@ internal static class AttestorWebServiceComposition
|
||||
manager.FeatureProviders.Add(new AttestorWebServiceControllerFeatureProvider(featureOptions));
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSingleton<IDsseVerifier, DsseVerifier>();
|
||||
builder.Services.AddAttestorInfrastructure();
|
||||
builder.Services.AddProofChainServices();
|
||||
|
||||
|
||||
@@ -98,3 +98,27 @@ public sealed class VerdictAttestationResponseDto
|
||||
[JsonPropertyName("createdAt")]
|
||||
public string CreatedAt { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for verdict lookup by deterministic hash.
|
||||
/// </summary>
|
||||
public sealed class VerdictLookupResponseDto
|
||||
{
|
||||
[JsonPropertyName("verdictId")]
|
||||
public string VerdictId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("attestationUri")]
|
||||
public string AttestationUri { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("envelope")]
|
||||
public string Envelope { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("keyId")]
|
||||
public string KeyId { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("createdAt")]
|
||||
public string CreatedAt { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("tenantId")]
|
||||
public string TenantId { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,17 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Attestation;
|
||||
using StellaOps.Attestor.Core.Signing;
|
||||
using StellaOps.Attestor.Core.Submission;
|
||||
using StellaOps.Attestor.WebService.Contracts;
|
||||
using StellaOps.Attestor.WebService.Options;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -31,21 +35,28 @@ public class VerdictController : ControllerBase
|
||||
{
|
||||
private readonly IAttestationSigningService _signingService;
|
||||
private readonly ILogger<VerdictController> _logger;
|
||||
private readonly IDsseVerifier _dsseVerifier;
|
||||
private readonly IHttpClientFactory? _httpClientFactory;
|
||||
private readonly AttestorWebServiceFeatures _features;
|
||||
private readonly VerdictAuthorityRosterOptions _verdictRosterOptions;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private static readonly ConcurrentDictionary<string, CachedVerdictRecord> VerdictCache = new(StringComparer.Ordinal);
|
||||
|
||||
public VerdictController(
|
||||
IAttestationSigningService signingService,
|
||||
ILogger<VerdictController> logger,
|
||||
IDsseVerifier dsseVerifier,
|
||||
IHttpClientFactory? httpClientFactory = null,
|
||||
IOptions<AttestorWebServiceFeatures>? features = null,
|
||||
IOptions<VerdictAuthorityRosterOptions>? verdictRosterOptions = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_signingService = signingService ?? throw new ArgumentNullException(nameof(signingService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_dsseVerifier = dsseVerifier ?? throw new ArgumentNullException(nameof(dsseVerifier));
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_features = features?.Value ?? new AttestorWebServiceFeatures();
|
||||
_verdictRosterOptions = verdictRosterOptions?.Value ?? new VerdictAuthorityRosterOptions();
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
@@ -75,6 +86,14 @@ public class VerdictController : ControllerBase
|
||||
"Creating verdict attestation for subject {SubjectName}",
|
||||
request.Subject.Name);
|
||||
|
||||
var tenantResolutionResult = ResolveTenantContext(User, Request.Headers);
|
||||
if (tenantResolutionResult.Error is not null)
|
||||
{
|
||||
return tenantResolutionResult.Error;
|
||||
}
|
||||
|
||||
var tenantId = tenantResolutionResult.TenantId!;
|
||||
|
||||
// Validate request
|
||||
if (string.IsNullOrWhiteSpace(request.PredicateType))
|
||||
{
|
||||
@@ -114,9 +133,17 @@ public class VerdictController : ControllerBase
|
||||
var predicateBase64 = Convert.ToBase64String(predicateBytes);
|
||||
|
||||
// Create signing request
|
||||
var requestedKeyId = string.IsNullOrWhiteSpace(request.KeyId) ? "default" : request.KeyId.Trim();
|
||||
var rosterResolution = ResolveRosterEntry(requestedKeyId);
|
||||
if (rosterResolution.Error is not null)
|
||||
{
|
||||
return rosterResolution.Error;
|
||||
}
|
||||
|
||||
var rosterEntry = rosterResolution.Entry!;
|
||||
var signingRequest = new AttestationSignRequest
|
||||
{
|
||||
KeyId = request.KeyId ?? "default",
|
||||
KeyId = requestedKeyId,
|
||||
PayloadType = request.PredicateType,
|
||||
PayloadBase64 = predicateBase64
|
||||
};
|
||||
@@ -127,7 +154,7 @@ public class VerdictController : ControllerBase
|
||||
CallerSubject = "system",
|
||||
CallerAudience = "policy-engine",
|
||||
CallerClientId = "policy-engine-verdict-attestor",
|
||||
CallerTenant = "default" // TODO: Extract from auth context
|
||||
CallerTenant = tenantId
|
||||
};
|
||||
|
||||
// Sign the predicate
|
||||
@@ -137,12 +164,37 @@ public class VerdictController : ControllerBase
|
||||
var envelope = signResult.Bundle.Dsse;
|
||||
var envelopeJson = SerializeEnvelope(envelope, signResult.KeyId);
|
||||
|
||||
if (!string.Equals(signResult.KeyId, rosterEntry.KeyId, StringComparison.Ordinal))
|
||||
{
|
||||
return StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
CreateProblem(
|
||||
title: "Signing key is not trusted by roster.",
|
||||
detail: $"Signed key '{signResult.KeyId}' does not match roster key '{rosterEntry.KeyId}'.",
|
||||
status: StatusCodes.Status403Forbidden,
|
||||
code: "authority_key_mismatch"));
|
||||
}
|
||||
|
||||
var signatureVerification = await _dsseVerifier.VerifyAsync(envelopeJson, rosterEntry.PublicKeyPem, ct).ConfigureAwait(false);
|
||||
if (!signatureVerification.IsValid)
|
||||
{
|
||||
return StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
CreateProblem(
|
||||
title: "Verdict signature is untrusted.",
|
||||
detail: "Signed verdict DSSE envelope failed authority roster verification.",
|
||||
status: StatusCodes.Status403Forbidden,
|
||||
code: "authority_signature_untrusted",
|
||||
issues: signatureVerification.Issues.ToArray()));
|
||||
}
|
||||
|
||||
// Rekor log index (not implemented in minimal handler)
|
||||
long? rekorLogIndex = null;
|
||||
|
||||
// Store in Evidence Locker (via HTTP call)
|
||||
await StoreVerdictInEvidenceLockerAsync(
|
||||
verdictId,
|
||||
tenantId,
|
||||
request.Subject.Name,
|
||||
envelopeJson,
|
||||
signResult,
|
||||
@@ -158,15 +210,13 @@ public class VerdictController : ControllerBase
|
||||
KeyId = signResult.KeyId ?? request.KeyId ?? "default",
|
||||
CreatedAt = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture)
|
||||
};
|
||||
VerdictCache[verdictId] = CachedVerdictRecord.From(response, tenantId);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Verdict attestation created successfully: {VerdictId}",
|
||||
verdictId);
|
||||
|
||||
return CreatedAtRoute(
|
||||
routeName: null, // No route name needed for external link
|
||||
routeValues: null,
|
||||
value: response);
|
||||
return Created(attestationUri, response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -186,6 +236,60 @@ public class VerdictController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a verdict attestation by deterministic verdict hash.
|
||||
/// </summary>
|
||||
[HttpGet("~/api/v1/verdicts/{verdictId}")]
|
||||
[Authorize("attestor:read")]
|
||||
[EnableRateLimiting("attestor-reads")]
|
||||
[ProducesResponseType(typeof(VerdictLookupResponseDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<VerdictLookupResponseDto>> GetVerdictByHashAsync(
|
||||
[FromRoute] string verdictId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (!_features.VerdictsEnabled)
|
||||
{
|
||||
return NotImplementedResult();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(verdictId))
|
||||
{
|
||||
return BadRequest(CreateProblem(
|
||||
title: "Invalid verdict identifier.",
|
||||
detail: "verdictId is required.",
|
||||
status: StatusCodes.Status400BadRequest,
|
||||
code: "invalid_verdict_id"));
|
||||
}
|
||||
|
||||
var tenantResolutionResult = ResolveTenantContext(User, Request.Headers);
|
||||
if (tenantResolutionResult.Error is not null)
|
||||
{
|
||||
return tenantResolutionResult.Error;
|
||||
}
|
||||
|
||||
var tenantId = tenantResolutionResult.TenantId!;
|
||||
if (VerdictCache.TryGetValue(verdictId, out var cached) &&
|
||||
string.Equals(cached.TenantId, tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
return Ok(cached.ToLookupResponse(verdictId));
|
||||
}
|
||||
|
||||
var lockerResult = await FetchVerdictFromEvidenceLockerAsync(verdictId, tenantId, ct).ConfigureAwait(false);
|
||||
if (lockerResult is not null)
|
||||
{
|
||||
VerdictCache[verdictId] = CachedVerdictRecord.From(lockerResult);
|
||||
return Ok(lockerResult);
|
||||
}
|
||||
|
||||
return NotFound(CreateProblem(
|
||||
title: "Verdict not found.",
|
||||
detail: $"No verdict exists for hash '{verdictId}' in tenant '{tenantId}'.",
|
||||
status: StatusCodes.Status404NotFound,
|
||||
code: "verdict_not_found"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic verdict ID from predicate content.
|
||||
/// </summary>
|
||||
@@ -227,6 +331,7 @@ public class VerdictController : ControllerBase
|
||||
/// </summary>
|
||||
private async Task StoreVerdictInEvidenceLockerAsync(
|
||||
string verdictId,
|
||||
string tenantId,
|
||||
string findingId,
|
||||
string envelopeJson,
|
||||
AttestationSignResult signResult,
|
||||
@@ -268,7 +373,7 @@ public class VerdictController : ControllerBase
|
||||
var storeRequest = new
|
||||
{
|
||||
verdict_id = verdictId,
|
||||
tenant_id = "default", // TODO: Extract from auth context (requires CallerTenant from SubmissionContext)
|
||||
tenant_id = tenantId,
|
||||
policy_run_id = policyRunId,
|
||||
policy_id = policyId,
|
||||
policy_version = policyVersion,
|
||||
@@ -310,6 +415,220 @@ public class VerdictController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
private (string? TenantId, ActionResult? Error) ResolveTenantContext(ClaimsPrincipal principal, IHeaderDictionary headers)
|
||||
{
|
||||
var tenantId = principal.FindFirst("tenant_id")?.Value
|
||||
?? principal.FindFirst("tenant")?.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return (null, StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
CreateProblem(
|
||||
title: "Tenant claim is required.",
|
||||
detail: "Authenticated principal does not contain tenant_id or tenant claim.",
|
||||
status: StatusCodes.Status403Forbidden,
|
||||
code: "tenant_claim_missing")));
|
||||
}
|
||||
|
||||
if (headers.TryGetValue("X-Tenant-Id", out var headerTenant) &&
|
||||
headerTenant.Count > 0 &&
|
||||
!string.Equals(headerTenant[0], tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
return (null, StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
CreateProblem(
|
||||
title: "Tenant mismatch detected.",
|
||||
detail: "Tenant header does not match authenticated tenant claim.",
|
||||
status: StatusCodes.Status403Forbidden,
|
||||
code: "tenant_mismatch")));
|
||||
}
|
||||
|
||||
return (tenantId, null);
|
||||
}
|
||||
|
||||
private (VerdictAuthorityKeyOptions? Entry, ActionResult? Error) ResolveRosterEntry(string keyId)
|
||||
{
|
||||
if (_verdictRosterOptions.Keys.Count == 0)
|
||||
{
|
||||
return (null, StatusCode(
|
||||
StatusCodes.Status503ServiceUnavailable,
|
||||
CreateProblem(
|
||||
title: "Authority roster is unavailable.",
|
||||
detail: "attestor:verdictTrust:keys must include at least one trusted key.",
|
||||
status: StatusCodes.Status503ServiceUnavailable,
|
||||
code: "authority_roster_unavailable")));
|
||||
}
|
||||
|
||||
var entry = _verdictRosterOptions.Keys
|
||||
.FirstOrDefault(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal));
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return (null, StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
CreateProblem(
|
||||
title: "Signing key is not in authority roster.",
|
||||
detail: $"Key '{keyId}' is not trusted for verdict creation.",
|
||||
status: StatusCodes.Status403Forbidden,
|
||||
code: "authority_key_unknown")));
|
||||
}
|
||||
|
||||
if (string.Equals(entry.Status, "revoked", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (null, StatusCode(
|
||||
StatusCodes.Status403Forbidden,
|
||||
CreateProblem(
|
||||
title: "Signing key is revoked.",
|
||||
detail: $"Key '{entry.KeyId}' is revoked in authority roster.",
|
||||
status: StatusCodes.Status403Forbidden,
|
||||
code: "authority_key_revoked")));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entry.PublicKeyPem))
|
||||
{
|
||||
return (null, StatusCode(
|
||||
StatusCodes.Status500InternalServerError,
|
||||
CreateProblem(
|
||||
title: "Authority roster key is incomplete.",
|
||||
detail: $"Key '{entry.KeyId}' is missing public key material.",
|
||||
status: StatusCodes.Status500InternalServerError,
|
||||
code: "authority_key_missing_public_key")));
|
||||
}
|
||||
|
||||
return (entry, null);
|
||||
}
|
||||
|
||||
private async Task<VerdictLookupResponseDto?> FetchVerdictFromEvidenceLockerAsync(
|
||||
string verdictId,
|
||||
string tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (_httpClientFactory is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("EvidenceLocker");
|
||||
var response = await client.GetAsync($"/api/v1/verdicts/{Uri.EscapeDataString(verdictId)}", ct).ConfigureAwait(false);
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Evidence Locker verdict lookup failed for {VerdictId}: {StatusCode}",
|
||||
verdictId,
|
||||
response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct).ConfigureAwait(false);
|
||||
if (payload.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lockerTenant = GetOptionalString(payload, "tenant_id", "tenantId");
|
||||
if (!string.IsNullOrWhiteSpace(lockerTenant) &&
|
||||
!string.Equals(lockerTenant, tenantId, StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var envelope = ExtractEnvelope(payload);
|
||||
if (string.IsNullOrWhiteSpace(envelope))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var keyId = GetOptionalString(payload, "key_id", "keyId") ?? "unknown";
|
||||
var createdAt = GetOptionalString(payload, "evaluated_at", "created_at", "createdAt")
|
||||
?? _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
return new VerdictLookupResponseDto
|
||||
{
|
||||
VerdictId = verdictId,
|
||||
AttestationUri = $"/api/v1/verdicts/{verdictId}",
|
||||
Envelope = envelope,
|
||||
KeyId = keyId,
|
||||
CreatedAt = createdAt,
|
||||
TenantId = tenantId
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Evidence Locker verdict lookup failed for {VerdictId}", verdictId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ExtractEnvelope(JsonElement payload)
|
||||
{
|
||||
if (!payload.TryGetProperty("envelope", out var envelopeElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (envelopeElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return envelopeElement.GetString();
|
||||
}
|
||||
|
||||
if (envelopeElement.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
|
||||
{
|
||||
var envelopeJson = envelopeElement.GetRawText();
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(envelopeJson));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetOptionalString(JsonElement payload, params string[] candidates)
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (payload.TryGetProperty(candidate, out var value) &&
|
||||
value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var text = value.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ProblemDetails CreateProblem(
|
||||
string title,
|
||||
string detail,
|
||||
int status,
|
||||
string code,
|
||||
string[]? issues = null)
|
||||
{
|
||||
var problem = new ProblemDetails
|
||||
{
|
||||
Title = title,
|
||||
Detail = detail,
|
||||
Status = status
|
||||
};
|
||||
|
||||
problem.Extensions["code"] = code;
|
||||
if (issues is not null && issues.Length > 0)
|
||||
{
|
||||
problem.Extensions["issues"] = issues;
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts verdict metadata from predicate JSON.
|
||||
/// </summary>
|
||||
@@ -418,4 +737,28 @@ public class VerdictController : ControllerBase
|
||||
StatusCode = StatusCodes.Status501NotImplemented
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record CachedVerdictRecord(
|
||||
string TenantId,
|
||||
string Envelope,
|
||||
string KeyId,
|
||||
string CreatedAt)
|
||||
{
|
||||
public static CachedVerdictRecord From(VerdictAttestationResponseDto response, string tenantId)
|
||||
=> new(tenantId, response.Envelope, response.KeyId, response.CreatedAt);
|
||||
|
||||
public static CachedVerdictRecord From(VerdictLookupResponseDto response)
|
||||
=> new(response.TenantId, response.Envelope, response.KeyId, response.CreatedAt);
|
||||
|
||||
public VerdictLookupResponseDto ToLookupResponse(string verdictId)
|
||||
=> new()
|
||||
{
|
||||
VerdictId = verdictId,
|
||||
AttestationUri = $"/api/v1/verdicts/{verdictId}",
|
||||
Envelope = Envelope,
|
||||
KeyId = KeyId,
|
||||
CreatedAt = CreatedAt,
|
||||
TenantId = TenantId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace StellaOps.Attestor.WebService.Options;
|
||||
|
||||
public sealed class VerdictAuthorityRosterOptions
|
||||
{
|
||||
public List<VerdictAuthorityKeyOptions> Keys { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class VerdictAuthorityKeyOptions
|
||||
{
|
||||
public string KeyId { get; set; } = string.Empty;
|
||||
|
||||
public string Status { get; set; } = "trusted";
|
||||
|
||||
public string PublicKeyPem { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
<PackageReference Include="StackExchange.Redis" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Attestation/StellaOps.Attestation.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Core\StellaOps.Attestor.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Attestor.Infrastructure\StellaOps.Attestor.Infrastructure.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.Configuration/StellaOps.Configuration.csproj" />
|
||||
|
||||
@@ -5,6 +5,9 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| ATTESTOR-225-002 | DOING | Sprint 225: enforce roster-based trust verification before verdict append. |
|
||||
| ATTESTOR-225-003 | DOING | Sprint 225: resolve tenant from authenticated claims and block spoofing. |
|
||||
| ATTESTOR-225-004 | DOING | Sprint 225: implement verdict-by-hash retrieval and tenant-scoped access checks. |
|
||||
| AUDIT-0072-M | DONE | Revalidated 2026-01-06 (maintainability audit). |
|
||||
| AUDIT-0072-T | DONE | Revalidated 2026-01-06 (test coverage audit). |
|
||||
| AUDIT-0072-A | DONE | Applied 2026-01-13 (feature gating, correlation ID provider, proof chain/verification summary updates, tests). |
|
||||
|
||||
Reference in New Issue
Block a user