266 lines
8.9 KiB
Bash
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 ""
|