Implement VEX document verification system with issuer management and signature verification
- Added IIssuerDirectory interface for managing VEX document issuers, including methods for registration, revocation, and trust validation. - Created InMemoryIssuerDirectory class as an in-memory implementation of IIssuerDirectory for testing and single-instance deployments. - Introduced ISignatureVerifier interface for verifying signatures on VEX documents, with support for multiple signature formats. - Developed SignatureVerifier class as the default implementation of ISignatureVerifier, allowing extensibility for different signature formats. - Implemented handlers for DSSE and JWS signature formats, including methods for verification and signature extraction. - Defined various records and enums for issuer and signature metadata, enhancing the structure and clarity of the verification process.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Policy.Engine.Attestation;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints for verification policy management per CONTRACT-VERIFICATION-POLICY-006.
|
||||
/// </summary>
|
||||
public static class VerificationPolicyEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapVerificationPolicies(this IEndpointRouteBuilder routes)
|
||||
{
|
||||
var group = routes.MapGroup("/api/v1/attestor/policies")
|
||||
.WithTags("Verification Policies");
|
||||
|
||||
group.MapPost("/", CreatePolicyAsync)
|
||||
.WithName("Attestor.CreatePolicy")
|
||||
.WithSummary("Create a new verification policy")
|
||||
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite))
|
||||
.Produces<VerificationPolicy>(StatusCodes.Status201Created)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status409Conflict);
|
||||
|
||||
group.MapGet("/{policyId}", GetPolicyAsync)
|
||||
.WithName("Attestor.GetPolicy")
|
||||
.WithSummary("Get a verification policy by ID")
|
||||
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyRead))
|
||||
.Produces<VerificationPolicy>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapGet("/", ListPoliciesAsync)
|
||||
.WithName("Attestor.ListPolicies")
|
||||
.WithSummary("List verification policies")
|
||||
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyRead))
|
||||
.Produces<VerificationPolicyListResponse>(StatusCodes.Status200OK);
|
||||
|
||||
group.MapPut("/{policyId}", UpdatePolicyAsync)
|
||||
.WithName("Attestor.UpdatePolicy")
|
||||
.WithSummary("Update a verification policy")
|
||||
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite))
|
||||
.Produces<VerificationPolicy>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
group.MapDelete("/{policyId}", DeletePolicyAsync)
|
||||
.WithName("Attestor.DeletePolicy")
|
||||
.WithSummary("Delete a verification policy")
|
||||
.RequireAuthorization(policy => policy.RequireClaim("scope", StellaOpsScopes.PolicyWrite))
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces<ProblemHttpResult>(StatusCodes.Status404NotFound);
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
private static async Task<IResult> CreatePolicyAsync(
|
||||
[FromBody] CreateVerificationPolicyRequest request,
|
||||
IVerificationPolicyStore store,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem(
|
||||
"Invalid request",
|
||||
"Request body is required.",
|
||||
"ERR_ATTEST_001"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.PolicyId))
|
||||
{
|
||||
return Results.BadRequest(CreateProblem(
|
||||
"Invalid request",
|
||||
"Policy ID is required.",
|
||||
"ERR_ATTEST_002"));
|
||||
}
|
||||
|
||||
if (request.PredicateTypes == null || request.PredicateTypes.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem(
|
||||
"Invalid request",
|
||||
"At least one predicate type is required.",
|
||||
"ERR_ATTEST_003"));
|
||||
}
|
||||
|
||||
if (await store.ExistsAsync(request.PolicyId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
return Results.Conflict(CreateProblem(
|
||||
"Policy exists",
|
||||
$"Policy '{request.PolicyId}' already exists.",
|
||||
"ERR_ATTEST_004"));
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
var policy = new VerificationPolicy(
|
||||
PolicyId: request.PolicyId,
|
||||
Version: request.Version ?? "1.0.0",
|
||||
Description: request.Description,
|
||||
TenantScope: request.TenantScope ?? "*",
|
||||
PredicateTypes: request.PredicateTypes,
|
||||
SignerRequirements: request.SignerRequirements ?? SignerRequirements.Default,
|
||||
ValidityWindow: request.ValidityWindow,
|
||||
Metadata: request.Metadata,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now);
|
||||
|
||||
await store.CreateAsync(policy, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Created(
|
||||
$"/api/v1/attestor/policies/{policy.PolicyId}",
|
||||
policy);
|
||||
}
|
||||
|
||||
private static async Task<IResult> GetPolicyAsync(
|
||||
[FromRoute] string policyId,
|
||||
IVerificationPolicyStore store,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var policy = await store.GetAsync(policyId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (policy == null)
|
||||
{
|
||||
return Results.NotFound(CreateProblem(
|
||||
"Policy not found",
|
||||
$"Policy '{policyId}' was not found.",
|
||||
"ERR_ATTEST_005"));
|
||||
}
|
||||
|
||||
return Results.Ok(policy);
|
||||
}
|
||||
|
||||
private static async Task<IResult> ListPoliciesAsync(
|
||||
[FromQuery] string? tenantScope,
|
||||
IVerificationPolicyStore store,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var policies = await store.ListAsync(tenantScope, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new VerificationPolicyListResponse(
|
||||
Policies: policies,
|
||||
Total: policies.Count));
|
||||
}
|
||||
|
||||
private static async Task<IResult> UpdatePolicyAsync(
|
||||
[FromRoute] string policyId,
|
||||
[FromBody] UpdateVerificationPolicyRequest request,
|
||||
IVerificationPolicyStore store,
|
||||
TimeProvider timeProvider,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return Results.BadRequest(CreateProblem(
|
||||
"Invalid request",
|
||||
"Request body is required.",
|
||||
"ERR_ATTEST_001"));
|
||||
}
|
||||
|
||||
var now = timeProvider.GetUtcNow();
|
||||
|
||||
var updated = await store.UpdateAsync(
|
||||
policyId,
|
||||
existing => existing with
|
||||
{
|
||||
Version = request.Version ?? existing.Version,
|
||||
Description = request.Description ?? existing.Description,
|
||||
PredicateTypes = request.PredicateTypes ?? existing.PredicateTypes,
|
||||
SignerRequirements = request.SignerRequirements ?? existing.SignerRequirements,
|
||||
ValidityWindow = request.ValidityWindow ?? existing.ValidityWindow,
|
||||
Metadata = request.Metadata ?? existing.Metadata,
|
||||
UpdatedAt = now
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (updated == null)
|
||||
{
|
||||
return Results.NotFound(CreateProblem(
|
||||
"Policy not found",
|
||||
$"Policy '{policyId}' was not found.",
|
||||
"ERR_ATTEST_005"));
|
||||
}
|
||||
|
||||
return Results.Ok(updated);
|
||||
}
|
||||
|
||||
private static async Task<IResult> DeletePolicyAsync(
|
||||
[FromRoute] string policyId,
|
||||
IVerificationPolicyStore store,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var deleted = await store.DeleteAsync(policyId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!deleted)
|
||||
{
|
||||
return Results.NotFound(CreateProblem(
|
||||
"Policy not found",
|
||||
$"Policy '{policyId}' was not found.",
|
||||
"ERR_ATTEST_005"));
|
||||
}
|
||||
|
||||
return Results.NoContent();
|
||||
}
|
||||
|
||||
private static ProblemDetails CreateProblem(string title, string detail, string? errorCode = null)
|
||||
{
|
||||
var problem = new ProblemDetails
|
||||
{
|
||||
Title = title,
|
||||
Detail = detail,
|
||||
Status = StatusCodes.Status400BadRequest
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(errorCode))
|
||||
{
|
||||
problem.Extensions["error_code"] = errorCode;
|
||||
}
|
||||
|
||||
return problem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response for listing verification policies.
|
||||
/// </summary>
|
||||
public sealed record VerificationPolicyListResponse(
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("policies")] IReadOnlyList<VerificationPolicy> Policies,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("total")] int Total);
|
||||
Reference in New Issue
Block a user