- 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.
160 lines
5.5 KiB
C#
160 lines
5.5 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using StellaOps.Policy.Engine.AirGap;
|
|
|
|
namespace StellaOps.Policy.Engine.Endpoints;
|
|
|
|
/// <summary>
|
|
/// Endpoints for sealed-mode operations per CONTRACT-SEALED-MODE-004.
|
|
/// </summary>
|
|
public static class SealedModeEndpoints
|
|
{
|
|
public static IEndpointRouteBuilder MapSealedMode(this IEndpointRouteBuilder routes)
|
|
{
|
|
var group = routes.MapGroup("/system/airgap");
|
|
|
|
group.MapPost("/seal", SealAsync)
|
|
.WithName("AirGap.Seal")
|
|
.WithDescription("Seal the environment")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", "airgap:seal"))
|
|
.ProducesProblem(StatusCodes.Status400BadRequest)
|
|
.ProducesProblem(StatusCodes.Status500InternalServerError);
|
|
|
|
group.MapPost("/unseal", UnsealAsync)
|
|
.WithName("AirGap.Unseal")
|
|
.WithDescription("Unseal the environment")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", "airgap:seal"))
|
|
.ProducesProblem(StatusCodes.Status500InternalServerError);
|
|
|
|
group.MapGet("/status", GetStatusAsync)
|
|
.WithName("AirGap.GetStatus")
|
|
.WithDescription("Get sealed-mode status")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", "airgap:status:read"));
|
|
|
|
group.MapPost("/verify", VerifyBundleAsync)
|
|
.WithName("AirGap.VerifyBundle")
|
|
.WithDescription("Verify a bundle against trust roots")
|
|
.RequireAuthorization(policy => policy.RequireClaim("scope", "airgap:verify"))
|
|
.ProducesProblem(StatusCodes.Status400BadRequest)
|
|
.ProducesProblem(StatusCodes.Status422UnprocessableEntity);
|
|
|
|
return routes;
|
|
}
|
|
|
|
private static async Task<IResult> SealAsync(
|
|
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
|
[FromBody] SealRequest request,
|
|
ISealedModeService service,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(tenantId))
|
|
{
|
|
tenantId = "default";
|
|
}
|
|
|
|
try
|
|
{
|
|
var response = await service.SealAsync(tenantId, request, cancellationToken).ConfigureAwait(false);
|
|
return Results.Ok(response);
|
|
}
|
|
catch (SealedModeException ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(ex);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(
|
|
SealedModeErrorCodes.SealFailed,
|
|
ex.Message,
|
|
"Ensure all required parameters are provided");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(
|
|
SealedModeErrorCodes.SealFailed,
|
|
$"Seal operation failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static async Task<IResult> UnsealAsync(
|
|
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
|
ISealedModeService service,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(tenantId))
|
|
{
|
|
tenantId = "default";
|
|
}
|
|
|
|
try
|
|
{
|
|
var response = await service.UnsealAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
|
return Results.Ok(response);
|
|
}
|
|
catch (SealedModeException ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(ex);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(
|
|
SealedModeErrorCodes.UnsealFailed,
|
|
$"Unseal operation failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static async Task<IResult> GetStatusAsync(
|
|
[FromHeader(Name = "X-Tenant-Id")] string? tenantId,
|
|
ISealedModeService service,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(tenantId))
|
|
{
|
|
tenantId = "default";
|
|
}
|
|
|
|
var status = await service.GetStatusAsync(tenantId, cancellationToken).ConfigureAwait(false);
|
|
return Results.Ok(status);
|
|
}
|
|
|
|
private static async Task<IResult> VerifyBundleAsync(
|
|
[FromBody] BundleVerifyRequest request,
|
|
ISealedModeService service,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var response = await service.VerifyBundleAsync(request, cancellationToken).ConfigureAwait(false);
|
|
|
|
// Return problem details if verification failed
|
|
if (!response.Valid && response.VerificationResult.Error is not null)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(
|
|
SealedModeErrorCodes.SignatureInvalid,
|
|
response.VerificationResult.Error,
|
|
"Verify bundle integrity and trust root configuration",
|
|
422);
|
|
}
|
|
|
|
return Results.Ok(response);
|
|
}
|
|
catch (SealedModeException ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(ex);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(
|
|
SealedModeErrorCodes.BundleInvalid,
|
|
ex.Message,
|
|
"Ensure bundle path is valid and accessible");
|
|
}
|
|
catch (FileNotFoundException ex)
|
|
{
|
|
return SealedModeResultHelper.ToProblem(
|
|
SealedModeErrorCodes.BundleInvalid,
|
|
$"Bundle file not found: {ex.FileName ?? ex.Message}",
|
|
"Verify the bundle path is correct");
|
|
}
|
|
}
|
|
}
|