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