Files
git.stella-ops.org/devops/scripts/rotate-signing-key.sh

266 lines
8.9 KiB
Bash

#!/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 <phase> [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 ""