// ----------------------------------------------------------------------------- // KeyRotationEndpoints.cs // Sprint: SPRINT_0501_0008_0001_proof_chain_key_rotation // Task: PROOF-KEY-0010 - Implement key rotation API endpoints // Description: API endpoints for key rotation and trust anchor management // ----------------------------------------------------------------------------- using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using StellaOps.Signer.KeyManagement; namespace StellaOps.Signer.WebService.Endpoints; /// /// API endpoints for key rotation operations. /// Implements advisory ยง8.2 key rotation workflow. /// public static class KeyRotationEndpoints { /// /// Map key rotation endpoints to the router. /// public static IEndpointRouteBuilder MapKeyRotationEndpoints(this IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("/api/v1/anchors") .WithTags("KeyRotation", "TrustAnchors") .RequireAuthorization("KeyManagement"); // Key management endpoints group.MapPost("/{anchorId:guid}/keys", AddKeyAsync) .WithName("AddKey") .WithSummary("Add a new signing key to a trust anchor") .Produces(StatusCodes.Status201Created) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); group.MapPost("/{anchorId:guid}/keys/{keyId}/revoke", RevokeKeyAsync) .WithName("RevokeKey") .WithSummary("Revoke a signing key from a trust anchor") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound); group.MapGet("/{anchorId:guid}/keys/{keyId}/validity", CheckKeyValidityAsync) .WithName("CheckKeyValidity") .WithSummary("Check if a key was valid at a specific time") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapGet("/{anchorId:guid}/keys/history", GetKeyHistoryAsync) .WithName("GetKeyHistory") .WithSummary("Get the full key history for a trust anchor") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); group.MapGet("/{anchorId:guid}/keys/warnings", GetRotationWarningsAsync) .WithName("GetRotationWarnings") .WithSummary("Get rotation warnings for a trust anchor") .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status404NotFound); return endpoints; } /// /// Add a new signing key to a trust anchor. /// private static async Task AddKeyAsync( [FromRoute] Guid anchorId, [FromBody] AddKeyRequestDto request, IKeyRotationService rotationService, ILoggerFactory loggerFactory, CancellationToken ct) { var logger = loggerFactory.CreateLogger("KeyRotationEndpoints.AddKey"); if (request is null) { return Results.Problem( title: "Invalid request", detail: "Request body is required.", statusCode: StatusCodes.Status400BadRequest); } try { var addRequest = new AddKeyRequest { KeyId = request.KeyId, PublicKey = request.PublicKey, Algorithm = request.Algorithm, ExpiresAt = request.ExpiresAt, Metadata = request.Metadata }; var result = await rotationService.AddKeyAsync(anchorId, addRequest, ct); if (!result.Success) { return Results.Problem( title: "Key addition failed", detail: result.ErrorMessage, statusCode: StatusCodes.Status400BadRequest); } logger.LogInformation( "Added key {KeyId} to anchor {AnchorId}, audit log {AuditLogId}", request.KeyId, anchorId, result.AuditLogId); var response = new AddKeyResponseDto { KeyId = request.KeyId, AnchorId = anchorId, AllowedKeyIds = result.AllowedKeyIds.ToList(), AuditLogId = result.AuditLogId }; return Results.Created($"/api/v1/anchors/{anchorId}/keys/{request.KeyId}", response); } catch (KeyNotFoundException) { return Results.Problem( title: "Anchor not found", detail: $"Trust anchor {anchorId} not found.", statusCode: StatusCodes.Status404NotFound); } catch (Exception ex) { logger.LogError(ex, "Failed to add key {KeyId} to anchor {AnchorId}", request.KeyId, anchorId); return Results.Problem( title: "Internal error", detail: "An unexpected error occurred.", statusCode: StatusCodes.Status500InternalServerError); } } /// /// Revoke a signing key from a trust anchor. /// private static async Task RevokeKeyAsync( [FromRoute] Guid anchorId, [FromRoute] string keyId, [FromBody] RevokeKeyRequestDto request, IKeyRotationService rotationService, ILoggerFactory loggerFactory, CancellationToken ct) { var logger = loggerFactory.CreateLogger("KeyRotationEndpoints.RevokeKey"); if (request is null || string.IsNullOrWhiteSpace(request.Reason)) { return Results.Problem( title: "Invalid request", detail: "Revocation reason is required.", statusCode: StatusCodes.Status400BadRequest); } try { var revokeRequest = new RevokeKeyRequest { Reason = request.Reason, EffectiveAt = request.EffectiveAt }; var result = await rotationService.RevokeKeyAsync(anchorId, keyId, revokeRequest, ct); if (!result.Success) { return Results.Problem( title: "Key revocation failed", detail: result.ErrorMessage, statusCode: StatusCodes.Status400BadRequest); } logger.LogInformation( "Revoked key {KeyId} from anchor {AnchorId}, reason: {Reason}, audit log {AuditLogId}", keyId, anchorId, request.Reason, result.AuditLogId); var response = new RevokeKeyResponseDto { KeyId = keyId, AnchorId = anchorId, RevokedAt = request.EffectiveAt ?? DateTimeOffset.UtcNow, Reason = request.Reason, AllowedKeyIds = result.AllowedKeyIds.ToList(), RevokedKeyIds = result.RevokedKeyIds.ToList(), AuditLogId = result.AuditLogId }; return Results.Ok(response); } catch (KeyNotFoundException) { return Results.Problem( title: "Key or anchor not found", detail: $"Trust anchor {anchorId} or key {keyId} not found.", statusCode: StatusCodes.Status404NotFound); } catch (Exception ex) { logger.LogError(ex, "Failed to revoke key {KeyId} from anchor {AnchorId}", keyId, anchorId); return Results.Problem( title: "Internal error", detail: "An unexpected error occurred.", statusCode: StatusCodes.Status500InternalServerError); } } /// /// Check if a key was valid at a specific time. /// private static async Task CheckKeyValidityAsync( [FromRoute] Guid anchorId, [FromRoute] string keyId, [FromQuery] DateTimeOffset? signedAt, IKeyRotationService rotationService, CancellationToken ct) { var checkTime = signedAt ?? DateTimeOffset.UtcNow; try { var result = await rotationService.CheckKeyValidityAsync(anchorId, keyId, checkTime, ct); var response = new KeyValidityResponseDto { KeyId = keyId, AnchorId = anchorId, CheckedAt = checkTime, IsValid = result.IsValid, Status = result.Status.ToString(), AddedAt = result.AddedAt, RevokedAt = result.RevokedAt, InvalidReason = result.InvalidReason }; return Results.Ok(response); } catch (KeyNotFoundException) { return Results.Problem( title: "Key or anchor not found", detail: $"Trust anchor {anchorId} or key {keyId} not found.", statusCode: StatusCodes.Status404NotFound); } } /// /// Get the full key history for a trust anchor. /// private static async Task GetKeyHistoryAsync( [FromRoute] Guid anchorId, IKeyRotationService rotationService, CancellationToken ct) { try { var history = await rotationService.GetKeyHistoryAsync(anchorId, ct); var response = new KeyHistoryResponseDto { AnchorId = anchorId, Entries = history.Select(e => new KeyHistoryEntryDto { KeyId = e.KeyId, Algorithm = e.Algorithm, AddedAt = e.AddedAt, RevokedAt = e.RevokedAt, RevokeReason = e.RevokeReason, ExpiresAt = e.ExpiresAt }).ToList() }; return Results.Ok(response); } catch (KeyNotFoundException) { return Results.Problem( title: "Anchor not found", detail: $"Trust anchor {anchorId} not found.", statusCode: StatusCodes.Status404NotFound); } } /// /// Get rotation warnings for a trust anchor. /// private static async Task GetRotationWarningsAsync( [FromRoute] Guid anchorId, IKeyRotationService rotationService, CancellationToken ct) { try { var warnings = await rotationService.GetRotationWarningsAsync(anchorId, ct); var response = new RotationWarningsResponseDto { AnchorId = anchorId, Warnings = warnings.Select(w => new RotationWarningDto { KeyId = w.KeyId, WarningType = w.WarningType.ToString(), Message = w.Message, CriticalAt = w.CriticalAt }).ToList() }; return Results.Ok(response); } catch (KeyNotFoundException) { return Results.Problem( title: "Anchor not found", detail: $"Trust anchor {anchorId} not found.", statusCode: StatusCodes.Status404NotFound); } } } #region Request/Response DTOs /// /// Request DTO for adding a key. /// public sealed record AddKeyRequestDto { [Required] public required string KeyId { get; init; } [Required] public required string PublicKey { get; init; } [Required] public required string Algorithm { get; init; } public DateTimeOffset? ExpiresAt { get; init; } public IReadOnlyDictionary? Metadata { get; init; } } /// /// Response DTO for adding a key. /// public sealed record AddKeyResponseDto { public required string KeyId { get; init; } public required Guid AnchorId { get; init; } public required List AllowedKeyIds { get; init; } public Guid? AuditLogId { get; init; } } /// /// Request DTO for revoking a key. /// public sealed record RevokeKeyRequestDto { [Required] public required string Reason { get; init; } public DateTimeOffset? EffectiveAt { get; init; } } /// /// Response DTO for revoking a key. /// public sealed record RevokeKeyResponseDto { public required string KeyId { get; init; } public required Guid AnchorId { get; init; } public required DateTimeOffset RevokedAt { get; init; } public required string Reason { get; init; } public required List AllowedKeyIds { get; init; } public required List RevokedKeyIds { get; init; } public Guid? AuditLogId { get; init; } } /// /// Response DTO for key validity check. /// public sealed record KeyValidityResponseDto { public required string KeyId { get; init; } public required Guid AnchorId { get; init; } public required DateTimeOffset CheckedAt { get; init; } public required bool IsValid { get; init; } public required string Status { get; init; } public required DateTimeOffset AddedAt { get; init; } public DateTimeOffset? RevokedAt { get; init; } public string? InvalidReason { get; init; } } /// /// Response DTO for key history. /// public sealed record KeyHistoryResponseDto { public required Guid AnchorId { get; init; } public required List Entries { get; init; } } /// /// DTO for a key history entry. /// public sealed record KeyHistoryEntryDto { public required string KeyId { get; init; } public required string Algorithm { get; init; } public required DateTimeOffset AddedAt { get; init; } public DateTimeOffset? RevokedAt { get; init; } public string? RevokeReason { get; init; } public DateTimeOffset? ExpiresAt { get; init; } } /// /// Response DTO for rotation warnings. /// public sealed record RotationWarningsResponseDto { public required Guid AnchorId { get; init; } public required List Warnings { get; init; } } /// /// DTO for a rotation warning. /// public sealed record RotationWarningDto { public required string KeyId { get; init; } public required string WarningType { get; init; } public required string Message { get; init; } public DateTimeOffset? CriticalAt { get; init; } } #endregion