Critical: build_leaf_cert self-signed leaves with the leaf key instead of the YubiKey-held Intermediate CA key. Now extracts TBS, signs via pkcs11-tool (ECDSA-SHA384), reassembles, and verifies against the intermediate CA public key before returning. - setup-certauth.sh: ~1100 lines of stale inline api/ copies replaced with copy-from-repo (single source of truth); writes private /etc/certauth/certauth.env (0600); DB init loads env, no more swallowed errors; systemd unit gets EnvironmentFile= - config.py: YubiKey serials no longer hard-coded (env, fail closed); aarch64-only PKCS#11 path replaced with arch-neutral default; all paths env-overridable (CERTAUTH_*) - main.py: removed dead fastapi.security.CSRFProtection import (crashed startup); module-relative static/templates dirs; created_by resolved from the authenticated user instead of hard-coded 1; unclosed file handles fixed; domain_id 0 stored as NULL (FK bug) - models.py: certificates.domain_id FK pointed at users(id), now domains(id) - login: CSRF token now actually sent and validated - tests: 23 tests (auth, API flows, DER helpers, signing pipeline) - README, LICENSE, requirements.txt, pyproject.toml
651 lines
22 KiB
Bash
651 lines
22 KiB
Bash
#!/bin/bash
|
|
###############################################################################
|
|
# CertAuth Setup Script
|
|
# Provisions a fresh Ubuntu machine as a Certificate Authority
|
|
#
|
|
# Prerequisites:
|
|
# - Fresh Ubuntu 24.04+ Server (aarch64/x86_64)
|
|
# - Two YubiKey 5 Nano devices plugged in
|
|
# - Root or sudo access
|
|
# - Network access to Ubuntu repositories
|
|
#
|
|
# Usage:
|
|
# sudo bash setup-certauth.sh
|
|
#
|
|
# Configuration: Edit the variables below or pass as environment variables
|
|
###############################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ===================== CONFIGURATION =====================
|
|
CA_ORG="${CA_ORG:-Home}"
|
|
CA_COUNTRY="${CA_COUNTRY:-US}"
|
|
ROOT_CA_CN="${ROOT_CA_CN:-certauth Root CA}"
|
|
INT_CA_CN="${INT_CA_CN:-certauth Intermediate CA}"
|
|
ROOT_VALID_DAYS="${ROOT_VALID_DAYS:-9125}" # 25 years
|
|
INT_VALID_DAYS="${INT_VALID_DAYS:-5475}" # 15 years
|
|
LEAF_VALID_DAYS="${LEAF_VALID_DAYS:-365}" # 1 year per cert
|
|
NETWORK_CIDR="${NETWORK_CIDR:-192.168.8.0/24}"
|
|
ADMIN_USER="${ADMIN_USER:-certauth}"
|
|
ADMIN_PASS="${ADMIN_PASS:-CHANGE_ME_ADMIN_PASS}"
|
|
|
|
# YubiKey assignment (will be detected automatically if not set)
|
|
YK1_SERIAL="${YK1_SERIAL:-}" # Root CA YubiKey serial
|
|
YK2_SERIAL="${YK2_SERIAL:-}" # Intermediate CA YubiKey serial
|
|
|
|
# PINs (will be generated if not set)
|
|
YK1_PIN="${YK1_PIN:-}"
|
|
YK1_PUK="${YK1_PUK:-}"
|
|
YK2_PIN="${YK2_PIN:-}"
|
|
YK2_PUK="${YK2_PUK:-}"
|
|
|
|
# ===================== COLORS =====================
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
|
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
|
error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; }
|
|
|
|
# ===================== PRE-FLIGHT CHECKS =====================
|
|
check_prerequisites() {
|
|
info "Running pre-flight checks..."
|
|
|
|
# Must be root
|
|
if [[ $EUID -ne 0 ]]; then
|
|
error "This script must be run as root (use sudo)"
|
|
fi
|
|
|
|
# Check Ubuntu version
|
|
if ! grep -q "Ubuntu" /etc/os-release; then
|
|
error "This script requires Ubuntu"
|
|
fi
|
|
UBUNTU_VERSION=$(grep VERSION_ID /etc/os-release | cut -d'"' -f2)
|
|
info "Ubuntu version: $UBUNTU_VERSION"
|
|
|
|
# Check architecture
|
|
ARCH=$(uname -m)
|
|
info "Architecture: $ARCH"
|
|
|
|
# Check YubiKeys
|
|
YK_COUNT=$(lsusb 2>/dev/null | grep -c "Yubico" || true)
|
|
if [[ $YK_COUNT -lt 2 ]]; then
|
|
error "Need at least 2 YubiKeys connected (found: $YK_COUNT)"
|
|
fi
|
|
success "Found $YK_COUNT YubiKeys"
|
|
|
|
# Check disk space
|
|
AVAIL=$(df -BG / | awk 'NR==2 {print $4}' | tr -d 'G')
|
|
if [[ $AVAIL -lt 5 ]]; then
|
|
error "Need at least 5GB free disk space (have: ${AVAIL}GB)"
|
|
fi
|
|
success "Disk space: ${AVAIL}GB available"
|
|
}
|
|
|
|
# ===================== STEP 1: SYSTEM SETUP =====================
|
|
setup_system() {
|
|
info "=== Step 1: System Setup ==="
|
|
|
|
# Update system
|
|
info "Updating package lists..."
|
|
apt-get update -qq
|
|
|
|
# Install base packages
|
|
info "Installing packages..."
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
|
ufw fail2ban opensc pcscd yubikey-manager ykcs11 \
|
|
python3-pip python3-venv caddy sqlite3 libsqlite3-dev \
|
|
haveged apparmor-utils curl wget \
|
|
2>/dev/null || true
|
|
|
|
# Install Python packages
|
|
info "Installing Python packages..."
|
|
pip3 install --break-system-packages --ignore-installed \
|
|
typing_extensions 2>/dev/null || true
|
|
pip3 install --break-system-packages \
|
|
fastapi "uvicorn[standard]" jinja2 python-multipart \
|
|
bcrypt "python-jose[cryptography]" ecdsa cryptography \
|
|
2>/dev/null || true
|
|
|
|
success "Packages installed"
|
|
}
|
|
|
|
# ===================== STEP 2: CREATE USER =====================
|
|
setup_user() {
|
|
info "=== Step 2: Create $ADMIN_USER user ==="
|
|
|
|
if ! id "$ADMIN_USER" &>/dev/null; then
|
|
useradd -m -s /bin/bash "$ADMIN_USER"
|
|
usermod -aG sudo,adm,plugdev,pcscd "$ADMIN_USER"
|
|
echo "$ADMIN_USER:$ADMIN_PASS" | chpasswd
|
|
echo "$ADMIN_USER ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/certauth
|
|
chmod 0440 /etc/sudoers.d/certauth
|
|
success "User $ADMIN_USER created"
|
|
else
|
|
warn "User $ADMIN_USER already exists"
|
|
fi
|
|
}
|
|
|
|
# ===================== STEP 3: FIREWALL =====================
|
|
setup_firewall() {
|
|
info "=== Step 3: Configure Firewall ==="
|
|
|
|
ufw --force reset 2>/dev/null || true
|
|
ufw default deny incoming
|
|
ufw default deny outgoing
|
|
ufw allow in on lo
|
|
ufw allow out on lo
|
|
ufw allow out 53
|
|
ufw allow out 123
|
|
ufw allow out 80/tcp
|
|
ufw allow out 443/tcp
|
|
ufw allow from "$NETWORK_CIDR" port 22 proto tcp
|
|
ufw allow from "$NETWORK_CIDR" port 80 proto tcp
|
|
ufw allow from "$NETWORK_CIDR" port 443 proto tcp
|
|
echo "y" | ufw enable
|
|
|
|
# Create swap
|
|
if [[ ! -f /swapfile ]]; then
|
|
fallocate -l 4G /swapfile
|
|
chmod 600 /swapfile
|
|
mkswap /swapfile
|
|
swapon /swapfile
|
|
echo "/swapfile none swap sw 0 0" >> /etc/fstab
|
|
success "Swap created (4GB)"
|
|
fi
|
|
|
|
success "Firewall configured"
|
|
}
|
|
|
|
# ===================== STEP 4: DETECT YUBIKEYS =====================
|
|
detect_yubikeys() {
|
|
info "=== Step 4: Detect YubiKeys ==="
|
|
|
|
# Start pcscd
|
|
systemctl enable pcscd
|
|
systemctl start pcscd
|
|
sleep 2
|
|
|
|
# Fix polkit for pcscd access
|
|
mkdir -p /etc/polkit-1/rules.d
|
|
cat > /etc/polkit-1/rules.d/45-access-pcsc.rules << 'POLKIT'
|
|
polkit.addRule(function(action, subject) {
|
|
if (action.id == "org.debian.pcsc-lite.access_pcsc") {
|
|
return polkit.Result.YES;
|
|
}
|
|
});
|
|
POLKIT
|
|
systemctl restart polkit
|
|
systemctl restart pcscd
|
|
sleep 2
|
|
|
|
# List YubiKeys
|
|
info "Connected YubiKeys:"
|
|
sudo -u "$ADMIN_USER" ykman list 2>/dev/null || ykman list
|
|
|
|
# Auto-detect serials if not set
|
|
SERIALS=$(ykman list 2>/dev/null | grep -oP 'Serial: \K\d+' || true)
|
|
if [[ -z "$YK1_SERIAL" ]]; then
|
|
YK1_SERIAL=$(echo "$SERIALS" | head -1)
|
|
fi
|
|
if [[ -z "$YK2_SERIAL" ]]; then
|
|
YK2_SERIAL=$(echo "$SERIALS" | tail -1)
|
|
fi
|
|
|
|
info "YK1 (Root CA): $YK1_SERIAL"
|
|
info "YK2 (Intermediate): $YK2_SERIAL"
|
|
|
|
# Generate PINs if not set
|
|
if [[ -z "$YK1_PIN" ]]; then
|
|
YK1_PIN=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
|
|
YK1_PUK=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
|
|
fi
|
|
if [[ -z "$YK2_PIN" ]]; then
|
|
YK2_PIN=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
|
|
YK2_PUK=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
|
|
fi
|
|
|
|
echo ""
|
|
echo "============================================================"
|
|
echo " YUBIKEY CREDENTIALS - RECORD THESE SECURELY"
|
|
echo "============================================================"
|
|
echo ""
|
|
echo "YK1 (Root CA) - Serial $YK1_SERIAL:"
|
|
echo " PIN: $YK1_PIN"
|
|
echo " PUK: $YK1_PUK"
|
|
echo ""
|
|
echo "YK2 (Intermediate) - Serial $YK2_SERIAL:"
|
|
echo " PIN: $YK2_PIN"
|
|
echo " PUK: $YK2_PUK"
|
|
echo ""
|
|
echo "============================================================"
|
|
echo ""
|
|
}
|
|
|
|
# ===================== STEP 5: CONFIGURE YUBIKEYS =====================
|
|
configure_yubikeys() {
|
|
info "=== Step 5: Configure YubiKeys ==="
|
|
DEFAULT_MGMT="010203040506070801020304050607080102030405060708"
|
|
|
|
# --- YK1: Root CA ---
|
|
info "Configuring YK1 (Root CA)..."
|
|
|
|
echo "y" | ykman -d "$YK1_SERIAL" piv reset
|
|
ykman -d "$YK1_SERIAL" piv keys generate --algorithm ECCP384 \
|
|
-m "$DEFAULT_MGMT" 9c /tmp/yk1-root-pub.pem
|
|
ykman -d "$YK1_SERIAL" piv access change-pin -P 123456 --new-pin "$YK1_PIN"
|
|
ykman -d "$YK1_SERIAL" piv access change-puk -p 12345678 --new-puk "$YK1_PUK"
|
|
echo "" | ykman -d "$YK1_SERIAL" piv certificates generate \
|
|
-P "$YK1_PIN" -m "$DEFAULT_MGMT" \
|
|
--subject "CN=$ROOT_CA_CN,O=$CA_ORG,C=$CA_COUNTRY" \
|
|
--valid-days "$ROOT_VALID_DAYS" --hash-algorithm SHA384 \
|
|
9c /tmp/yk1-root-pub.pem
|
|
|
|
ykman -d "$YK1_SERIAL" piv certificates export 9c /tmp/root-ca-temp.crt
|
|
success "YK1 configured"
|
|
|
|
# --- YK2: Intermediate CA ---
|
|
info "Configuring YK2 (Intermediate CA)..."
|
|
|
|
echo "y" | ykman -d "$YK2_SERIAL" piv reset
|
|
ykman -d "$YK2_SERIAL" piv keys generate --algorithm ECCP384 \
|
|
-m "$DEFAULT_MGMT" 9c /tmp/yk2-int-pub.pem
|
|
ykman -d "$YK2_SERIAL" piv access change-pin -P 123456 --new-pin "$YK2_PIN"
|
|
ykman -d "$YK2_SERIAL" piv access change-puk -p 12345678 --new-puk "$YK2_PUK"
|
|
echo "" | ykman -d "$YK2_SERIAL" piv certificates request \
|
|
-P "$YK2_PIN" \
|
|
--subject "CN=$INT_CA_CN,O=$CA_ORG,C=$CA_COUNTRY" \
|
|
9c /tmp/yk2-int-pub.pem /tmp/yk2-intermediate.csr
|
|
|
|
success "YK2 configured"
|
|
}
|
|
|
|
# ===================== STEP 6: GENERATE CA CERTS =====================
|
|
generate_ca_certs() {
|
|
info "=== Step 6: Generate CA Certificates ==="
|
|
|
|
# Create directories
|
|
mkdir -p /etc/ssl/ca/{root,intermediate,issued,crl}
|
|
mkdir -p /var/lib/certauth/tmp
|
|
mkdir -p /var/log/certauth
|
|
chown -R "$ADMIN_USER:$ADMIN_USER" /etc/ssl/ca /var/lib/certauth /var/log/certauth
|
|
chmod -R 700 /etc/ssl/ca /var/lib/certauth /var/log/certauth
|
|
|
|
# Generate Root CA cert with proper extensions
|
|
info "Generating Root CA certificate..."
|
|
python3 << ROOTPY
|
|
import datetime, subprocess, base64
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.x509.oid import NameOID
|
|
|
|
with open("/tmp/yk1-root-pub.pem", "rb") as f:
|
|
root_pub = serialization.load_pem_public_key(f.read())
|
|
|
|
subj = x509.Name([
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "$CA_COUNTRY"),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "$CA_ORG"),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, "$ROOT_CA_CN"),
|
|
])
|
|
|
|
builder = (x509.CertificateBuilder()
|
|
.subject_name(subj).issuer_name(subj)
|
|
.public_key(root_pub).serial_number(x509.random_serial_number())
|
|
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
|
|
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=$ROOT_VALID_DAYS))
|
|
.add_extension(x509.BasicConstraints(ca=True, path_length=1), critical=True)
|
|
.add_extension(x509.KeyUsage(digital_signature=True, key_cert_sign=True, crl_sign=True,
|
|
key_encipherment=False, content_commitment=False, data_encipherment=False,
|
|
key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
|
|
.add_extension(x509.SubjectKeyIdentifier.from_public_key(root_pub), critical=False)
|
|
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(root_pub), critical=False))
|
|
|
|
tmp = ec.generate_private_key(ec.SECP384R1())
|
|
temp = builder.sign(tmp, hashes.SHA384())
|
|
td = temp.public_bytes(serialization.Encoding.DER)
|
|
|
|
# Parse TBS
|
|
o = 1
|
|
if td[o] & 0x80: n = td[o] & 0x7f; o += 1 + n
|
|
else: o += 1
|
|
tbs_start = o
|
|
o += 1
|
|
if td[o] & 0x80: n = td[o] & 0x7f; tl = int.from_bytes(td[o+1:o+1+n], "big"); o += 1 + n
|
|
else: tl = td[o]; o += 1
|
|
tbs_end = o + tl
|
|
tbs_full = td[tbs_start:tbs_end]
|
|
alg_start = tbs_end
|
|
o2 = alg_start + 1
|
|
if td[o2] & 0x80: n = td[o2] & 0x7f; al = int.from_bytes(td[o2+1:o2+1+n], "big"); o2 += 1 + n
|
|
else: al = td[o2]; o2 += 1
|
|
alg_full = td[alg_start:o2+al]
|
|
|
|
# Sign with YubiKey
|
|
with open("/tmp/tbs.der", "wb") as f: f.write(tbs_full)
|
|
r = subprocess.run(["pkcs11-tool", "--login", "--pin", "$YK1_PIN",
|
|
"--sign", "--mechanism", "ECDSA-SHA384", "--label", "SIGN key",
|
|
"--input-file", "/tmp/tbs.der", "--output-file", "/tmp/sig.bin"],
|
|
capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
print(f"Sign failed: {r.stderr}")
|
|
exit(1)
|
|
|
|
with open("/tmp/sig.bin", "rb") as f: raw = f.read()
|
|
rb = raw[:48].lstrip(b"\x00") or b"\x00"
|
|
sb = raw[48:].lstrip(b"\x00") or b"\x00"
|
|
if rb[0] & 0x80: rb = b"\x00" + rb
|
|
if sb[0] & 0x80: sb = b"\x00" + sb
|
|
r_der = b"\x02" + bytes([len(rb)]) + rb
|
|
s_der = b"\x02" + bytes([len(sb)]) + sb
|
|
seq = b"\x30" + bytes([len(r_der+s_der)]) + r_der + s_der
|
|
bs = b"\x00" + seq
|
|
new_sig = b"\x03" + bytes([len(bs)]) + bs
|
|
|
|
content = tbs_full + alg_full + new_sig
|
|
cl = len(content)
|
|
final = b"\x30\x82" + bytes([cl>>8, cl&0xff]) + content
|
|
|
|
with open("/tmp/root.der", "wb") as f: f.write(final)
|
|
r = subprocess.run(["openssl", "x509", "-inform", "DER", "-outform", "PEM",
|
|
"-in", "/tmp/root.der", "-out", "/tmp/root.pem"], capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
print(f"DER->PEM failed: {r.stderr}")
|
|
exit(1)
|
|
|
|
with open("/tmp/root.pem") as f: pem = f.read()
|
|
with open("/etc/ssl/ca/root/root-ca.crt", "w") as f: f.write(pem)
|
|
|
|
# Verify
|
|
r = subprocess.run(["openssl", "verify", "-CAfile", "/etc/ssl/ca/root/root-ca.crt",
|
|
"/etc/ssl/ca/root/root-ca.crt"], capture_output=True, text=True)
|
|
print(f"Root CA self-verify: {r.stdout.strip() or r.stderr.strip()}")
|
|
|
|
# Import to YK1
|
|
subprocess.run(["echo", "", "|", "ykman", "-d", "$YK1_SERIAL", "piv", "certificates",
|
|
"import", "9c", "/etc/ssl/ca/root/root-ca.crt"], shell=True)
|
|
print("Root CA imported to YK1")
|
|
ROOTPY
|
|
|
|
# Generate Intermediate CA cert signed by Root
|
|
info "Generating Intermediate CA certificate..."
|
|
python3 << INTPY
|
|
import datetime, subprocess, base64
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.x509.oid import NameOID
|
|
|
|
with open("/tmp/yk1-root-pub.pem", "rb") as f:
|
|
root_pub = serialization.load_pem_public_key(f.read())
|
|
with open("/tmp/yk2-int-pub.pem", "rb") as f:
|
|
int_pub = serialization.load_pem_public_key(f.read())
|
|
|
|
root_subj = x509.Name([
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "$CA_COUNTRY"),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "$CA_ORG"),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, "$ROOT_CA_CN"),
|
|
])
|
|
int_subj = x509.Name([
|
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "$CA_COUNTRY"),
|
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "$CA_ORG"),
|
|
x509.NameAttribute(NameOID.COMMON_NAME, "$INT_CA_CN"),
|
|
])
|
|
|
|
builder = (x509.CertificateBuilder()
|
|
.subject_name(int_subj).issuer_name(root_subj)
|
|
.public_key(int_pub).serial_number(x509.random_serial_number())
|
|
.not_valid_before(datetime.datetime.now(datetime.timezone.utc))
|
|
.not_valid_after(datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=$INT_VALID_DAYS))
|
|
.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
|
|
.add_extension(x509.KeyUsage(digital_signature=True, key_cert_sign=True, crl_sign=True,
|
|
key_encipherment=False, content_commitment=False, data_encipherment=False,
|
|
key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
|
|
.add_extension(x509.SubjectKeyIdentifier.from_public_key(int_pub), critical=False)
|
|
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(root_pub), critical=False))
|
|
|
|
tmp = ec.generate_private_key(ec.SECP384R1())
|
|
temp = builder.sign(tmp, hashes.SHA384())
|
|
td = temp.public_bytes(serialization.Encoding.DER)
|
|
|
|
o = 1
|
|
if td[o] & 0x80: n = td[o] & 0x7f; o += 1 + n
|
|
else: o += 1
|
|
tbs_start = o
|
|
o += 1
|
|
if td[o] & 0x80: n = td[o] & 0x7f; tl = int.from_bytes(td[o+1:o+1+n], "big"); o += 1 + n
|
|
else: tl = td[o]; o += 1
|
|
tbs_end = o + tl
|
|
tbs_full = td[tbs_start:tbs_end]
|
|
alg_start = tbs_end
|
|
o2 = alg_start + 1
|
|
if td[o2] & 0x80: n = td[o2] & 0x7f; al = int.from_bytes(td[o2+1:o2+1+n], "big"); o2 += 1 + n
|
|
else: al = td[o2]; o2 += 1
|
|
alg_full = td[alg_start:o2+al]
|
|
|
|
with open("/tmp/tbs.der", "wb") as f: f.write(tbs_full)
|
|
r = subprocess.run(["pkcs11-tool", "--login", "--pin", "$YK1_PIN",
|
|
"--sign", "--mechanism", "ECDSA-SHA384", "--label", "SIGN key",
|
|
"--input-file", "/tmp/tbs.der", "--output-file", "/tmp/sig.bin"],
|
|
capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
print(f"Sign failed: {r.stderr}")
|
|
exit(1)
|
|
|
|
with open("/tmp/sig.bin", "rb") as f: raw = f.read()
|
|
rb = raw[:48].lstrip(b"\x00") or b"\x00"
|
|
sb = raw[48:].lstrip(b"\x00") or b"\x00"
|
|
if rb[0] & 0x80: rb = b"\x00" + rb
|
|
if sb[0] & 0x80: sb = b"\x00" + sb
|
|
r_der = b"\x02" + bytes([len(rb)]) + rb
|
|
s_der = b"\x02" + bytes([len(sb)]) + sb
|
|
seq = b"\x30" + bytes([len(r_der+s_der)]) + r_der + s_der
|
|
bs = b"\x00" + seq
|
|
new_sig = b"\x03" + bytes([len(bs)]) + bs
|
|
|
|
content = tbs_full + alg_full + new_sig
|
|
cl = len(content)
|
|
final = b"\x30\x82" + bytes([cl>>8, cl&0xff]) + content
|
|
|
|
with open("/tmp/int.der", "wb") as f: f.write(final)
|
|
r = subprocess.run(["openssl", "x509", "-inform", "DER", "-outform", "PEM",
|
|
"-in", "/tmp/int.der", "-out", "/tmp/int.pem"], capture_output=True, text=True)
|
|
if r.returncode != 0:
|
|
print(f"DER->PEM failed: {r.stderr}")
|
|
exit(1)
|
|
|
|
with open("/tmp/int.pem") as f: pem = f.read()
|
|
with open("/etc/ssl/ca/intermediate/intermediate-ca.crt", "w") as f: f.write(pem)
|
|
|
|
# Verify chain
|
|
r = subprocess.run(["openssl", "verify", "-CAfile", "/etc/ssl/ca/root/root-ca.crt",
|
|
"/etc/ssl/ca/intermediate/intermediate-ca.crt"], capture_output=True, text=True)
|
|
print(f"Chain verify: {r.stdout.strip() or r.stderr.strip()}")
|
|
|
|
# Import to YK2
|
|
subprocess.run(["echo", "", "|", "ykman", "-d", "$YK2_SERIAL", "piv", "certificates",
|
|
"import", "9c", "/etc/ssl/ca/intermediate/intermediate-ca.crt"], shell=True)
|
|
print("Intermediate CA imported to YK2")
|
|
INTPY
|
|
|
|
# Create CA chain file
|
|
cat /etc/ssl/ca/intermediate/intermediate-ca.crt /etc/ssl/ca/root/root-ca.crt \
|
|
> /etc/ssl/ca/ca-chain.crt
|
|
|
|
success "CA certificates generated and verified"
|
|
}
|
|
|
|
# ===================== STEP 7: INSTALL API =====================
|
|
install_api() {
|
|
info "=== Step 7: Install Certificate API ==="
|
|
|
|
# Create API directory
|
|
mkdir -p /opt/certauth/{api/templates,api/static,logs}
|
|
chown -R "$ADMIN_USER:$ADMIN_USER" /opt/certauth
|
|
|
|
# Deploy API from the repository (single source of truth: the api/
|
|
# directory next to this script). Never inline a second copy here.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
for f in config.py models.py auth.py signing.py main.py; do
|
|
if [ ! -f "$SCRIPT_DIR/api/$f" ]; then
|
|
error "api/$f not found next to setup-certauth.sh (run from the repository checkout)"
|
|
fi
|
|
done
|
|
cp "$SCRIPT_DIR/api/config.py" "$SCRIPT_DIR/api/models.py" "$SCRIPT_DIR/api/auth.py" \
|
|
"$SCRIPT_DIR/api/signing.py" "$SCRIPT_DIR/api/main.py" /opt/certauth/api/
|
|
cp -r "$SCRIPT_DIR/api/templates/." /opt/certauth/api/templates/
|
|
mkdir -p "$SCRIPT_DIR/api/static"
|
|
cp -r "$SCRIPT_DIR/api/static/." /opt/certauth/api/static/
|
|
|
|
# Runtime configuration. The API fails closed on startup if any variable
|
|
# is missing, so this file must be complete and private.
|
|
JWT_SECRET_GENERATED="$(python3 -c 'import secrets; print(secrets.token_hex(32))')"
|
|
install -d -m 750 -o "$ADMIN_USER" -g "$ADMIN_USER" /etc/certauth
|
|
cat > /etc/certauth/certauth.env << ENV_EOF
|
|
YK_ROOT_SERIAL=$YK1_SERIAL
|
|
YK_INT_SERIAL=$YK2_SERIAL
|
|
YK_ROOT_PIN=$YK1_PIN
|
|
YK_INT_PIN=$YK2_PIN
|
|
JWT_SECRET=$JWT_SECRET_GENERATED
|
|
ADMIN_USERNAME=$ADMIN_USER
|
|
ADMIN_PASSWORD=$ADMIN_PASS
|
|
ENV_EOF
|
|
chown "$ADMIN_USER:$ADMIN_USER" /etc/certauth/certauth.env
|
|
chmod 600 /etc/certauth/certauth.env
|
|
|
|
# Initialize database (loads the private env file as the API user)
|
|
sudo -u "$ADMIN_USER" bash -c 'set -a; . /etc/certauth/certauth.env; set +a; cd /opt/certauth/api; python3 -c "from models import init_db; init_db()"'
|
|
|
|
success "API installed"
|
|
}
|
|
|
|
# ===================== STEP 8: SERVICES =====================
|
|
setup_services() {
|
|
info "=== Step 8: Configure Services ==="
|
|
|
|
# Caddy
|
|
cat > /etc/caddy/Caddyfile << 'CADDYEOF'
|
|
:80 {
|
|
encode gzip
|
|
reverse_proxy 127.0.0.1:8000 {
|
|
header_up Host {host}
|
|
header_up X-Real-IP {remote}
|
|
}
|
|
}
|
|
CADDYEOF
|
|
|
|
# Systemd service for API
|
|
cat > /etc/systemd/system/certauth-api.service << SVCCEOF
|
|
[Unit]
|
|
Description=CertAuth Certificate Management API
|
|
After=network.target pcscd.service
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=$ADMIN_USER
|
|
Group=$ADMIN_USER
|
|
WorkingDirectory=/opt/certauth/api
|
|
Environment=PYTHONUNBUFFERED=1
|
|
EnvironmentFile=/etc/certauth/certauth.env
|
|
ExecStart=/usr/bin/python3 -m uvicorn main:app --host 127.0.0.1 --port 8000
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
SVCCEOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable caddy certauth-api pcscd
|
|
systemctl start caddy certauth-api
|
|
|
|
success "Services configured and started"
|
|
}
|
|
|
|
# ===================== STEP 9: VERIFY =====================
|
|
verify_setup() {
|
|
info "=== Step 9: Verification ==="
|
|
|
|
sleep 3
|
|
|
|
# Check services
|
|
for svc in certauth-api caddy pcscd; do
|
|
STATUS=$(systemctl is-active "$svc")
|
|
if [[ "$STATUS" == "active" ]]; then
|
|
success "$svc: active"
|
|
else
|
|
warn "$svc: $STATUS"
|
|
fi
|
|
done
|
|
|
|
# Test API
|
|
HEALTH=$(curl -s http://localhost:8000/api/health 2>/dev/null || echo "FAILED")
|
|
if echo "$HEALTH" | grep -q "ok"; then
|
|
success "API health check: OK"
|
|
else
|
|
warn "API health check: $HEALTH"
|
|
fi
|
|
|
|
# Verify CA chain
|
|
openssl verify -CAfile /etc/ssl/ca/root/root-ca.crt \
|
|
/etc/ssl/ca/intermediate/intermediate-ca.crt 2>&1 | while read line; do
|
|
if echo "$line" | grep -q "OK"; then
|
|
success "CA chain verification: OK"
|
|
else
|
|
warn "CA chain: $line"
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo "============================================================"
|
|
echo " SETUP COMPLETE"
|
|
echo "============================================================"
|
|
echo ""
|
|
echo " Access the Key Vault UI at:"
|
|
echo " http://$(hostname -I | awk '{print $1}')/"
|
|
echo ""
|
|
echo " Admin login:"
|
|
echo " Username: $ADMIN_USER"
|
|
echo " Password: $ADMIN_PASS"
|
|
echo ""
|
|
echo " API endpoints:"
|
|
echo " POST /api/token - Login (returns JWT)"
|
|
echo " GET /api/domains - List domains"
|
|
echo " POST /api/domains - Register domain"
|
|
echo " GET /api/certs - List certificates"
|
|
echo " POST /api/certs/request - Request certificate"
|
|
echo " POST /api/certs/{id}/sign - Sign certificate (requires YK2)"
|
|
echo " GET /api/certs/{id}/download - Download cert"
|
|
echo " GET /api/certs/{id}/key - Download private key"
|
|
echo " GET /api/ca-chain - Download CA chain"
|
|
echo ""
|
|
echo " IMPORTANT: Change the admin password after first login!"
|
|
echo "============================================================"
|
|
}
|
|
|
|
# ===================== MAIN =====================
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
echo ""
|
|
echo "============================================================"
|
|
echo " CertAuth Setup"
|
|
echo "============================================================"
|
|
echo ""
|
|
|
|
check_prerequisites
|
|
setup_system
|
|
setup_user
|
|
setup_firewall
|
|
detect_yubikeys
|
|
configure_yubikeys
|
|
generate_ca_certs
|
|
install_api
|
|
setup_services
|
|
verify_setup
|
|
|
|
echo ""
|
|
success "All done!" |