using System.CommandLine; using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Cli.Extensions; namespace StellaOps.Cli.Commands.Proof; /// /// Command group for key rotation operations. /// Sprint: SPRINT_0501_0008_0001_proof_chain_key_rotation /// Task: PROOF-KEY-0011 /// Implements advisory §8.2 key rotation commands. /// public class KeyRotationCommandGroup { private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; public KeyRotationCommandGroup(ILogger logger) { _logger = logger; } /// /// Build the key rotation command tree. /// public Command BuildCommand() { var keyCommand = new Command("key", "Key management and rotation commands"); keyCommand.Add(BuildListCommand()); keyCommand.Add(BuildAddCommand()); keyCommand.Add(BuildRevokeCommand()); keyCommand.Add(BuildRotateCommand()); keyCommand.Add(BuildStatusCommand()); keyCommand.Add(BuildHistoryCommand()); keyCommand.Add(BuildVerifyCommand()); return keyCommand; } private Command BuildListCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var includeRevokedOption = new Option("--include-revoked") { Description = "Include revoked keys in output" }.SetDefaultValue(false); var outputOption = new Option("--output") { Description = "Output format: text, json" }.SetDefaultValue("text").FromAmong("text", "json"); var listCommand = new Command("list", "List keys for a trust anchor") { anchorArg, includeRevokedOption, outputOption }; listCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var includeRevoked = parseResult.GetValue(includeRevokedOption); var output = parseResult.GetValue(outputOption) ?? "text"; Environment.ExitCode = await ListKeysAsync(anchorId, includeRevoked, output, ct).ConfigureAwait(false); }); return listCommand; } private Command BuildAddCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var keyIdArg = new Argument("keyId") { Description = "New key ID" }; var algorithmOption = new Option("--algorithm", new[] { "-a" }) { Description = "Key algorithm: Ed25519, ES256, ES384, RS256" }.SetDefaultValue("Ed25519").FromAmong("Ed25519", "ES256", "ES384", "RS256"); var publicKeyOption = new Option("--public-key") { Description = "Path to public key file (PEM format)" }; var notesOption = new Option("--notes") { Description = "Human-readable notes about the key" }; var addCommand = new Command("add", "Add a new key to a trust anchor") { anchorArg, keyIdArg, algorithmOption, publicKeyOption, notesOption }; addCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var keyId = parseResult.GetValue(keyIdArg); var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519"; var publicKeyPath = parseResult.GetValue(publicKeyOption); var notes = parseResult.GetValue(notesOption); Environment.ExitCode = await AddKeyAsync(anchorId, keyId, algorithm, publicKeyPath, notes, ct).ConfigureAwait(false); }); return addCommand; } private Command BuildRevokeCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var keyIdArg = new Argument("keyId") { Description = "Key ID to revoke" }; var reasonOption = new Option("--reason", new[] { "-r" }) { Description = "Reason for revocation" }.SetDefaultValue("rotation-complete"); var effectiveOption = new Option("--effective-at") { Description = "Effective revocation time (default: now). ISO-8601 format." }; var forceOption = new Option("--force") { Description = "Skip confirmation prompt" }.SetDefaultValue(false); var revokeCommand = new Command("revoke", "Revoke a key from a trust anchor") { anchorArg, keyIdArg, reasonOption, effectiveOption, forceOption }; revokeCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var keyId = parseResult.GetValue(keyIdArg); var reason = parseResult.GetValue(reasonOption) ?? "rotation-complete"; var effectiveAt = parseResult.GetValue(effectiveOption) ?? DateTimeOffset.UtcNow; var force = parseResult.GetValue(forceOption); Environment.ExitCode = await RevokeKeyAsync(anchorId, keyId, reason, effectiveAt, force, ct).ConfigureAwait(false); }); return revokeCommand; } private Command BuildRotateCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var oldKeyIdArg = new Argument("oldKeyId") { Description = "Old key ID to replace" }; var newKeyIdArg = new Argument("newKeyId") { Description = "New key ID" }; var algorithmOption = new Option("--algorithm", new[] { "-a" }) { Description = "Key algorithm: Ed25519, ES256, ES384, RS256" }.SetDefaultValue("Ed25519").FromAmong("Ed25519", "ES256", "ES384", "RS256"); var publicKeyOption = new Option("--public-key") { Description = "Path to new public key file (PEM format)" }; var overlapOption = new Option("--overlap-days") { Description = "Days to keep both keys active before revoking old" }.SetDefaultValue(30); var rotateCommand = new Command("rotate", "Rotate a key (add new, schedule old revocation)") { anchorArg, oldKeyIdArg, newKeyIdArg, algorithmOption, publicKeyOption, overlapOption }; rotateCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var oldKeyId = parseResult.GetValue(oldKeyIdArg); var newKeyId = parseResult.GetValue(newKeyIdArg); var algorithm = parseResult.GetValue(algorithmOption) ?? "Ed25519"; var publicKeyPath = parseResult.GetValue(publicKeyOption); var overlapDays = parseResult.GetValue(overlapOption); Environment.ExitCode = await RotateKeyAsync(anchorId, oldKeyId, newKeyId, algorithm, publicKeyPath, overlapDays, ct).ConfigureAwait(false); }); return rotateCommand; } private Command BuildStatusCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var outputOption = new Option("--output") { Description = "Output format: text, json" }.SetDefaultValue("text").FromAmong("text", "json"); var statusCommand = new Command("status", "Show key rotation status and warnings") { anchorArg, outputOption }; statusCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var output = parseResult.GetValue(outputOption) ?? "text"; Environment.ExitCode = await ShowStatusAsync(anchorId, output, ct).ConfigureAwait(false); }); return statusCommand; } private Command BuildHistoryCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var keyIdOption = new Option("--key-id", new[] { "-k" }) { Description = "Filter by specific key ID" }; var limitOption = new Option("--limit") { Description = "Maximum entries to show" }.SetDefaultValue(50); var outputOption = new Option("--output") { Description = "Output format: text, json" }.SetDefaultValue("text").FromAmong("text", "json"); var historyCommand = new Command("history", "Show key audit history") { anchorArg, keyIdOption, limitOption, outputOption }; historyCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var keyId = parseResult.GetValue(keyIdOption); var limit = parseResult.GetValue(limitOption); var output = parseResult.GetValue(outputOption) ?? "text"; Environment.ExitCode = await ShowHistoryAsync(anchorId, keyId, limit, output, ct).ConfigureAwait(false); }); return historyCommand; } private Command BuildVerifyCommand() { var anchorArg = new Argument("anchorId") { Description = "Trust anchor ID" }; var keyIdArg = new Argument("keyId") { Description = "Key ID to verify" }; var signedAtOption = new Option("--signed-at", new[] { "-t" }) { Description = "Verify key was valid at this time (ISO-8601)" }; var verifyCommand = new Command("verify", "Verify a key's validity at a point in time") { anchorArg, keyIdArg, signedAtOption }; verifyCommand.SetAction(async (parseResult, ct) => { var anchorId = parseResult.GetValue(anchorArg); var keyId = parseResult.GetValue(keyIdArg); var signedAt = parseResult.GetValue(signedAtOption) ?? DateTimeOffset.UtcNow; Environment.ExitCode = await VerifyKeyAsync(anchorId, keyId, signedAt, ct).ConfigureAwait(false); }); return verifyCommand; } #region Handler Implementations private async Task ListKeysAsync(Guid anchorId, bool includeRevoked, string output, CancellationToken ct) { try { _logger.LogInformation("Listing keys for anchor {AnchorId}, includeRevoked={IncludeRevoked}", anchorId, includeRevoked); // TODO: Wire up to IKeyRotationService when DI is available if (output == "json") { var result = new { anchorId = anchorId.ToString(), activeKeys = Array.Empty(), revokedKeys = includeRevoked ? Array.Empty() : null }; Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); } else { Console.WriteLine($"Keys for Trust Anchor: {anchorId}"); Console.WriteLine("═════════════════════════════════════════════"); Console.WriteLine(); Console.WriteLine("Active Keys:"); Console.WriteLine(" (No active keys found - connect to service)"); if (includeRevoked) { Console.WriteLine(); Console.WriteLine("Revoked Keys:"); Console.WriteLine(" (No revoked keys found - connect to service)"); } } return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to list keys for anchor {AnchorId}", anchorId); return ProofExitCodes.SystemError; } } private async Task AddKeyAsync(Guid anchorId, string keyId, string algorithm, string? publicKeyPath, string? notes, CancellationToken ct) { try { _logger.LogInformation("Adding key {KeyId} to anchor {AnchorId}", keyId, anchorId); string? publicKey = null; if (publicKeyPath != null) { if (!File.Exists(publicKeyPath)) { Console.Error.WriteLine($"Error: Public key file not found: {publicKeyPath}"); return ProofExitCodes.SystemError; } publicKey = await File.ReadAllTextAsync(publicKeyPath, ct); } // TODO: Wire up to IKeyRotationService.AddKeyAsync Console.WriteLine("Adding key to trust anchor..."); Console.WriteLine($" Anchor: {anchorId}"); Console.WriteLine($" Key ID: {keyId}"); Console.WriteLine($" Algorithm: {algorithm}"); Console.WriteLine($" Public Key: {(publicKey != null ? "Provided" : "Not specified")}"); if (notes != null) Console.WriteLine($" Notes: {notes}"); Console.WriteLine(); Console.WriteLine("✓ Key added successfully (simulation)"); return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to add key {KeyId} to anchor {AnchorId}", keyId, anchorId); return ProofExitCodes.SystemError; } } private async Task RevokeKeyAsync(Guid anchorId, string keyId, string reason, DateTimeOffset effectiveAt, bool force, CancellationToken ct) { try { _logger.LogInformation("Revoking key {KeyId} from anchor {AnchorId}", keyId, anchorId); if (!force) { Console.Write($"Revoke key '{keyId}' from anchor {anchorId}? [y/N] "); var response = Console.ReadLine(); if (response?.ToLowerInvariant() != "y") { Console.WriteLine("Cancelled."); return ProofExitCodes.Success; } } // TODO: Wire up to IKeyRotationService.RevokeKeyAsync Console.WriteLine("Revoking key..."); Console.WriteLine($" Anchor: {anchorId}"); Console.WriteLine($" Key ID: {keyId}"); Console.WriteLine($" Reason: {reason}"); Console.WriteLine($" Effective At: {effectiveAt:O}"); Console.WriteLine(); Console.WriteLine("✓ Key revoked successfully (simulation)"); Console.WriteLine(); Console.WriteLine("Note: Proofs signed before revocation remain valid."); return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to revoke key {KeyId} from anchor {AnchorId}", keyId, anchorId); return ProofExitCodes.SystemError; } } private async Task RotateKeyAsync(Guid anchorId, string oldKeyId, string newKeyId, string algorithm, string? publicKeyPath, int overlapDays, CancellationToken ct) { try { _logger.LogInformation("Rotating key {OldKeyId} -> {NewKeyId} for anchor {AnchorId}", oldKeyId, newKeyId, anchorId); string? publicKey = null; if (publicKeyPath != null) { if (!File.Exists(publicKeyPath)) { Console.Error.WriteLine($"Error: Public key file not found: {publicKeyPath}"); return ProofExitCodes.SystemError; } publicKey = await File.ReadAllTextAsync(publicKeyPath, ct); } var revokeAt = DateTimeOffset.UtcNow.AddDays(overlapDays); // TODO: Wire up to IKeyRotationService Console.WriteLine("Key Rotation Plan"); Console.WriteLine("═════════════════"); Console.WriteLine($" Anchor: {anchorId}"); Console.WriteLine($" Old Key: {oldKeyId}"); Console.WriteLine($" New Key: {newKeyId}"); Console.WriteLine($" Algorithm: {algorithm}"); Console.WriteLine($" Overlap Period: {overlapDays} days"); Console.WriteLine($" Old Key Revokes At: {revokeAt:O}"); Console.WriteLine(); Console.WriteLine("Step 1: Add new key to allowedKeyIds..."); Console.WriteLine(" ✓ Key added (simulation)"); Console.WriteLine(); Console.WriteLine("Step 2: Schedule old key revocation..."); Console.WriteLine($" ✓ Old key will be revoked on {revokeAt:yyyy-MM-dd} (simulation)"); Console.WriteLine(); Console.WriteLine("✓ Key rotation initiated successfully"); Console.WriteLine(); Console.WriteLine("Next Steps:"); Console.WriteLine($" 1. Start using '{newKeyId}' for new signatures"); Console.WriteLine($" 2. Old key remains valid until {revokeAt:yyyy-MM-dd}"); Console.WriteLine($" 3. Run 'stellaops key status {anchorId}' to check rotation warnings"); return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to rotate key {OldKeyId} -> {NewKeyId} for anchor {AnchorId}", oldKeyId, newKeyId, anchorId); return ProofExitCodes.SystemError; } } private async Task ShowStatusAsync(Guid anchorId, string output, CancellationToken ct) { try { _logger.LogInformation("Showing key status for anchor {AnchorId}", anchorId); // TODO: Wire up to IKeyRotationService.GetRotationWarningsAsync if (output == "json") { var result = new { anchorId = anchorId.ToString(), status = "healthy", warnings = Array.Empty() }; Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); } else { Console.WriteLine($"Key Status for Trust Anchor: {anchorId}"); Console.WriteLine("═════════════════════════════════════════════"); Console.WriteLine(); Console.WriteLine("Overall Status: ✓ Healthy (simulation)"); Console.WriteLine(); Console.WriteLine("Active Keys: 0"); Console.WriteLine("Revoked Keys: 0"); Console.WriteLine(); Console.WriteLine("Rotation Warnings: None"); } return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to show status for anchor {AnchorId}", anchorId); return ProofExitCodes.SystemError; } } private async Task ShowHistoryAsync(Guid anchorId, string? keyId, int limit, string output, CancellationToken ct) { try { _logger.LogInformation("Showing key history for anchor {AnchorId}, keyId={KeyId}, limit={Limit}", anchorId, keyId, limit); // TODO: Wire up to IKeyRotationService.GetKeyHistoryAsync if (output == "json") { var result = new { anchorId = anchorId.ToString(), keyId = keyId, entries = Array.Empty() }; Console.WriteLine(JsonSerializer.Serialize(result, JsonOptions)); } else { Console.WriteLine($"Key Audit History for Trust Anchor: {anchorId}"); if (keyId != null) Console.WriteLine($" Filtered by Key: {keyId}"); Console.WriteLine("═════════════════════════════════════════════"); Console.WriteLine(); Console.WriteLine("Timestamp | Operation | Key ID | Operator"); Console.WriteLine("───────────────────────────────────────────────────────────────────"); Console.WriteLine("(No history entries - connect to service)"); } return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to show history for anchor {AnchorId}", anchorId); return ProofExitCodes.SystemError; } } private async Task VerifyKeyAsync(Guid anchorId, string keyId, DateTimeOffset signedAt, CancellationToken ct) { try { _logger.LogInformation("Verifying key {KeyId} validity at {SignedAt} for anchor {AnchorId}", keyId, signedAt, anchorId); // TODO: Wire up to IKeyRotationService.CheckKeyValidityAsync Console.WriteLine($"Key Validity Check"); Console.WriteLine("═════════════════════════════════════════════"); Console.WriteLine($" Anchor: {anchorId}"); Console.WriteLine($" Key ID: {keyId}"); Console.WriteLine($" Time: {signedAt:O}"); Console.WriteLine(); Console.WriteLine("Result: ⚠ Unknown (connect to service for verification)"); Console.WriteLine(); Console.WriteLine("Temporal validation checks:"); Console.WriteLine(" [ ] Key existed at specified time"); Console.WriteLine(" [ ] Key was not revoked before specified time"); Console.WriteLine(" [ ] Key algorithm is currently trusted"); return ProofExitCodes.Success; } catch (Exception ex) { _logger.LogError(ex, "Failed to verify key {KeyId} for anchor {AnchorId}", keyId, anchorId); return ProofExitCodes.SystemError; } } #endregion }