#!/bin/bash # ----------------------------------------------------------------------------- # rotate-signing-key.sh # Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance # Task: WORKFLOW-002 - Create key rotation workflow script # Description: Rotate organization signing key with dual-key grace period # ----------------------------------------------------------------------------- set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "${BLUE}[STEP]${NC} $1"; } usage() { echo "Usage: $0 [options]" echo "" echo "Rotate organization signing key through a dual-key grace period." echo "" echo "Phases:" echo " generate Generate new signing key" echo " activate Activate new key (dual-key period starts)" echo " verify Verify both keys are functional" echo " retire Retire old key (after grace period)" echo "" echo "Options:" echo " --key-dir DIR Directory for signing keys (default: /etc/stellaops/keys)" echo " --key-type TYPE Key type: ecdsa-p256, ecdsa-p384, rsa-4096 (default: ecdsa-p256)" echo " --new-key NAME Name for new key (default: signing-key-v{N+1})" echo " --old-key NAME Name of old key to retire" echo " --grace-days N Grace period in days (default: 14)" echo " --ci-config FILE CI config file to update" echo " -h, --help Show this help message" echo "" echo "Example (4-phase rotation):" echo " # Phase 1: Generate new key" echo " $0 generate --key-dir /etc/stellaops/keys" echo "" echo " # Phase 2: Activate (update CI to use both keys)" echo " $0 activate --ci-config .gitea/workflows/ci.yaml" echo "" echo " # Wait for grace period" echo " sleep 14d" echo "" echo " # Phase 3: Verify" echo " $0 verify" echo "" echo " # Phase 4: Retire old key" echo " $0 retire --old-key signing-key-v1" exit 1 } PHASE="" KEY_DIR="/etc/stellaops/keys" KEY_TYPE="ecdsa-p256" NEW_KEY_NAME="" OLD_KEY_NAME="" GRACE_DAYS=14 CI_CONFIG="" while [[ $# -gt 0 ]]; do case $1 in generate|activate|verify|retire) PHASE="$1" shift ;; --key-dir) KEY_DIR="$2"; shift 2 ;; --key-type) KEY_TYPE="$2"; shift 2 ;; --new-key) NEW_KEY_NAME="$2"; shift 2 ;; --old-key) OLD_KEY_NAME="$2"; shift 2 ;; --grace-days) GRACE_DAYS="$2"; shift 2 ;; --ci-config) CI_CONFIG="$2"; shift 2 ;; -h|--help) usage ;; *) log_error "Unknown argument: $1"; usage ;; esac done if [[ -z "$PHASE" ]]; then log_error "Phase is required" usage fi echo "" echo "================================================" echo " Signing Key Rotation - Phase: $PHASE" echo "================================================" echo "" case "$PHASE" in generate) log_step "Generating new signing key..." mkdir -p "$KEY_DIR" chmod 700 "$KEY_DIR" # Determine new key name if not specified if [[ -z "$NEW_KEY_NAME" ]]; then HIGHEST=$(ls "$KEY_DIR" 2>/dev/null | grep -E '^signing-key-v[0-9]+' | \ sed 's/signing-key-v//' | sed 's/\.pem$//' | sort -n | tail -1 || echo "0") NEW_VERSION=$((HIGHEST + 1)) NEW_KEY_NAME="signing-key-v${NEW_VERSION}" fi NEW_KEY_PATH="$KEY_DIR/${NEW_KEY_NAME}.pem" NEW_PUB_PATH="$KEY_DIR/${NEW_KEY_NAME}.pub" if [[ -f "$NEW_KEY_PATH" ]]; then log_error "Key already exists: $NEW_KEY_PATH" exit 1 fi case "$KEY_TYPE" in ecdsa-p256) openssl ecparam -name prime256v1 -genkey -noout -out "$NEW_KEY_PATH" openssl ec -in "$NEW_KEY_PATH" -pubout -out "$NEW_PUB_PATH" 2>/dev/null ;; ecdsa-p384) openssl ecparam -name secp384r1 -genkey -noout -out "$NEW_KEY_PATH" openssl ec -in "$NEW_KEY_PATH" -pubout -out "$NEW_PUB_PATH" 2>/dev/null ;; rsa-4096) openssl genrsa -out "$NEW_KEY_PATH" 4096 openssl rsa -in "$NEW_KEY_PATH" -pubout -out "$NEW_PUB_PATH" 2>/dev/null ;; *) log_error "Unknown key type: $KEY_TYPE" exit 1 ;; esac chmod 600 "$NEW_KEY_PATH" chmod 644 "$NEW_PUB_PATH" log_info "" log_info "New signing key generated:" log_info " Private key: $NEW_KEY_PATH" log_info " Public key: $NEW_PUB_PATH" log_info "" log_info "Key fingerprint:" openssl dgst -sha256 -r "$NEW_PUB_PATH" | cut -d' ' -f1 log_info "" log_warn "Store the public key securely for distribution." log_warn "Next: Run '$0 activate' to enable dual-key signing." ;; activate) log_step "Activating dual-key signing..." # List available keys log_info "Available signing keys in $KEY_DIR:" ls -la "$KEY_DIR"/*.pem 2>/dev/null || log_warn "No .pem files found" if [[ -n "$CI_CONFIG" ]] && [[ -f "$CI_CONFIG" ]]; then log_info "" log_info "CI config file: $CI_CONFIG" log_warn "Manual update required:" echo " 1. Add the new key path to signing configuration" echo " 2. Ensure both old and new keys can sign" echo " 3. Update verification to accept both key signatures" fi log_info "" log_info "Dual-key activation checklist:" echo " [ ] New key added to CI/CD pipeline" echo " [ ] New public key distributed to verifiers" echo " [ ] Both keys tested for signing" echo " [ ] Grace period documented: $GRACE_DAYS days" log_info "" log_warn "Grace period starts now. Do not retire old key for $GRACE_DAYS days." log_info "Next: Run '$0 verify' to confirm both keys work." ;; verify) log_step "Verifying signing key status..." # Test each key log_info "Testing signing keys in $KEY_DIR:" TEST_FILE=$(mktemp) echo "StellaOps key rotation verification $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TEST_FILE" for keyfile in "$KEY_DIR"/*.pem; do if [[ -f "$keyfile" ]]; then keyname=$(basename "$keyfile" .pem) TEST_SIG=$(mktemp) if openssl dgst -sha256 -sign "$keyfile" -out "$TEST_SIG" "$TEST_FILE" 2>/dev/null; then log_info " $keyname: OK (signing works)" else log_warn " $keyname: FAILED (cannot sign)" fi rm -f "$TEST_SIG" fi done rm -f "$TEST_FILE" log_info "" log_info "Verification checklist:" echo " [ ] All active keys can sign successfully" echo " [ ] Old attestations still verify" echo " [ ] New attestations verify with new key" echo " [ ] Verifiers have both public keys" ;; retire) if [[ -z "$OLD_KEY_NAME" ]]; then log_error "retire requires --old-key" usage fi OLD_KEY_PATH="$KEY_DIR/${OLD_KEY_NAME}.pem" OLD_PUB_PATH="$KEY_DIR/${OLD_KEY_NAME}.pub" if [[ ! -f "$OLD_KEY_PATH" ]] && [[ ! -f "$KEY_DIR/${OLD_KEY_NAME}" ]]; then log_error "Old key not found: $OLD_KEY_NAME" exit 1 fi log_step "Retiring old signing key: $OLD_KEY_NAME" log_warn "This is IRREVERSIBLE. Ensure:" echo " 1. Grace period ($GRACE_DAYS days) has passed" echo " 2. All systems have been updated to use new key" echo " 3. Old attestations have been resigned or archived" read -p "Type 'RETIRE' to proceed: " CONFIRM if [[ "$CONFIRM" != "RETIRE" ]]; then log_error "Aborted" exit 1 fi # Archive old key (don't delete immediately) ARCHIVE_DIR="$KEY_DIR/archived" mkdir -p "$ARCHIVE_DIR" chmod 700 "$ARCHIVE_DIR" TIMESTAMP=$(date -u +%Y%m%d%H%M%S) if [[ -f "$OLD_KEY_PATH" ]]; then mv "$OLD_KEY_PATH" "$ARCHIVE_DIR/${OLD_KEY_NAME}-retired-${TIMESTAMP}.pem" fi if [[ -f "$OLD_PUB_PATH" ]]; then mv "$OLD_PUB_PATH" "$ARCHIVE_DIR/${OLD_KEY_NAME}-retired-${TIMESTAMP}.pub" fi log_info "" log_info "Old key archived to: $ARCHIVE_DIR/" log_info "Key rotation complete!" log_warn "" log_warn "Post-retirement checklist:" echo " [ ] Remove old key from CI/CD configuration" echo " [ ] Update documentation" echo " [ ] Notify stakeholders of completion" echo " [ ] Delete archived key after retention period" ;; esac echo ""