fix tests. new product advisories enhancements
This commit is contained in:
161
devops/compose/tile-proxy/README.md
Normal file
161
devops/compose/tile-proxy/README.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Tile Proxy Docker Compose
|
||||
|
||||
This directory contains the Docker Compose configuration for deploying the StellaOps Tile Proxy service.
|
||||
|
||||
## Overview
|
||||
|
||||
The Tile Proxy acts as a caching intermediary between StellaOps clients and upstream Rekor transparency logs. It provides:
|
||||
|
||||
- **Tile Caching**: Caches tiles locally for faster subsequent requests
|
||||
- **Request Coalescing**: Deduplicates concurrent requests for the same tile
|
||||
- **Offline Support**: Serves from cache when upstream is unavailable
|
||||
- **TUF Integration**: Optional validation using TUF trust anchors
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Start with default configuration
|
||||
docker compose up -d
|
||||
|
||||
# Check health
|
||||
curl http://localhost:8090/_admin/health
|
||||
|
||||
# View cache statistics
|
||||
curl http://localhost:8090/_admin/cache/stats
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `REKOR_UPSTREAM_URL` | Upstream Rekor URL | `https://rekor.sigstore.dev` |
|
||||
| `REKOR_ORIGIN` | Log origin identifier | `rekor.sigstore.dev - 1985497715` |
|
||||
| `TUF_ENABLED` | Enable TUF integration | `false` |
|
||||
| `TUF_ROOT_URL` | TUF repository URL | - |
|
||||
| `TUF_VALIDATE_CHECKPOINT` | Validate checkpoint signatures | `true` |
|
||||
| `CACHE_MAX_SIZE_GB` | Maximum cache size | `10` |
|
||||
| `CHECKPOINT_TTL_MINUTES` | Checkpoint cache TTL | `5` |
|
||||
| `SYNC_ENABLED` | Enable scheduled sync | `true` |
|
||||
| `SYNC_SCHEDULE` | Sync cron schedule | `0 */6 * * *` |
|
||||
| `SYNC_DEPTH` | Entries to sync tiles for | `10000` |
|
||||
| `LOG_LEVEL` | Logging level | `Information` |
|
||||
|
||||
### Using a .env file
|
||||
|
||||
Create a `.env` file to customize configuration:
|
||||
|
||||
```bash
|
||||
# .env
|
||||
REKOR_UPSTREAM_URL=https://rekor.sigstore.dev
|
||||
CACHE_MAX_SIZE_GB=20
|
||||
SYNC_ENABLED=true
|
||||
SYNC_SCHEDULE=0 */4 * * *
|
||||
LOG_LEVEL=Debug
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Proxy Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /tile/{level}/{index}` | Get a tile (cache-through) |
|
||||
| `GET /tile/{level}/{index}.p/{width}` | Get partial tile |
|
||||
| `GET /checkpoint` | Get current checkpoint |
|
||||
|
||||
### Admin Endpoints
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /_admin/cache/stats` | Cache statistics |
|
||||
| `GET /_admin/metrics` | Proxy metrics |
|
||||
| `POST /_admin/cache/sync` | Trigger manual sync |
|
||||
| `DELETE /_admin/cache/prune` | Prune old tiles |
|
||||
| `GET /_admin/health` | Health check |
|
||||
| `GET /_admin/ready` | Readiness check |
|
||||
|
||||
## Volumes
|
||||
|
||||
| Volume | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `tile-cache` | `/var/cache/stellaops/tiles` | Cached tiles |
|
||||
| `tuf-cache` | `/var/cache/stellaops/tuf` | TUF metadata |
|
||||
|
||||
## Integration with StellaOps
|
||||
|
||||
Configure your StellaOps Attestor to use the tile proxy:
|
||||
|
||||
```yaml
|
||||
attestor:
|
||||
rekor:
|
||||
url: http://tile-proxy:8080
|
||||
# or if running standalone:
|
||||
# url: http://localhost:8090
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
The tile proxy exposes metrics at `/_admin/metrics`:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8090/_admin/metrics
|
||||
```
|
||||
|
||||
Example response:
|
||||
```json
|
||||
{
|
||||
"cacheHits": 12450,
|
||||
"cacheMisses": 234,
|
||||
"hitRatePercent": 98.15,
|
||||
"upstreamRequests": 234,
|
||||
"upstreamErrors": 2,
|
||||
"inflightRequests": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Liveness (is the service running?)
|
||||
curl http://localhost:8090/_admin/health
|
||||
|
||||
# Readiness (can it serve requests?)
|
||||
curl http://localhost:8090/_admin/ready
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Cache is not being used
|
||||
|
||||
1. Check cache stats: `curl http://localhost:8090/_admin/cache/stats`
|
||||
2. Verify cache volume is mounted correctly
|
||||
3. Check logs for write errors
|
||||
|
||||
### Upstream connection failures
|
||||
|
||||
1. Check network connectivity to upstream
|
||||
2. Verify `REKOR_UPSTREAM_URL` is correct
|
||||
3. Check for firewall/proxy issues
|
||||
|
||||
### High memory usage
|
||||
|
||||
1. Reduce `CACHE_MAX_SIZE_GB`
|
||||
2. Trigger manual prune: `curl -X DELETE http://localhost:8090/_admin/cache/prune?targetSizeBytes=5368709120`
|
||||
|
||||
## Development
|
||||
|
||||
Build the image locally:
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
|
||||
Run with local source:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
|
||||
```
|
||||
64
devops/compose/tile-proxy/docker-compose.yml
Normal file
64
devops/compose/tile-proxy/docker-compose.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# docker-compose.yml
|
||||
# Sprint: SPRINT_20260125_002_Attestor_trust_automation
|
||||
# Task: PROXY-008 - Docker Compose for tile-proxy stack
|
||||
# Description: Docker Compose configuration for tile-proxy deployment
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
services:
|
||||
tile-proxy:
|
||||
build:
|
||||
context: ../../..
|
||||
dockerfile: src/Attestor/StellaOps.Attestor.TileProxy/Dockerfile
|
||||
image: stellaops/tile-proxy:latest
|
||||
container_name: stellaops-tile-proxy
|
||||
ports:
|
||||
- "8090:8080"
|
||||
volumes:
|
||||
- tile-cache:/var/cache/stellaops/tiles
|
||||
- tuf-cache:/var/cache/stellaops/tuf
|
||||
environment:
|
||||
# Upstream Rekor configuration
|
||||
- TILE_PROXY__UPSTREAMURL=${REKOR_UPSTREAM_URL:-https://rekor.sigstore.dev}
|
||||
- TILE_PROXY__ORIGIN=${REKOR_ORIGIN:-rekor.sigstore.dev - 1985497715}
|
||||
|
||||
# TUF configuration (optional)
|
||||
- TILE_PROXY__TUF__ENABLED=${TUF_ENABLED:-false}
|
||||
- TILE_PROXY__TUF__URL=${TUF_ROOT_URL:-}
|
||||
- TILE_PROXY__TUF__VALIDATECHECKPOINTSIGNATURE=${TUF_VALIDATE_CHECKPOINT:-true}
|
||||
|
||||
# Cache configuration
|
||||
- TILE_PROXY__CACHE__BASEPATH=/var/cache/stellaops/tiles
|
||||
- TILE_PROXY__CACHE__MAXSIZEGB=${CACHE_MAX_SIZE_GB:-10}
|
||||
- TILE_PROXY__CACHE__CHECKPOINTTTLMINUTES=${CHECKPOINT_TTL_MINUTES:-5}
|
||||
|
||||
# Sync job configuration
|
||||
- TILE_PROXY__SYNC__ENABLED=${SYNC_ENABLED:-true}
|
||||
- TILE_PROXY__SYNC__SCHEDULE=${SYNC_SCHEDULE:-0 */6 * * *}
|
||||
- TILE_PROXY__SYNC__DEPTH=${SYNC_DEPTH:-10000}
|
||||
|
||||
# Request handling
|
||||
- TILE_PROXY__REQUEST__COALESCINGENABLED=${COALESCING_ENABLED:-true}
|
||||
- TILE_PROXY__REQUEST__TIMEOUTSECONDS=${REQUEST_TIMEOUT_SECONDS:-30}
|
||||
|
||||
# Logging
|
||||
- Serilog__MinimumLevel__Default=${LOG_LEVEL:-Information}
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/_admin/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- stellaops
|
||||
|
||||
volumes:
|
||||
tile-cache:
|
||||
driver: local
|
||||
tuf-cache:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
stellaops:
|
||||
driver: bridge
|
||||
170
devops/scripts/bootstrap-trust-offline.sh
Normal file
170
devops/scripts/bootstrap-trust-offline.sh
Normal file
@@ -0,0 +1,170 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# bootstrap-trust-offline.sh
|
||||
# Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance
|
||||
# Task: WORKFLOW-001 - Create bootstrap workflow script
|
||||
# Description: Initialize trust for air-gapped StellaOps deployment
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
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 <trust-bundle> [options]"
|
||||
echo ""
|
||||
echo "Initialize trust for an air-gapped StellaOps deployment."
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " trust-bundle Path to trust bundle (tar.zst or directory)"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --key-dir DIR Directory for signing keys (default: /etc/stellaops/keys)"
|
||||
echo " --reject-if-stale D Reject bundle if older than D (e.g., 7d, 24h)"
|
||||
echo " --skip-keygen Skip signing key generation"
|
||||
echo " --force Force import even if validation fails"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 /media/usb/trust-bundle-2026-01-25.tar.zst"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BUNDLE_PATH=""
|
||||
KEY_DIR="/etc/stellaops/keys"
|
||||
REJECT_STALE=""
|
||||
SKIP_KEYGEN=false
|
||||
FORCE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--key-dir) KEY_DIR="$2"; shift 2 ;;
|
||||
--reject-if-stale) REJECT_STALE="$2"; shift 2 ;;
|
||||
--skip-keygen) SKIP_KEYGEN=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*) log_error "Unknown option: $1"; usage ;;
|
||||
*)
|
||||
if [[ -z "$BUNDLE_PATH" ]]; then
|
||||
BUNDLE_PATH="$1"
|
||||
else
|
||||
log_error "Unexpected argument: $1"
|
||||
usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$BUNDLE_PATH" ]]; then
|
||||
log_error "Trust bundle path is required"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -e "$BUNDLE_PATH" ]]; then
|
||||
log_error "Trust bundle not found: $BUNDLE_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " StellaOps Offline Trust Bootstrap"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_info "Trust Bundle: $BUNDLE_PATH"
|
||||
log_info "Key Directory: $KEY_DIR"
|
||||
if [[ -n "$REJECT_STALE" ]]; then
|
||||
log_info "Staleness Threshold: $REJECT_STALE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 1: Generate signing keys (if using local keys)
|
||||
if [[ "$SKIP_KEYGEN" != "true" ]]; then
|
||||
log_step "Step 1: Generating signing keys..."
|
||||
|
||||
mkdir -p "$KEY_DIR"
|
||||
chmod 700 "$KEY_DIR"
|
||||
|
||||
if [[ ! -f "$KEY_DIR/signing-key.pem" ]]; then
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out "$KEY_DIR/signing-key.pem"
|
||||
chmod 600 "$KEY_DIR/signing-key.pem"
|
||||
log_info "Generated signing key: $KEY_DIR/signing-key.pem"
|
||||
else
|
||||
log_info "Signing key already exists: $KEY_DIR/signing-key.pem"
|
||||
fi
|
||||
else
|
||||
log_step "Step 1: Skipping key generation (--skip-keygen)"
|
||||
fi
|
||||
|
||||
# Step 2: Import trust bundle
|
||||
log_step "Step 2: Importing trust bundle..."
|
||||
|
||||
IMPORT_ARGS="--verify-manifest"
|
||||
if [[ -n "$REJECT_STALE" ]]; then
|
||||
IMPORT_ARGS="$IMPORT_ARGS --reject-if-stale $REJECT_STALE"
|
||||
fi
|
||||
if [[ "$FORCE" == "true" ]]; then
|
||||
IMPORT_ARGS="$IMPORT_ARGS --force"
|
||||
fi
|
||||
|
||||
stella trust import "$BUNDLE_PATH" $IMPORT_ARGS
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
log_error "Failed to import trust bundle"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Trust bundle imported successfully"
|
||||
|
||||
# Step 3: Verify trust state
|
||||
log_step "Step 3: Verifying trust state..."
|
||||
|
||||
stella trust status --show-keys
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
log_error "Failed to verify trust status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 4: Test offline verification
|
||||
log_step "Step 4: Testing offline verification capability..."
|
||||
|
||||
# Check that we have TUF metadata
|
||||
CACHE_DIR="${HOME}/.local/share/StellaOps/TufCache"
|
||||
if [[ -f "$CACHE_DIR/root.json" ]] && [[ -f "$CACHE_DIR/timestamp.json" ]]; then
|
||||
log_info "TUF metadata present"
|
||||
else
|
||||
log_warn "TUF metadata may be incomplete"
|
||||
fi
|
||||
|
||||
# Check for tiles (if snapshot included them)
|
||||
if [[ -d "$CACHE_DIR/tiles" ]]; then
|
||||
TILE_COUNT=$(find "$CACHE_DIR/tiles" -name "*.tile" 2>/dev/null | wc -l)
|
||||
log_info "Tiles cached: $TILE_COUNT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo -e "${GREEN} Offline Bootstrap Complete!${NC}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_info "Trust state imported to: $CACHE_DIR"
|
||||
log_info "Signing key (if generated): $KEY_DIR/signing-key.pem"
|
||||
echo ""
|
||||
log_info "This system can now verify attestations offline using the imported trust state."
|
||||
log_warn "Remember to periodically update the trust bundle to maintain freshness."
|
||||
echo ""
|
||||
log_info "To update trust state:"
|
||||
echo " 1. On connected system: stella trust snapshot export --out bundle.tar.zst"
|
||||
echo " 2. Transfer bundle to this system"
|
||||
echo " 3. Run: $0 bundle.tar.zst"
|
||||
echo ""
|
||||
196
devops/scripts/bootstrap-trust.sh
Normal file
196
devops/scripts/bootstrap-trust.sh
Normal file
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# bootstrap-trust.sh
|
||||
# Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance
|
||||
# Task: WORKFLOW-001 - Create bootstrap workflow script
|
||||
# Description: Initialize trust for new StellaOps deployment
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
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 "Initialize trust for a new StellaOps deployment."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --tuf-url URL TUF repository URL (required)"
|
||||
echo " --service-map NAME Service map target name (default: sigstore-services-v1)"
|
||||
echo " --pin KEY Rekor key to pin (can specify multiple)"
|
||||
echo " --key-dir DIR Directory for signing keys (default: /etc/stellaops/keys)"
|
||||
echo " --skip-keygen Skip signing key generation"
|
||||
echo " --skip-test Skip sign/verify test"
|
||||
echo " --offline Initialize in offline mode"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 --tuf-url https://trust.example.com/tuf/ --pin rekor-key-v1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
TUF_URL=""
|
||||
SERVICE_MAP="sigstore-services-v1"
|
||||
PIN_KEYS=()
|
||||
KEY_DIR="/etc/stellaops/keys"
|
||||
SKIP_KEYGEN=false
|
||||
SKIP_TEST=false
|
||||
OFFLINE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--tuf-url) TUF_URL="$2"; shift 2 ;;
|
||||
--service-map) SERVICE_MAP="$2"; shift 2 ;;
|
||||
--pin) PIN_KEYS+=("$2"); shift 2 ;;
|
||||
--key-dir) KEY_DIR="$2"; shift 2 ;;
|
||||
--skip-keygen) SKIP_KEYGEN=true; shift ;;
|
||||
--skip-test) SKIP_TEST=true; shift ;;
|
||||
--offline) OFFLINE=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
*) log_error "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TUF_URL" ]]; then
|
||||
log_error "TUF URL is required"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ${#PIN_KEYS[@]} -eq 0 ]]; then
|
||||
PIN_KEYS=("rekor-key-v1")
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " StellaOps Trust Bootstrap"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_info "TUF URL: $TUF_URL"
|
||||
log_info "Service Map: $SERVICE_MAP"
|
||||
log_info "Pinned Keys: ${PIN_KEYS[*]}"
|
||||
log_info "Key Directory: $KEY_DIR"
|
||||
echo ""
|
||||
|
||||
# Step 1: Generate signing keys (if using local keys)
|
||||
if [[ "$SKIP_KEYGEN" != "true" ]]; then
|
||||
log_step "Step 1: Generating signing keys..."
|
||||
|
||||
mkdir -p "$KEY_DIR"
|
||||
chmod 700 "$KEY_DIR"
|
||||
|
||||
if [[ ! -f "$KEY_DIR/signing-key.pem" ]]; then
|
||||
stella keys generate --type ecdsa-p256 --out "$KEY_DIR/signing-key.pem" 2>/dev/null || \
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out "$KEY_DIR/signing-key.pem"
|
||||
|
||||
chmod 600 "$KEY_DIR/signing-key.pem"
|
||||
log_info "Generated signing key: $KEY_DIR/signing-key.pem"
|
||||
else
|
||||
log_info "Signing key already exists: $KEY_DIR/signing-key.pem"
|
||||
fi
|
||||
else
|
||||
log_step "Step 1: Skipping key generation (--skip-keygen)"
|
||||
fi
|
||||
|
||||
# Step 2: Initialize TUF client
|
||||
log_step "Step 2: Initializing TUF client..."
|
||||
|
||||
PIN_ARGS=""
|
||||
for key in "${PIN_KEYS[@]}"; do
|
||||
PIN_ARGS="$PIN_ARGS --pin $key"
|
||||
done
|
||||
|
||||
OFFLINE_ARG=""
|
||||
if [[ "$OFFLINE" == "true" ]]; then
|
||||
OFFLINE_ARG="--offline"
|
||||
fi
|
||||
|
||||
stella trust init \
|
||||
--tuf-url "$TUF_URL" \
|
||||
--service-map "$SERVICE_MAP" \
|
||||
$PIN_ARGS \
|
||||
$OFFLINE_ARG \
|
||||
--force
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
log_error "Failed to initialize TUF client"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "TUF client initialized successfully"
|
||||
|
||||
# Step 3: Verify TUF metadata loaded
|
||||
log_step "Step 3: Verifying TUF metadata..."
|
||||
|
||||
stella trust status --show-keys --show-endpoints
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
log_error "Failed to verify TUF status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 4: Test sign/verify cycle
|
||||
if [[ "$SKIP_TEST" != "true" ]] && [[ "$SKIP_KEYGEN" != "true" ]]; then
|
||||
log_step "Step 4: Testing sign/verify cycle..."
|
||||
|
||||
TEST_FILE=$(mktemp)
|
||||
TEST_SIG=$(mktemp)
|
||||
echo "StellaOps bootstrap test $(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$TEST_FILE"
|
||||
|
||||
stella sign "$TEST_FILE" --key "$KEY_DIR/signing-key.pem" --out "$TEST_SIG" 2>/dev/null || {
|
||||
# Fallback to openssl if stella sign not available
|
||||
openssl dgst -sha256 -sign "$KEY_DIR/signing-key.pem" -out "$TEST_SIG" "$TEST_FILE"
|
||||
}
|
||||
|
||||
if [[ -f "$TEST_SIG" ]] && [[ -s "$TEST_SIG" ]]; then
|
||||
log_info "Sign/verify test passed"
|
||||
else
|
||||
log_warn "Sign test could not be verified (this may be expected)"
|
||||
fi
|
||||
|
||||
rm -f "$TEST_FILE" "$TEST_SIG"
|
||||
else
|
||||
log_step "Step 4: Skipping sign/verify test"
|
||||
fi
|
||||
|
||||
# Step 5: Test Rekor connectivity (if online)
|
||||
if [[ "$OFFLINE" != "true" ]]; then
|
||||
log_step "Step 5: Testing Rekor connectivity..."
|
||||
|
||||
REKOR_URL=$(stella trust status --output json 2>/dev/null | grep -o '"rekor_url"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | cut -d'"' -f4 || echo "")
|
||||
|
||||
if [[ -n "$REKOR_URL" ]]; then
|
||||
if curl -sf "${REKOR_URL}/api/v1/log" >/dev/null 2>&1; then
|
||||
log_info "Rekor connectivity: OK"
|
||||
else
|
||||
log_warn "Rekor connectivity check failed (service may be unavailable)"
|
||||
fi
|
||||
else
|
||||
log_warn "Could not determine Rekor URL from trust status"
|
||||
fi
|
||||
else
|
||||
log_step "Step 5: Skipping Rekor test (offline mode)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo -e "${GREEN} Bootstrap Complete!${NC}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_info "Trust repository initialized at: ~/.local/share/StellaOps/TufCache"
|
||||
log_info "Signing key (if generated): $KEY_DIR/signing-key.pem"
|
||||
echo ""
|
||||
log_info "Next steps:"
|
||||
echo " 1. Configure your CI/CD to use the signing key"
|
||||
echo " 2. Set up periodic 'stella trust sync' for metadata freshness"
|
||||
echo " 3. For air-gap deployments, run 'stella trust export' to create bundles"
|
||||
echo ""
|
||||
195
devops/scripts/disaster-swap-endpoint.sh
Normal file
195
devops/scripts/disaster-swap-endpoint.sh
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# disaster-swap-endpoint.sh
|
||||
# Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance
|
||||
# Task: WORKFLOW-003 - Create disaster endpoint swap script
|
||||
# Description: Emergency endpoint swap via TUF (no client reconfiguration)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
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 --repo <dir> --new-rekor-url <url> [options]"
|
||||
echo ""
|
||||
echo "Emergency endpoint swap via TUF update."
|
||||
echo "Clients will auto-discover new endpoints without reconfiguration."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --repo DIR TUF repository directory (required)"
|
||||
echo " --new-rekor-url URL New Rekor URL (required)"
|
||||
echo " --new-fulcio-url URL New Fulcio URL (optional)"
|
||||
echo " --note TEXT Note explaining the change"
|
||||
echo " --version N New service map version (auto-increment if not specified)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 --repo /path/to/tuf \\"
|
||||
echo " --new-rekor-url https://rekor-mirror.internal:8080 \\"
|
||||
echo " --note 'Emergency: Production Rekor outage'"
|
||||
echo ""
|
||||
echo "IMPORTANT: This changes where ALL clients send requests!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
REPO_DIR=""
|
||||
NEW_REKOR_URL=""
|
||||
NEW_FULCIO_URL=""
|
||||
NOTE=""
|
||||
VERSION=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--repo) REPO_DIR="$2"; shift 2 ;;
|
||||
--new-rekor-url) NEW_REKOR_URL="$2"; shift 2 ;;
|
||||
--new-fulcio-url) NEW_FULCIO_URL="$2"; shift 2 ;;
|
||||
--note) NOTE="$2"; shift 2 ;;
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
-h|--help) usage ;;
|
||||
*) log_error "Unknown argument: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$REPO_DIR" ]] || [[ -z "$NEW_REKOR_URL" ]]; then
|
||||
log_error "--repo and --new-rekor-url are required"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$REPO_DIR" ]]; then
|
||||
log_error "TUF repository not found: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo -e "${RED} EMERGENCY ENDPOINT SWAP${NC}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_warn "This will redirect ALL clients to new endpoints!"
|
||||
echo ""
|
||||
log_info "TUF Repository: $REPO_DIR"
|
||||
log_info "New Rekor URL: $NEW_REKOR_URL"
|
||||
if [[ -n "$NEW_FULCIO_URL" ]]; then
|
||||
log_info "New Fulcio URL: $NEW_FULCIO_URL"
|
||||
fi
|
||||
if [[ -n "$NOTE" ]]; then
|
||||
log_info "Note: $NOTE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
read -p "Type 'SWAP' to confirm endpoint change: " CONFIRM
|
||||
if [[ "$CONFIRM" != "SWAP" ]]; then
|
||||
log_error "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find current service map
|
||||
CURRENT_MAP=$(ls "$REPO_DIR/targets/" 2>/dev/null | grep -E '^sigstore-services-v[0-9]+\.json$' | sort -V | tail -1 || echo "")
|
||||
|
||||
if [[ -z "$CURRENT_MAP" ]]; then
|
||||
log_error "No service map found in $REPO_DIR/targets/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_PATH="$REPO_DIR/targets/$CURRENT_MAP"
|
||||
log_info "Current service map: $CURRENT_MAP"
|
||||
|
||||
# Determine new version
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
CURRENT_VERSION=$(echo "$CURRENT_MAP" | grep -oE '[0-9]+' | tail -1)
|
||||
VERSION=$((CURRENT_VERSION + 1))
|
||||
fi
|
||||
|
||||
NEW_MAP="sigstore-services-v${VERSION}.json"
|
||||
NEW_PATH="$REPO_DIR/targets/$NEW_MAP"
|
||||
|
||||
log_step "Creating new service map: $NEW_MAP"
|
||||
|
||||
# Read current map and update
|
||||
if command -v python3 &>/dev/null; then
|
||||
python3 - "$CURRENT_PATH" "$NEW_PATH" "$NEW_REKOR_URL" "$NEW_FULCIO_URL" "$NOTE" "$VERSION" << 'PYTHON_SCRIPT'
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
current_path = sys.argv[1]
|
||||
new_path = sys.argv[2]
|
||||
new_rekor_url = sys.argv[3]
|
||||
new_fulcio_url = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else None
|
||||
note = sys.argv[5] if len(sys.argv) > 5 and sys.argv[5] else None
|
||||
version = int(sys.argv[6]) if len(sys.argv) > 6 else 1
|
||||
|
||||
with open(current_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Update endpoints
|
||||
data['version'] = version
|
||||
data['rekor']['url'] = new_rekor_url
|
||||
|
||||
if new_fulcio_url and 'fulcio' in data:
|
||||
data['fulcio']['url'] = new_fulcio_url
|
||||
|
||||
# Update metadata
|
||||
if 'metadata' not in data:
|
||||
data['metadata'] = {}
|
||||
data['metadata']['updated_at'] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
if note:
|
||||
data['metadata']['note'] = note
|
||||
|
||||
with open(new_path, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
print(f"Created: {new_path}")
|
||||
PYTHON_SCRIPT
|
||||
else
|
||||
# Fallback: simple JSON creation
|
||||
cat > "$NEW_PATH" << EOF
|
||||
{
|
||||
"version": $VERSION,
|
||||
"rekor": {
|
||||
"url": "$NEW_REKOR_URL"
|
||||
},
|
||||
"metadata": {
|
||||
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"note": "$NOTE"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
log_info "New service map created: $NEW_PATH"
|
||||
|
||||
# Add to targets
|
||||
log_step "Adding new service map to TUF targets..."
|
||||
|
||||
if [[ -x "$REPO_DIR/scripts/add-target.sh" ]]; then
|
||||
"$REPO_DIR/scripts/add-target.sh" "$NEW_PATH" "$NEW_MAP" --repo "$REPO_DIR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo -e "${GREEN} Endpoint Swap Prepared${NC}"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_warn "NEXT STEPS (REQUIRED):"
|
||||
echo " 1. Review the new service map: cat $NEW_PATH"
|
||||
echo " 2. Sign the updated targets.json with targets key"
|
||||
echo " 3. Update snapshot.json and sign with snapshot key"
|
||||
echo " 4. Update timestamp.json and sign with timestamp key"
|
||||
echo " 5. Deploy updated metadata to TUF server"
|
||||
echo ""
|
||||
log_info "Clients will auto-discover the new endpoint within their refresh interval."
|
||||
log_info "For immediate effect, clients can run: stella trust sync --force"
|
||||
echo ""
|
||||
log_warn "Monitor client traffic to ensure failover is working!"
|
||||
echo ""
|
||||
197
devops/scripts/rotate-rekor-key.sh
Normal file
197
devops/scripts/rotate-rekor-key.sh
Normal file
@@ -0,0 +1,197 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# rotate-rekor-key.sh
|
||||
# Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance
|
||||
# Task: WORKFLOW-002 - Create key rotation workflow script
|
||||
# Description: Rotate Rekor public key with 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 Rekor public key through a dual-key grace period."
|
||||
echo ""
|
||||
echo "Phases:"
|
||||
echo " add-key Add new key to TUF (starts grace period)"
|
||||
echo " verify Verify both keys are active"
|
||||
echo " remove-old Remove old key (after grace period)"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --repo DIR TUF repository directory"
|
||||
echo " --new-key FILE Path to new Rekor public key"
|
||||
echo " --new-key-name NAME Target name for new key (default: rekor-key-v{N+1})"
|
||||
echo " --old-key-name NAME Target name for old key to remove"
|
||||
echo " --grace-days N Grace period in days (default: 7)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example (3-phase rotation):"
|
||||
echo " # Phase 1: Add new key"
|
||||
echo " $0 add-key --repo /path/to/tuf --new-key rekor-key-v2.pub"
|
||||
echo ""
|
||||
echo " # Wait for grace period (clients sync)"
|
||||
echo " sleep 7d"
|
||||
echo ""
|
||||
echo " # Phase 2: Verify"
|
||||
echo " $0 verify"
|
||||
echo ""
|
||||
echo " # Phase 3: Remove old key"
|
||||
echo " $0 remove-old --repo /path/to/tuf --old-key-name rekor-key-v1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
PHASE=""
|
||||
REPO_DIR=""
|
||||
NEW_KEY=""
|
||||
NEW_KEY_NAME=""
|
||||
OLD_KEY_NAME=""
|
||||
GRACE_DAYS=7
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
add-key|verify|remove-old)
|
||||
PHASE="$1"
|
||||
shift
|
||||
;;
|
||||
--repo) REPO_DIR="$2"; shift 2 ;;
|
||||
--new-key) NEW_KEY="$2"; shift 2 ;;
|
||||
--new-key-name) NEW_KEY_NAME="$2"; shift 2 ;;
|
||||
--old-key-name) OLD_KEY_NAME="$2"; shift 2 ;;
|
||||
--grace-days) GRACE_DAYS="$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 " Rekor Key Rotation - Phase: $PHASE"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
case "$PHASE" in
|
||||
add-key)
|
||||
if [[ -z "$REPO_DIR" ]] || [[ -z "$NEW_KEY" ]]; then
|
||||
log_error "add-key requires --repo and --new-key"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -f "$NEW_KEY" ]]; then
|
||||
log_error "New key file not found: $NEW_KEY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$REPO_DIR" ]]; then
|
||||
log_error "TUF repository not found: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine new key name if not specified
|
||||
if [[ -z "$NEW_KEY_NAME" ]]; then
|
||||
# Find highest version and increment
|
||||
HIGHEST=$(ls "$REPO_DIR/targets/" 2>/dev/null | grep -E '^rekor-key-v[0-9]+' | \
|
||||
sed 's/rekor-key-v//' | sed 's/\.pub$//' | sort -n | tail -1 || echo "0")
|
||||
NEW_VERSION=$((HIGHEST + 1))
|
||||
NEW_KEY_NAME="rekor-key-v${NEW_VERSION}"
|
||||
fi
|
||||
|
||||
log_step "Adding new Rekor key: $NEW_KEY_NAME"
|
||||
log_info "Source: $NEW_KEY"
|
||||
|
||||
# Copy key to targets
|
||||
cp "$NEW_KEY" "$REPO_DIR/targets/${NEW_KEY_NAME}.pub"
|
||||
|
||||
# Add to targets.json
|
||||
if [[ -x "$REPO_DIR/scripts/add-target.sh" ]]; then
|
||||
"$REPO_DIR/scripts/add-target.sh" "$REPO_DIR/targets/${NEW_KEY_NAME}.pub" "${NEW_KEY_NAME}.pub" --repo "$REPO_DIR"
|
||||
else
|
||||
log_warn "add-target.sh not found, updating targets.json manually required"
|
||||
fi
|
||||
|
||||
log_info ""
|
||||
log_info "Key added: $NEW_KEY_NAME"
|
||||
log_info ""
|
||||
log_warn "IMPORTANT: Dual-key period has started."
|
||||
log_warn "Wait at least $GRACE_DAYS days before running 'remove-old' phase."
|
||||
log_warn "During this time, clients will sync and receive both keys."
|
||||
log_info ""
|
||||
log_info "Next steps:"
|
||||
echo " 1. Sign and publish updated TUF metadata"
|
||||
echo " 2. Monitor client sync status"
|
||||
echo " 3. After $GRACE_DAYS days, run: $0 remove-old --repo $REPO_DIR --old-key-name <old-key>"
|
||||
;;
|
||||
|
||||
verify)
|
||||
log_step "Verifying key rotation status..."
|
||||
|
||||
# Check local trust state
|
||||
stella trust status --show-keys
|
||||
|
||||
log_info ""
|
||||
log_info "Verify that:"
|
||||
echo " 1. Both old and new Rekor keys are listed"
|
||||
echo " 2. Service endpoints are resolving correctly"
|
||||
echo " 3. Attestations signed with old key still verify"
|
||||
;;
|
||||
|
||||
remove-old)
|
||||
if [[ -z "$REPO_DIR" ]] || [[ -z "$OLD_KEY_NAME" ]]; then
|
||||
log_error "remove-old requires --repo and --old-key-name"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -d "$REPO_DIR" ]]; then
|
||||
log_error "TUF repository not found: $REPO_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OLD_KEY_FILE="$REPO_DIR/targets/${OLD_KEY_NAME}.pub"
|
||||
if [[ ! -f "$OLD_KEY_FILE" ]]; then
|
||||
OLD_KEY_FILE="$REPO_DIR/targets/${OLD_KEY_NAME}"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$OLD_KEY_FILE" ]]; then
|
||||
log_error "Old key not found: $OLD_KEY_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_step "Removing old Rekor key: $OLD_KEY_NAME"
|
||||
log_warn "This is IRREVERSIBLE. Ensure all clients have synced the new key."
|
||||
|
||||
read -p "Type 'CONFIRM' to proceed: " CONFIRM
|
||||
if [[ "$CONFIRM" != "CONFIRM" ]]; then
|
||||
log_error "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove key file
|
||||
rm -f "$OLD_KEY_FILE"
|
||||
|
||||
# Remove from targets.json (simplified - production should use proper JSON manipulation)
|
||||
log_warn "Remember to update targets.json to remove the old key entry"
|
||||
log_warn "Then sign and publish the updated metadata"
|
||||
|
||||
log_info ""
|
||||
log_info "Old key removed: $OLD_KEY_NAME"
|
||||
log_info "Key rotation complete!"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
265
devops/scripts/rotate-signing-key.sh
Normal file
265
devops/scripts/rotate-signing-key.sh
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/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 ""
|
||||
162
devops/trust-repo-template/README.md
Normal file
162
devops/trust-repo-template/README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Stella TUF Trust Repository Template
|
||||
|
||||
This directory contains a template for creating a TUF (The Update Framework) repository
|
||||
for distributing trust anchors to StellaOps clients.
|
||||
|
||||
## WARNING
|
||||
|
||||
**The sample keys in this template are for DEMONSTRATION ONLY.**
|
||||
**DO NOT USE THESE KEYS IN PRODUCTION.**
|
||||
|
||||
Generate new keys using the `scripts/init-tuf-repo.sh` script before deploying.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
stella-trust/
|
||||
├── root.json # Root metadata (rotates rarely, high ceremony)
|
||||
├── snapshot.json # Current target versions
|
||||
├── timestamp.json # Freshness indicator (rotates frequently)
|
||||
├── targets.json # Target file metadata
|
||||
└── targets/
|
||||
├── rekor-key-v1.pub # Rekor log public key
|
||||
├── fulcio-chain.pem # Fulcio certificate chain
|
||||
└── sigstore-services-v1.json # Service endpoint map
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Initialize a New Repository
|
||||
|
||||
```bash
|
||||
# Generate new signing keys (do this in a secure environment)
|
||||
./scripts/init-tuf-repo.sh /path/to/new-repo
|
||||
|
||||
# This creates:
|
||||
# - Root key (keep offline, backup securely)
|
||||
# - Snapshot key
|
||||
# - Timestamp key
|
||||
# - Targets key
|
||||
# - Initial metadata files
|
||||
```
|
||||
|
||||
### 2. Add a Target
|
||||
|
||||
```bash
|
||||
# Add Rekor public key as a target
|
||||
./scripts/add-target.sh /path/to/rekor-key.pub rekor-key-v1
|
||||
|
||||
# Add service map
|
||||
./scripts/add-target.sh /path/to/sigstore-services.json sigstore-services-v1
|
||||
```
|
||||
|
||||
### 3. Publish Updates
|
||||
|
||||
```bash
|
||||
# Update timestamp (do this regularly, e.g., daily)
|
||||
./scripts/update-timestamp.sh
|
||||
|
||||
# The timestamp.json should be refreshed frequently to maintain client trust
|
||||
```
|
||||
|
||||
### 4. Deploy
|
||||
|
||||
Host the repository contents on a web server:
|
||||
- HTTPS required for production
|
||||
- Set appropriate cache headers (short TTL for timestamp.json)
|
||||
- Consider CDN for global distribution
|
||||
|
||||
## Key Management
|
||||
|
||||
### Key Hierarchy
|
||||
|
||||
```
|
||||
Root Key (offline, high ceremony)
|
||||
├── Snapshot Key (can be online)
|
||||
├── Timestamp Key (must be online for automation)
|
||||
└── Targets Key (can be online)
|
||||
```
|
||||
|
||||
### Security Recommendations
|
||||
|
||||
1. **Root Key**: Store offline in HSM or air-gapped system. Only use for:
|
||||
- Initial repository creation
|
||||
- Root key rotation (rare)
|
||||
- Emergency recovery
|
||||
|
||||
2. **Snapshot/Targets Keys**: Can be stored in secure KMS for automation.
|
||||
|
||||
3. **Timestamp Key**: Must be accessible for automated updates. Use short-lived
|
||||
credentials and rotate regularly.
|
||||
|
||||
### Key Rotation
|
||||
|
||||
See `docs/operations/key-rotation-runbook.md` for detailed procedures.
|
||||
|
||||
Quick rotation example:
|
||||
```bash
|
||||
# Add new key while keeping old one active
|
||||
./scripts/rotate-key.sh targets --add-key /path/to/new-key.pub
|
||||
|
||||
# After grace period (clients have updated), remove old key
|
||||
./scripts/rotate-key.sh targets --remove-key old-key-id
|
||||
```
|
||||
|
||||
## Client Configuration
|
||||
|
||||
Configure StellaOps clients to use your TUF repository:
|
||||
|
||||
```yaml
|
||||
attestor:
|
||||
trust_repo:
|
||||
enabled: true
|
||||
tuf_url: https://trust.yourcompany.com/tuf/
|
||||
service_map_target: sigstore-services-v1
|
||||
rekor_key_targets:
|
||||
- rekor-key-v1
|
||||
```
|
||||
|
||||
Or via CLI:
|
||||
```bash
|
||||
stella trust init \
|
||||
--tuf-url https://trust.yourcompany.com/tuf/ \
|
||||
--service-map sigstore-services-v1 \
|
||||
--pin rekor-key-v1
|
||||
```
|
||||
|
||||
## Metadata Expiration
|
||||
|
||||
Default expiration times (configurable in init script):
|
||||
- `root.json`: 365 days
|
||||
- `snapshot.json`: 7 days
|
||||
- `timestamp.json`: 1 day
|
||||
- `targets.json`: 30 days
|
||||
|
||||
Clients will refuse to use metadata past its expiration. Ensure automated
|
||||
timestamp updates are running.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Client reports "metadata expired"
|
||||
The timestamp.json hasn't been updated. Run:
|
||||
```bash
|
||||
./scripts/update-timestamp.sh
|
||||
```
|
||||
|
||||
### Client reports "signature verification failed"
|
||||
Keys may have rotated without client update. Client should run:
|
||||
```bash
|
||||
stella trust sync --force
|
||||
```
|
||||
|
||||
### Client reports "unknown target"
|
||||
Target hasn't been added to repository. Add it:
|
||||
```bash
|
||||
./scripts/add-target.sh /path/to/target target-name
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [TUF Specification](https://theupdateframework.github.io/specification/latest/)
|
||||
- [StellaOps Trust Documentation](docs/modules/attestor/tuf-integration.md)
|
||||
- [Key Rotation Runbook](docs/operations/key-rotation-runbook.md)
|
||||
42
devops/trust-repo-template/root.json.sample
Normal file
42
devops/trust-repo-template/root.json.sample
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"signed": {
|
||||
"_type": "root",
|
||||
"spec_version": "1.0.0",
|
||||
"version": 1,
|
||||
"expires": "2027-01-25T00:00:00Z",
|
||||
"keys": {
|
||||
"SAMPLE_ROOT_KEY_ID_DO_NOT_USE": {
|
||||
"keytype": "ed25519",
|
||||
"scheme": "ed25519",
|
||||
"keyval": {
|
||||
"public": "SAMPLE_PUBLIC_KEY_BASE64_DO_NOT_USE"
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"root": {
|
||||
"keyids": ["SAMPLE_ROOT_KEY_ID_DO_NOT_USE"],
|
||||
"threshold": 1
|
||||
},
|
||||
"snapshot": {
|
||||
"keyids": ["SAMPLE_SNAPSHOT_KEY_ID"],
|
||||
"threshold": 1
|
||||
},
|
||||
"timestamp": {
|
||||
"keyids": ["SAMPLE_TIMESTAMP_KEY_ID"],
|
||||
"threshold": 1
|
||||
},
|
||||
"targets": {
|
||||
"keyids": ["SAMPLE_TARGETS_KEY_ID"],
|
||||
"threshold": 1
|
||||
}
|
||||
},
|
||||
"consistent_snapshot": true
|
||||
},
|
||||
"signatures": [
|
||||
{
|
||||
"keyid": "SAMPLE_ROOT_KEY_ID_DO_NOT_USE",
|
||||
"sig": "SAMPLE_SIGNATURE_DO_NOT_USE"
|
||||
}
|
||||
]
|
||||
}
|
||||
150
devops/trust-repo-template/scripts/add-target.sh
Normal file
150
devops/trust-repo-template/scripts/add-target.sh
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# add-target.sh
|
||||
# Sprint: SPRINT_20260125_001_Attestor_tuf_trust_foundation
|
||||
# Task: TUF-006 - Create TUF repository structure template
|
||||
# Description: Add a new target file to the TUF repository
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <source-file> <target-name> [options]"
|
||||
echo ""
|
||||
echo "Add a target file to the TUF repository."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --repo DIR Repository directory (default: current directory)"
|
||||
echo " --custom-hash HASH Override SHA256 hash (for testing only)"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 /path/to/rekor-key.pub rekor-key-v1"
|
||||
echo " $0 /path/to/services.json sigstore-services-v1 --repo /var/lib/tuf"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
SOURCE_FILE=""
|
||||
TARGET_NAME=""
|
||||
REPO_DIR="."
|
||||
CUSTOM_HASH=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--repo)
|
||||
REPO_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--custom-hash)
|
||||
CUSTOM_HASH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$SOURCE_FILE" ]]; then
|
||||
SOURCE_FILE="$1"
|
||||
elif [[ -z "$TARGET_NAME" ]]; then
|
||||
TARGET_NAME="$1"
|
||||
else
|
||||
log_error "Unknown argument: $1"
|
||||
usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$SOURCE_FILE" ]] || [[ -z "$TARGET_NAME" ]]; then
|
||||
log_error "Source file and target name are required"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SOURCE_FILE" ]]; then
|
||||
log_error "Source file not found: $SOURCE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$REPO_DIR/targets.json" ]]; then
|
||||
log_error "Not a TUF repository: $REPO_DIR (targets.json not found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate file hash and size
|
||||
FILE_SIZE=$(stat -f%z "$SOURCE_FILE" 2>/dev/null || stat -c%s "$SOURCE_FILE")
|
||||
if [[ -n "$CUSTOM_HASH" ]]; then
|
||||
FILE_HASH="$CUSTOM_HASH"
|
||||
else
|
||||
FILE_HASH=$(openssl dgst -sha256 -hex "$SOURCE_FILE" | awk '{print $2}')
|
||||
fi
|
||||
|
||||
log_info "Adding target: $TARGET_NAME"
|
||||
log_info " Source: $SOURCE_FILE"
|
||||
log_info " Size: $FILE_SIZE bytes"
|
||||
log_info " SHA256: $FILE_HASH"
|
||||
|
||||
# Copy file to targets directory
|
||||
TARGETS_DIR="$REPO_DIR/targets"
|
||||
mkdir -p "$TARGETS_DIR"
|
||||
cp "$SOURCE_FILE" "$TARGETS_DIR/$TARGET_NAME"
|
||||
|
||||
# Update targets.json
|
||||
# This is a simplified implementation - production should use proper JSON manipulation
|
||||
TARGETS_JSON="$REPO_DIR/targets.json"
|
||||
|
||||
# Read current version
|
||||
CURRENT_VERSION=$(grep -o '"version"[[:space:]]*:[[:space:]]*[0-9]*' "$TARGETS_JSON" | head -1 | grep -o '[0-9]*')
|
||||
NEW_VERSION=$((CURRENT_VERSION + 1))
|
||||
|
||||
# Calculate new expiry (30 days from now)
|
||||
NEW_EXPIRES=$(date -u -d "+30 days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v+30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
log_info "Updating targets.json (version $CURRENT_VERSION -> $NEW_VERSION)"
|
||||
|
||||
# Create new targets entry
|
||||
python3 - "$TARGETS_JSON" "$TARGET_NAME" "$FILE_SIZE" "$FILE_HASH" "$NEW_VERSION" "$NEW_EXPIRES" << 'PYTHON_SCRIPT'
|
||||
import json
|
||||
import sys
|
||||
|
||||
targets_file = sys.argv[1]
|
||||
target_name = sys.argv[2]
|
||||
file_size = int(sys.argv[3])
|
||||
file_hash = sys.argv[4]
|
||||
new_version = int(sys.argv[5])
|
||||
new_expires = sys.argv[6]
|
||||
|
||||
with open(targets_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
data['signed']['version'] = new_version
|
||||
data['signed']['expires'] = new_expires
|
||||
data['signed']['targets'][target_name] = {
|
||||
'length': file_size,
|
||||
'hashes': {
|
||||
'sha256': file_hash
|
||||
}
|
||||
}
|
||||
|
||||
# Clear signatures (need to re-sign)
|
||||
data['signatures'] = []
|
||||
|
||||
with open(targets_file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
print(f"Updated {targets_file}")
|
||||
PYTHON_SCRIPT
|
||||
|
||||
log_info ""
|
||||
log_info "Target added successfully!"
|
||||
log_warn "IMPORTANT: targets.json signatures have been cleared."
|
||||
log_warn "Run the signing script to re-sign metadata before publishing."
|
||||
314
devops/trust-repo-template/scripts/init-tuf-repo.sh
Normal file
314
devops/trust-repo-template/scripts/init-tuf-repo.sh
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# init-tuf-repo.sh
|
||||
# Sprint: SPRINT_20260125_001_Attestor_tuf_trust_foundation
|
||||
# Task: TUF-006 - Create TUF repository structure template
|
||||
# Description: Initialize a new TUF repository with signing keys
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <output-directory> [options]"
|
||||
echo ""
|
||||
echo "Initialize a new TUF repository for StellaOps trust distribution."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --key-type TYPE Key algorithm: ed25519 (default), ecdsa-p256"
|
||||
echo " --root-expiry DAYS Root metadata expiry (default: 365)"
|
||||
echo " --force Overwrite existing repository"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 /var/lib/stellaops/trust-repo --key-type ed25519"
|
||||
exit 1
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
OUTPUT_DIR=""
|
||||
KEY_TYPE="ed25519"
|
||||
ROOT_EXPIRY=365
|
||||
FORCE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--key-type)
|
||||
KEY_TYPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--root-expiry)
|
||||
ROOT_EXPIRY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--force)
|
||||
FORCE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
OUTPUT_DIR="$1"
|
||||
else
|
||||
log_error "Unknown argument: $1"
|
||||
usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$OUTPUT_DIR" ]]; then
|
||||
log_error "Output directory is required"
|
||||
usage
|
||||
fi
|
||||
|
||||
# Check if directory exists
|
||||
if [[ -d "$OUTPUT_DIR" ]] && [[ "$FORCE" != "true" ]]; then
|
||||
log_error "Directory already exists: $OUTPUT_DIR"
|
||||
log_error "Use --force to overwrite"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create directory structure
|
||||
log_info "Creating TUF repository at: $OUTPUT_DIR"
|
||||
mkdir -p "$OUTPUT_DIR/keys" "$OUTPUT_DIR/targets"
|
||||
|
||||
# Generate keys
|
||||
log_info "Generating signing keys (type: $KEY_TYPE)..."
|
||||
|
||||
generate_key() {
|
||||
local name=$1
|
||||
local key_file="$OUTPUT_DIR/keys/$name"
|
||||
|
||||
case $KEY_TYPE in
|
||||
ed25519)
|
||||
# Generate Ed25519 key pair
|
||||
openssl genpkey -algorithm ED25519 -out "$key_file.pem" 2>/dev/null
|
||||
openssl pkey -in "$key_file.pem" -pubout -out "$key_file.pub" 2>/dev/null
|
||||
;;
|
||||
ecdsa-p256)
|
||||
# Generate ECDSA P-256 key pair
|
||||
openssl ecparam -name prime256v1 -genkey -noout -out "$key_file.pem" 2>/dev/null
|
||||
openssl ec -in "$key_file.pem" -pubout -out "$key_file.pub" 2>/dev/null
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown key type: $KEY_TYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
chmod 600 "$key_file.pem"
|
||||
log_info " Generated: $name"
|
||||
}
|
||||
|
||||
generate_key "root"
|
||||
generate_key "snapshot"
|
||||
generate_key "timestamp"
|
||||
generate_key "targets"
|
||||
|
||||
# Calculate expiration dates
|
||||
NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
ROOT_EXPIRES=$(date -u -d "+${ROOT_EXPIRY} days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v+${ROOT_EXPIRY}d +%Y-%m-%dT%H:%M:%SZ)
|
||||
SNAPSHOT_EXPIRES=$(date -u -d "+7 days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v+7d +%Y-%m-%dT%H:%M:%SZ)
|
||||
TIMESTAMP_EXPIRES=$(date -u -d "+1 day" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v+1d +%Y-%m-%dT%H:%M:%SZ)
|
||||
TARGETS_EXPIRES=$(date -u -d "+30 days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v+30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Get key IDs (SHA256 of public key)
|
||||
get_key_id() {
|
||||
local pubkey_file=$1
|
||||
openssl pkey -pubin -in "$pubkey_file" -outform DER 2>/dev/null | openssl dgst -sha256 -hex | awk '{print $2}'
|
||||
}
|
||||
|
||||
ROOT_KEY_ID=$(get_key_id "$OUTPUT_DIR/keys/root.pub")
|
||||
SNAPSHOT_KEY_ID=$(get_key_id "$OUTPUT_DIR/keys/snapshot.pub")
|
||||
TIMESTAMP_KEY_ID=$(get_key_id "$OUTPUT_DIR/keys/timestamp.pub")
|
||||
TARGETS_KEY_ID=$(get_key_id "$OUTPUT_DIR/keys/targets.pub")
|
||||
|
||||
# Create root.json
|
||||
log_info "Creating metadata files..."
|
||||
|
||||
cat > "$OUTPUT_DIR/root.json" << EOF
|
||||
{
|
||||
"signed": {
|
||||
"_type": "root",
|
||||
"spec_version": "1.0.0",
|
||||
"version": 1,
|
||||
"expires": "$ROOT_EXPIRES",
|
||||
"keys": {
|
||||
"$ROOT_KEY_ID": {
|
||||
"keytype": "$KEY_TYPE",
|
||||
"scheme": "$KEY_TYPE",
|
||||
"keyval": {
|
||||
"public": "$(base64 -w0 "$OUTPUT_DIR/keys/root.pub")"
|
||||
}
|
||||
},
|
||||
"$SNAPSHOT_KEY_ID": {
|
||||
"keytype": "$KEY_TYPE",
|
||||
"scheme": "$KEY_TYPE",
|
||||
"keyval": {
|
||||
"public": "$(base64 -w0 "$OUTPUT_DIR/keys/snapshot.pub")"
|
||||
}
|
||||
},
|
||||
"$TIMESTAMP_KEY_ID": {
|
||||
"keytype": "$KEY_TYPE",
|
||||
"scheme": "$KEY_TYPE",
|
||||
"keyval": {
|
||||
"public": "$(base64 -w0 "$OUTPUT_DIR/keys/timestamp.pub")"
|
||||
}
|
||||
},
|
||||
"$TARGETS_KEY_ID": {
|
||||
"keytype": "$KEY_TYPE",
|
||||
"scheme": "$KEY_TYPE",
|
||||
"keyval": {
|
||||
"public": "$(base64 -w0 "$OUTPUT_DIR/keys/targets.pub")"
|
||||
}
|
||||
}
|
||||
},
|
||||
"roles": {
|
||||
"root": {
|
||||
"keyids": ["$ROOT_KEY_ID"],
|
||||
"threshold": 1
|
||||
},
|
||||
"snapshot": {
|
||||
"keyids": ["$SNAPSHOT_KEY_ID"],
|
||||
"threshold": 1
|
||||
},
|
||||
"timestamp": {
|
||||
"keyids": ["$TIMESTAMP_KEY_ID"],
|
||||
"threshold": 1
|
||||
},
|
||||
"targets": {
|
||||
"keyids": ["$TARGETS_KEY_ID"],
|
||||
"threshold": 1
|
||||
}
|
||||
},
|
||||
"consistent_snapshot": true
|
||||
},
|
||||
"signatures": []
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create targets.json
|
||||
cat > "$OUTPUT_DIR/targets.json" << EOF
|
||||
{
|
||||
"signed": {
|
||||
"_type": "targets",
|
||||
"spec_version": "1.0.0",
|
||||
"version": 1,
|
||||
"expires": "$TARGETS_EXPIRES",
|
||||
"targets": {}
|
||||
},
|
||||
"signatures": []
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create snapshot.json
|
||||
cat > "$OUTPUT_DIR/snapshot.json" << EOF
|
||||
{
|
||||
"signed": {
|
||||
"_type": "snapshot",
|
||||
"spec_version": "1.0.0",
|
||||
"version": 1,
|
||||
"expires": "$SNAPSHOT_EXPIRES",
|
||||
"meta": {
|
||||
"targets.json": {
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"signatures": []
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create timestamp.json
|
||||
cat > "$OUTPUT_DIR/timestamp.json" << EOF
|
||||
{
|
||||
"signed": {
|
||||
"_type": "timestamp",
|
||||
"spec_version": "1.0.0",
|
||||
"version": 1,
|
||||
"expires": "$TIMESTAMP_EXPIRES",
|
||||
"meta": {
|
||||
"snapshot.json": {
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"signatures": []
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create sample service map
|
||||
cat > "$OUTPUT_DIR/targets/sigstore-services-v1.json" << EOF
|
||||
{
|
||||
"version": 1,
|
||||
"rekor": {
|
||||
"url": "https://rekor.sigstore.dev",
|
||||
"log_id": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
|
||||
"public_key_target": "rekor-key-v1"
|
||||
},
|
||||
"fulcio": {
|
||||
"url": "https://fulcio.sigstore.dev",
|
||||
"root_cert_target": "fulcio-chain.pem"
|
||||
},
|
||||
"ct_log": {
|
||||
"url": "https://ctfe.sigstore.dev"
|
||||
},
|
||||
"overrides": {
|
||||
"staging": {
|
||||
"rekor_url": "https://rekor.sigstage.dev",
|
||||
"fulcio_url": "https://fulcio.sigstage.dev"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"updated_at": "$NOW",
|
||||
"note": "Production Sigstore endpoints"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Copy scripts
|
||||
cp "$TEMPLATE_DIR/scripts/add-target.sh" "$OUTPUT_DIR/scripts/" 2>/dev/null || true
|
||||
cp "$TEMPLATE_DIR/scripts/update-timestamp.sh" "$OUTPUT_DIR/scripts/" 2>/dev/null || true
|
||||
mkdir -p "$OUTPUT_DIR/scripts"
|
||||
|
||||
log_info ""
|
||||
log_info "TUF repository initialized successfully!"
|
||||
log_info ""
|
||||
log_info "Directory structure:"
|
||||
log_info " $OUTPUT_DIR/"
|
||||
log_info " ├── keys/ # Signing keys (keep root key offline!)"
|
||||
log_info " ├── targets/ # Target files"
|
||||
log_info " ├── root.json # Root metadata"
|
||||
log_info " ├── snapshot.json # Snapshot metadata"
|
||||
log_info " ├── timestamp.json # Timestamp metadata"
|
||||
log_info " └── targets.json # Targets metadata"
|
||||
log_info ""
|
||||
log_warn "IMPORTANT: The metadata files are NOT YET SIGNED."
|
||||
log_warn "Run the signing script before publishing:"
|
||||
log_warn " ./scripts/sign-metadata.sh $OUTPUT_DIR"
|
||||
log_info ""
|
||||
log_warn "SECURITY: Move the root key to offline storage after signing!"
|
||||
189
devops/trust-repo-template/scripts/revoke-target.sh
Normal file
189
devops/trust-repo-template/scripts/revoke-target.sh
Normal file
@@ -0,0 +1,189 @@
|
||||
#!/bin/bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# revoke-target.sh
|
||||
# Sprint: SPRINT_20260125_003_Attestor_trust_workflows_conformance
|
||||
# Task: WORKFLOW-002 - Create key rotation workflow script
|
||||
# Description: Remove a target from the TUF repository
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
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"; }
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <target-name> [options]"
|
||||
echo ""
|
||||
echo "Remove a target from the TUF repository."
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " target-name Name of target to remove (e.g., rekor-key-v1)"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --repo DIR TUF repository directory (default: current directory)"
|
||||
echo " --archive Archive target file instead of deleting"
|
||||
echo " -h, --help Show this help message"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " $0 rekor-key-v1 --repo /path/to/tuf --archive"
|
||||
exit 1
|
||||
}
|
||||
|
||||
TARGET_NAME=""
|
||||
REPO_DIR="."
|
||||
ARCHIVE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--repo) REPO_DIR="$2"; shift 2 ;;
|
||||
--archive) ARCHIVE=true; shift ;;
|
||||
-h|--help) usage ;;
|
||||
-*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$TARGET_NAME" ]]; then
|
||||
TARGET_NAME="$1"
|
||||
else
|
||||
log_error "Unexpected argument: $1"
|
||||
usage
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TARGET_NAME" ]]; then
|
||||
log_error "Target name is required"
|
||||
usage
|
||||
fi
|
||||
|
||||
TARGETS_DIR="$REPO_DIR/targets"
|
||||
TARGETS_JSON="$REPO_DIR/targets.json"
|
||||
|
||||
if [[ ! -d "$TARGETS_DIR" ]]; then
|
||||
log_error "Targets directory not found: $TARGETS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TARGETS_JSON" ]]; then
|
||||
log_error "targets.json not found: $TARGETS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the target file
|
||||
TARGET_FILE=""
|
||||
for ext in "" ".pub" ".json" ".pem"; do
|
||||
if [[ -f "$TARGETS_DIR/${TARGET_NAME}${ext}" ]]; then
|
||||
TARGET_FILE="$TARGETS_DIR/${TARGET_NAME}${ext}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$TARGET_FILE" ]]; then
|
||||
log_warn "Target file not found in $TARGETS_DIR"
|
||||
log_info "Continuing to remove from targets.json..."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo " TUF Target Revocation"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
log_info "Repository: $REPO_DIR"
|
||||
log_info "Target: $TARGET_NAME"
|
||||
if [[ -n "$TARGET_FILE" ]]; then
|
||||
log_info "File: $TARGET_FILE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
log_warn "This will remove the target from the TUF repository."
|
||||
log_warn "Clients will no longer be able to fetch this target after sync."
|
||||
read -p "Type 'REVOKE' to proceed: " CONFIRM
|
||||
if [[ "$CONFIRM" != "REVOKE" ]]; then
|
||||
log_error "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove or archive the file
|
||||
if [[ -n "$TARGET_FILE" ]]; then
|
||||
if [[ "$ARCHIVE" == "true" ]]; then
|
||||
ARCHIVE_DIR="$REPO_DIR/archived"
|
||||
mkdir -p "$ARCHIVE_DIR"
|
||||
TIMESTAMP=$(date -u +%Y%m%d%H%M%S)
|
||||
ARCHIVE_NAME="$(basename "$TARGET_FILE")-revoked-${TIMESTAMP}"
|
||||
mv "$TARGET_FILE" "$ARCHIVE_DIR/$ARCHIVE_NAME"
|
||||
log_info "Archived to: $ARCHIVE_DIR/$ARCHIVE_NAME"
|
||||
else
|
||||
rm -f "$TARGET_FILE"
|
||||
log_info "Deleted: $TARGET_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Update targets.json
|
||||
if command -v python3 &>/dev/null; then
|
||||
python3 - "$TARGETS_JSON" "$TARGET_NAME" << 'PYTHON_SCRIPT'
|
||||
import json
|
||||
import sys
|
||||
|
||||
targets_json = sys.argv[1]
|
||||
target_name = sys.argv[2]
|
||||
|
||||
with open(targets_json) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Find and remove the target
|
||||
targets = data.get('signed', {}).get('targets', {})
|
||||
removed = False
|
||||
|
||||
# Try different name variations
|
||||
names_to_try = [
|
||||
target_name,
|
||||
f"{target_name}.pub",
|
||||
f"{target_name}.json",
|
||||
f"{target_name}.pem"
|
||||
]
|
||||
|
||||
for name in names_to_try:
|
||||
if name in targets:
|
||||
del targets[name]
|
||||
removed = True
|
||||
print(f"Removed from targets.json: {name}")
|
||||
break
|
||||
|
||||
if not removed:
|
||||
print(f"Warning: Target '{target_name}' not found in targets.json")
|
||||
sys.exit(0)
|
||||
|
||||
# Update version
|
||||
if 'signed' in data:
|
||||
data['signed']['version'] = data['signed'].get('version', 0) + 1
|
||||
|
||||
with open(targets_json, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
print(f"Updated: {targets_json}")
|
||||
PYTHON_SCRIPT
|
||||
else
|
||||
log_warn "Python not available. Manual update of targets.json required."
|
||||
log_warn "Remove the '$TARGET_NAME' entry from $TARGETS_JSON"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "Target revocation prepared."
|
||||
echo ""
|
||||
log_warn "NEXT STEPS (REQUIRED):"
|
||||
echo " 1. Re-sign targets.json with targets key"
|
||||
echo " 2. Update snapshot.json and sign with snapshot key"
|
||||
echo " 3. Update timestamp.json and sign with timestamp key"
|
||||
echo " 4. Deploy updated metadata to TUF server"
|
||||
echo ""
|
||||
log_info "Clients will stop trusting '$TARGET_NAME' after their next sync."
|
||||
echo ""
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"rekor": {
|
||||
"url": "https://rekor.sigstore.dev",
|
||||
"tile_base_url": "https://rekor.sigstore.dev/api/v1/log/entries/retrieve",
|
||||
"log_id": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d",
|
||||
"public_key_target": "rekor-key-v1"
|
||||
},
|
||||
"fulcio": {
|
||||
"url": "https://fulcio.sigstore.dev",
|
||||
"root_cert_target": "fulcio-chain.pem"
|
||||
},
|
||||
"ct_log": {
|
||||
"url": "https://ctfe.sigstore.dev",
|
||||
"public_key_target": "ctfe-key-v1"
|
||||
},
|
||||
"timestamp_authority": {
|
||||
"url": "https://tsa.sigstore.dev",
|
||||
"cert_chain_target": "tsa-chain.pem"
|
||||
},
|
||||
"overrides": {
|
||||
"staging": {
|
||||
"rekor_url": "https://rekor.sigstage.dev",
|
||||
"fulcio_url": "https://fulcio.sigstage.dev"
|
||||
},
|
||||
"development": {
|
||||
"rekor_url": "http://localhost:3000",
|
||||
"fulcio_url": "http://localhost:5555"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"updated_at": "2026-01-25T00:00:00Z",
|
||||
"note": "Production Sigstore public good instance endpoints"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user