// -----------------------------------------------------------------------------
// 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