- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
439 lines
15 KiB
C#
439 lines
15 KiB
C#
// -----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
/// <summary>
|
|
/// API endpoints for key rotation operations.
|
|
/// Implements advisory §8.2 key rotation workflow.
|
|
/// </summary>
|
|
public static class KeyRotationEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Map key rotation endpoints to the router.
|
|
/// </summary>
|
|
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<AddKeyResponseDto>(StatusCodes.Status201Created)
|
|
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
|
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapPost("/{anchorId:guid}/keys/{keyId}/revoke", RevokeKeyAsync)
|
|
.WithName("RevokeKey")
|
|
.WithSummary("Revoke a signing key from a trust anchor")
|
|
.Produces<RevokeKeyResponseDto>(StatusCodes.Status200OK)
|
|
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
|
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/{anchorId:guid}/keys/{keyId}/validity", CheckKeyValidityAsync)
|
|
.WithName("CheckKeyValidity")
|
|
.WithSummary("Check if a key was valid at a specific time")
|
|
.Produces<KeyValidityResponseDto>(StatusCodes.Status200OK)
|
|
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/{anchorId:guid}/keys/history", GetKeyHistoryAsync)
|
|
.WithName("GetKeyHistory")
|
|
.WithSummary("Get the full key history for a trust anchor")
|
|
.Produces<KeyHistoryResponseDto>(StatusCodes.Status200OK)
|
|
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
|
|
|
group.MapGet("/{anchorId:guid}/keys/warnings", GetRotationWarningsAsync)
|
|
.WithName("GetRotationWarnings")
|
|
.WithSummary("Get rotation warnings for a trust anchor")
|
|
.Produces<RotationWarningsResponseDto>(StatusCodes.Status200OK)
|
|
.Produces<ProblemDetails>(StatusCodes.Status404NotFound);
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a new signing key to a trust anchor.
|
|
/// </summary>
|
|
private static async Task<IResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Revoke a signing key from a trust anchor.
|
|
/// </summary>
|
|
private static async Task<IResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a key was valid at a specific time.
|
|
/// </summary>
|
|
private static async Task<IResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the full key history for a trust anchor.
|
|
/// </summary>
|
|
private static async Task<IResult> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get rotation warnings for a trust anchor.
|
|
/// </summary>
|
|
private static async Task<IResult> 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
|
|
|
|
/// <summary>
|
|
/// Request DTO for adding a key.
|
|
/// </summary>
|
|
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<string, string>? Metadata { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response DTO for adding a key.
|
|
/// </summary>
|
|
public sealed record AddKeyResponseDto
|
|
{
|
|
public required string KeyId { get; init; }
|
|
public required Guid AnchorId { get; init; }
|
|
public required List<string> AllowedKeyIds { get; init; }
|
|
public Guid? AuditLogId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request DTO for revoking a key.
|
|
/// </summary>
|
|
public sealed record RevokeKeyRequestDto
|
|
{
|
|
[Required]
|
|
public required string Reason { get; init; }
|
|
|
|
public DateTimeOffset? EffectiveAt { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response DTO for revoking a key.
|
|
/// </summary>
|
|
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<string> AllowedKeyIds { get; init; }
|
|
public required List<string> RevokedKeyIds { get; init; }
|
|
public Guid? AuditLogId { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response DTO for key validity check.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response DTO for key history.
|
|
/// </summary>
|
|
public sealed record KeyHistoryResponseDto
|
|
{
|
|
public required Guid AnchorId { get; init; }
|
|
public required List<KeyHistoryEntryDto> Entries { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTO for a key history entry.
|
|
/// </summary>
|
|
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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Response DTO for rotation warnings.
|
|
/// </summary>
|
|
public sealed record RotationWarningsResponseDto
|
|
{
|
|
public required Guid AnchorId { get; init; }
|
|
public required List<RotationWarningDto> Warnings { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// DTO for a rotation warning.
|
|
/// </summary>
|
|
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
|