Open TSA — Free European RFC 3161 Timestamp Authority. - 4-tier CA hierarchy via setup-ca.sh (Root → TSA Root → Intermediate → Signing) - Node.js service (service.js): RFC 3161 endpoint, health, info, stats, /now, Prometheus metrics, rate limiting, audit log, admin endpoints - All identities and paths configurable via environment variables - Documented in README, including reverse-proxy setup and operating notes MIT licensed. See https://open-tsa.eu for the live service.
526 lines
21 KiB
Bash
Executable file
526 lines
21 KiB
Bash
Executable file
#!/bin/bash
|
|
# ============================================================
|
|
# Open TSA — CA & Certificate Setup (4-tier hierarchy)
|
|
# https://open-tsa.eu
|
|
#
|
|
# Builds a 4-tier RFC 3161 TSA CA hierarchy from scratch.
|
|
# Designed to be re-used by independent operators, not only by
|
|
# the Open TSA project itself. All names, organisations, paths,
|
|
# and the policy OID are configurable via environment variables.
|
|
#
|
|
# The defaults below are deliberately neutral and project-agnostic
|
|
# so that any operator can run this script unchanged for a basic
|
|
# evaluation, and override only what is needed for production.
|
|
#
|
|
# CA Hierarchy:
|
|
# 1. Root CA (self-signed, 25 years) ← OFFLINE/HSM
|
|
# 2. TSA Root CA (15 years) ← OFFLINE
|
|
# 3. TSA Intermediate CA (10 years, server)
|
|
# 4. TSA Signing Cert (2 years, operational)
|
|
#
|
|
# OpenSSL 3.x compatible (AlmaLinux 9, RHEL 9, Debian 12)
|
|
# MIT License — Open TSA Project
|
|
#
|
|
# Usage:
|
|
# sudo bash setup-ca.sh
|
|
#
|
|
# Environment variables (override defaults):
|
|
# COUNTRY Two-letter country code (default: DE)
|
|
# CA_DIR Path for CA hierarchy (default: /opt/open-tsa/ca)
|
|
# PUBLIC_DIR Path for public certificates (default: /opt/open-tsa/public/certs)
|
|
# CONFIG_PATH Path for openssl-tsa.cnf (default: /opt/open-tsa/openssl-tsa.cnf)
|
|
#
|
|
# ROOT_ORG Root CA "O=" (default: Open TSA)
|
|
# ROOT_CN Root CA "CN=" (default: Open TSA Root CA)
|
|
#
|
|
# TSA_ROOT_ORG TSA Root + lower tiers "O=" (default: Open TSA)
|
|
# TSA_ROOT_CN TSA Root CA "CN=" (default: Open TSA TSA Root CA)
|
|
# TSA_INTER_CN Intermediate CA "CN=" (default: Open TSA TSA Intermediate CA)
|
|
# TSA_SIGN_CN Signing Cert "CN=" (default: Open TSA TSA Signing Certificate)
|
|
#
|
|
# DOMAIN Public service hostname (default: tsr.example.org)
|
|
# (informational only — used in the summary)
|
|
#
|
|
# TSA_POLICY_OID Your policy OID (default: 1.3.6.1.4.1.99999.1.1)
|
|
# Allocate your own under IANA PEN
|
|
# (https://pen.iana.org/) — do not reuse
|
|
# another project's OID in production.
|
|
#
|
|
# SERVICE_USER User the service runs as (default: nodejs)
|
|
# SERVICE_GROUP Group the service runs as (default: nodejs)
|
|
#
|
|
# Example for an independent operator in Finland:
|
|
#
|
|
# sudo COUNTRY=FI \
|
|
# CA_DIR=/opt/example-tsa/ca \
|
|
# PUBLIC_DIR=/opt/example-tsa/public/certs \
|
|
# CONFIG_PATH=/opt/example-tsa/openssl-tsa.cnf \
|
|
# ROOT_ORG="example.fi" \
|
|
# ROOT_CN="example.fi Root CA" \
|
|
# TSA_ROOT_ORG="tsa.example.fi" \
|
|
# TSA_ROOT_CN="example.fi TSA Root CA" \
|
|
# TSA_INTER_CN="example.fi TSA Intermediate CA" \
|
|
# TSA_SIGN_CN="example.fi TSA Signing Certificate" \
|
|
# DOMAIN=tsr.example.fi \
|
|
# TSA_POLICY_OID="1.3.6.1.4.1.99999.1.1" \
|
|
# bash setup-ca.sh
|
|
# ============================================================
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Paths ─────────────────────────────────────────────────────
|
|
CA_DIR="${CA_DIR:-/opt/open-tsa/ca}"
|
|
PUBLIC_DIR="${PUBLIC_DIR:-/opt/open-tsa/public/certs}"
|
|
CONFIG_PATH="${CONFIG_PATH:-/opt/open-tsa/openssl-tsa.cnf}"
|
|
|
|
# ── Country ───────────────────────────────────────────────────
|
|
COUNTRY="${COUNTRY:-DE}"
|
|
|
|
# ── Identities ────────────────────────────────────────────────
|
|
# Neutral defaults — production operators should override these.
|
|
ROOT_ORG="${ROOT_ORG:-Open TSA}"
|
|
ROOT_CN="${ROOT_CN:-Open TSA Root CA}"
|
|
|
|
TSA_ROOT_ORG="${TSA_ROOT_ORG:-Open TSA}"
|
|
TSA_ROOT_CN="${TSA_ROOT_CN:-Open TSA TSA Root CA}"
|
|
|
|
TSA_INTER_CN="${TSA_INTER_CN:-Open TSA TSA Intermediate CA}"
|
|
TSA_SIGN_CN="${TSA_SIGN_CN:-Open TSA TSA Signing Certificate}"
|
|
|
|
DOMAIN="${DOMAIN:-tsr.example.org}"
|
|
|
|
# ── TSA Policy OID ────────────────────────────────────────────
|
|
# RFC 3161 requires a policy OID in every issued timestamp token.
|
|
# The default below is a placeholder under the IANA PEN reserved range.
|
|
# DO NOT use this OID in production — register your own at
|
|
# https://pen.iana.org/ and substitute it here.
|
|
TSA_POLICY_OID="${TSA_POLICY_OID:-1.3.6.1.4.1.99999.1.1}"
|
|
|
|
# ── Service user (the user that the Node.js service runs as) ─
|
|
SERVICE_USER="${SERVICE_USER:-nodejs}"
|
|
SERVICE_GROUP="${SERVICE_GROUP:-nodejs}"
|
|
|
|
GREEN='\033[0;32m'
|
|
BLUE='\033[0;34m'
|
|
RED='\033[0;31m'
|
|
YELLOW='\033[0;33m'
|
|
NC='\033[0m'
|
|
|
|
echo -e "${BLUE}=== Open TSA — 4-Tier CA Setup ===${NC}"
|
|
echo -e "${BLUE}Hierarchy:${NC}"
|
|
echo " 1. $ROOT_CN (25y, OFFLINE)"
|
|
echo " 2. $TSA_ROOT_CN (15y, OFFLINE)"
|
|
echo " 3. $TSA_INTER_CN (10y)"
|
|
echo " 4. $TSA_SIGN_CN (2y)"
|
|
echo ""
|
|
echo -e "${BLUE}Paths:${NC}"
|
|
echo " CA_DIR : $CA_DIR"
|
|
echo " PUBLIC_DIR : $PUBLIC_DIR"
|
|
echo " CONFIG_PATH : $CONFIG_PATH"
|
|
echo ""
|
|
echo -e "${BLUE}Service:${NC}"
|
|
echo " user/group : $SERVICE_USER:$SERVICE_GROUP"
|
|
echo " policy OID : $TSA_POLICY_OID"
|
|
echo ""
|
|
|
|
# Warn if the user is reusing the placeholder OID
|
|
if [ "$TSA_POLICY_OID" = "1.3.6.1.4.1.99999.1.1" ]; then
|
|
echo -e "${YELLOW}WARNING: You are using the placeholder policy OID.${NC}"
|
|
echo -e "${YELLOW} Please allocate your own at https://pen.iana.org/ before production use.${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
# ── Pre-flight ────────────────────────────────────────────────
|
|
if [ "$EUID" -ne 0 ]; then
|
|
echo -e "${RED}This script must be run as root (sudo).${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
|
|
echo -e "${RED}Service user '$SERVICE_USER' does not exist.${NC}"
|
|
echo "Create it first: useradd -r -m -s /sbin/nologin $SERVICE_USER"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -d "$CA_DIR" ]; then
|
|
echo -e "${RED}Warning: $CA_DIR already exists. It will be reset.${NC}"
|
|
read -p "Continue? (yes/no): " -r CONFIRM
|
|
if [ "$CONFIRM" != "yes" ]; then
|
|
echo "Aborted."
|
|
exit 0
|
|
fi
|
|
rm -rf "$CA_DIR"
|
|
fi
|
|
|
|
# ── Directory structure ───────────────────────────────────────
|
|
for tier in root tsa-root intermediate tsa; do
|
|
mkdir -p "$CA_DIR/$tier"/{private,certs,crl,newcerts}
|
|
touch "$CA_DIR/$tier/index.txt"
|
|
# IMPORTANT: OpenSSL parses the serial file as a hex string and requires
|
|
# an EVEN number of characters. "1000" (4 chars) is fine, "10000"
|
|
# (5 chars) would trigger "a2i_ASN1_INTEGER: odd number of chars".
|
|
# If you change the start value later, always keep it even-length —
|
|
# e.g. "010000" instead of "10000".
|
|
echo "1000" > "$CA_DIR/$tier/serial"
|
|
echo "1000" > "$CA_DIR/$tier/crlnumber"
|
|
done
|
|
|
|
chmod 700 "$CA_DIR/root/private"
|
|
chmod 700 "$CA_DIR/tsa-root/private"
|
|
chmod 700 "$CA_DIR/intermediate/private"
|
|
chmod 750 "$CA_DIR/tsa/private"
|
|
mkdir -p "$PUBLIC_DIR"
|
|
mkdir -p "$(dirname "$CONFIG_PATH")"
|
|
|
|
# ── openssl.cnf — Tier 1: Root CA ─────────────────────────────
|
|
cat > "$CA_DIR/root/openssl.cnf" << EOF
|
|
[ ca ]
|
|
default_ca = CA_default
|
|
|
|
[ CA_default ]
|
|
dir = $CA_DIR/root
|
|
certs = \$dir/certs
|
|
crl_dir = \$dir/crl
|
|
new_certs_dir = \$dir/newcerts
|
|
database = \$dir/index.txt
|
|
serial = \$dir/serial
|
|
private_key = \$dir/private/root.key
|
|
certificate = \$dir/certs/root.crt
|
|
crlnumber = \$dir/crlnumber
|
|
default_md = sha256
|
|
policy = policy_loose
|
|
default_days = 3650
|
|
copy_extensions = copy
|
|
|
|
[ policy_loose ]
|
|
countryName = optional
|
|
organizationName = optional
|
|
commonName = supplied
|
|
|
|
[ req ]
|
|
default_bits = 4096
|
|
distinguished_name = req_distinguished_name
|
|
string_mask = utf8only
|
|
default_md = sha256
|
|
prompt = no
|
|
|
|
[ req_distinguished_name ]
|
|
C = $COUNTRY
|
|
O = $ROOT_ORG
|
|
CN = $ROOT_CN
|
|
|
|
[ v3_root_ca ]
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
|
|
[ v3_sub_root_ca ]
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true, pathlen:1
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
EOF
|
|
|
|
# ── openssl.cnf — Tier 2: TSA Root CA ─────────────────────────
|
|
cat > "$CA_DIR/tsa-root/openssl.cnf" << EOF
|
|
[ ca ]
|
|
default_ca = CA_default
|
|
|
|
[ CA_default ]
|
|
dir = $CA_DIR/tsa-root
|
|
certs = \$dir/certs
|
|
crl_dir = \$dir/crl
|
|
new_certs_dir = \$dir/newcerts
|
|
database = \$dir/index.txt
|
|
serial = \$dir/serial
|
|
private_key = \$dir/private/tsa-root.key
|
|
certificate = \$dir/certs/tsa-root.crt
|
|
crlnumber = \$dir/crlnumber
|
|
default_md = sha256
|
|
policy = policy_loose
|
|
default_days = 3650
|
|
copy_extensions = copy
|
|
|
|
[ policy_loose ]
|
|
countryName = optional
|
|
organizationName = optional
|
|
commonName = supplied
|
|
|
|
[ req ]
|
|
default_bits = 4096
|
|
distinguished_name = req_distinguished_name
|
|
string_mask = utf8only
|
|
default_md = sha256
|
|
prompt = no
|
|
|
|
[ req_distinguished_name ]
|
|
C = $COUNTRY
|
|
O = $TSA_ROOT_ORG
|
|
CN = $TSA_ROOT_CN
|
|
|
|
[ v3_intermediate_ca ]
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:true, pathlen:0
|
|
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
|
|
EOF
|
|
|
|
# ── openssl.cnf — Tier 3: TSA Intermediate CA ─────────────────
|
|
cat > "$CA_DIR/intermediate/openssl.cnf" << EOF
|
|
[ ca ]
|
|
default_ca = CA_default
|
|
|
|
[ CA_default ]
|
|
dir = $CA_DIR/intermediate
|
|
certs = \$dir/certs
|
|
crl_dir = \$dir/crl
|
|
new_certs_dir = \$dir/newcerts
|
|
database = \$dir/index.txt
|
|
serial = \$dir/serial
|
|
private_key = \$dir/private/intermediate.key
|
|
certificate = \$dir/certs/intermediate.crt
|
|
crlnumber = \$dir/crlnumber
|
|
default_md = sha384
|
|
policy = policy_loose
|
|
default_days = 730
|
|
copy_extensions = copy
|
|
|
|
[ policy_loose ]
|
|
countryName = optional
|
|
organizationName = optional
|
|
commonName = supplied
|
|
|
|
[ req ]
|
|
default_bits = 4096
|
|
distinguished_name = req_distinguished_name
|
|
string_mask = utf8only
|
|
default_md = sha384
|
|
prompt = no
|
|
|
|
[ req_distinguished_name ]
|
|
C = $COUNTRY
|
|
O = $TSA_ROOT_ORG
|
|
CN = $TSA_INTER_CN
|
|
|
|
[ v3_tsa ]
|
|
subjectKeyIdentifier = hash
|
|
authorityKeyIdentifier = keyid:always,issuer
|
|
basicConstraints = critical, CA:false
|
|
keyUsage = critical, digitalSignature
|
|
extendedKeyUsage = critical, timeStamping
|
|
EOF
|
|
|
|
echo -e "${GREEN}✓ Configuration files created${NC}"
|
|
|
|
# ── [1/4] Root CA ─────────────────────────────────────────────
|
|
echo -e "\n${BLUE}[1/4] $ROOT_CN (25 years)...${NC}"
|
|
openssl genrsa -out "$CA_DIR/root/private/root.key" 4096
|
|
chmod 400 "$CA_DIR/root/private/root.key"
|
|
openssl req -new -x509 \
|
|
-config "$CA_DIR/root/openssl.cnf" \
|
|
-key "$CA_DIR/root/private/root.key" \
|
|
-out "$CA_DIR/root/certs/root.crt" \
|
|
-days 9125 \
|
|
-extensions v3_root_ca \
|
|
-subj "/C=$COUNTRY/O=$ROOT_ORG/CN=$ROOT_CN"
|
|
echo -e "${GREEN}✓ Root CA created${NC}"
|
|
openssl x509 -noout -subject -dates -in "$CA_DIR/root/certs/root.crt"
|
|
|
|
# ── [2/4] TSA Root CA ─────────────────────────────────────────
|
|
echo -e "\n${BLUE}[2/4] $TSA_ROOT_CN (15 years)...${NC}"
|
|
openssl genrsa -out "$CA_DIR/tsa-root/private/tsa-root.key" 4096
|
|
chmod 400 "$CA_DIR/tsa-root/private/tsa-root.key"
|
|
openssl req -new \
|
|
-key "$CA_DIR/tsa-root/private/tsa-root.key" \
|
|
-out "$CA_DIR/tsa-root/certs/tsa-root.csr" \
|
|
-subj "/C=$COUNTRY/O=$TSA_ROOT_ORG/CN=$TSA_ROOT_CN"
|
|
openssl ca -batch \
|
|
-config "$CA_DIR/root/openssl.cnf" \
|
|
-extensions v3_sub_root_ca \
|
|
-days 5475 -notext \
|
|
-in "$CA_DIR/tsa-root/certs/tsa-root.csr" \
|
|
-out "$CA_DIR/tsa-root/certs/tsa-root.crt"
|
|
echo -e "${GREEN}✓ TSA Root CA created${NC}"
|
|
openssl x509 -noout -subject -dates -in "$CA_DIR/tsa-root/certs/tsa-root.crt"
|
|
|
|
# ── [3/4] TSA Intermediate CA ─────────────────────────────────
|
|
echo -e "\n${BLUE}[3/4] $TSA_INTER_CN (10 years)...${NC}"
|
|
openssl genrsa -out "$CA_DIR/intermediate/private/intermediate.key" 4096
|
|
chmod 400 "$CA_DIR/intermediate/private/intermediate.key"
|
|
openssl req -new \
|
|
-key "$CA_DIR/intermediate/private/intermediate.key" \
|
|
-out "$CA_DIR/intermediate/certs/intermediate.csr" \
|
|
-subj "/C=$COUNTRY/O=$TSA_ROOT_ORG/CN=$TSA_INTER_CN"
|
|
openssl ca -batch \
|
|
-config "$CA_DIR/tsa-root/openssl.cnf" \
|
|
-extensions v3_intermediate_ca \
|
|
-days 3650 -notext \
|
|
-in "$CA_DIR/intermediate/certs/intermediate.csr" \
|
|
-out "$CA_DIR/intermediate/certs/intermediate.crt"
|
|
echo -e "${GREEN}✓ TSA Intermediate CA created${NC}"
|
|
|
|
# ── [4/4] TSA Signing Certificate ─────────────────────────────
|
|
echo -e "\n${BLUE}[4/4] $TSA_SIGN_CN (2 years)...${NC}"
|
|
openssl genrsa -out "$CA_DIR/tsa/private/tsa.key" 4096
|
|
chown "root:$SERVICE_GROUP" "$CA_DIR/tsa/private/tsa.key"
|
|
chmod 440 "$CA_DIR/tsa/private/tsa.key"
|
|
chown "root:$SERVICE_GROUP" "$CA_DIR/tsa/private"
|
|
chmod 750 "$CA_DIR/tsa/private"
|
|
openssl req -new \
|
|
-key "$CA_DIR/tsa/private/tsa.key" \
|
|
-out "$CA_DIR/tsa/certs/tsa.csr" \
|
|
-subj "/C=$COUNTRY/O=$TSA_ROOT_ORG/CN=$TSA_SIGN_CN"
|
|
openssl ca -batch \
|
|
-config "$CA_DIR/intermediate/openssl.cnf" \
|
|
-extensions v3_tsa \
|
|
-days 730 -notext \
|
|
-in "$CA_DIR/tsa/certs/tsa.csr" \
|
|
-out "$CA_DIR/tsa/certs/tsa.crt"
|
|
|
|
# chain.pem = Intermediate only (OpenSSL 3.x: ess_cert_id_chain = no)
|
|
cp "$CA_DIR/intermediate/certs/intermediate.crt" "$CA_DIR/tsa/certs/chain.pem"
|
|
echo -e "${GREEN}✓ TSA Signing Certificate created${NC}"
|
|
|
|
# ── Public certs ──────────────────────────────────────────────
|
|
# ca.crt = Root (trust anchor)
|
|
cp "$CA_DIR/root/certs/root.crt" "$PUBLIC_DIR/ca.crt"
|
|
cp "$CA_DIR/tsa-root/certs/tsa-root.crt" "$PUBLIC_DIR/tsa-root.crt"
|
|
cp "$CA_DIR/intermediate/certs/intermediate.crt" "$PUBLIC_DIR/intermediate.crt"
|
|
cp "$CA_DIR/tsa/certs/tsa.crt" "$PUBLIC_DIR/tsa.crt"
|
|
cp "$CA_DIR/tsa/certs/chain.pem" "$PUBLIC_DIR/chain.pem"
|
|
|
|
# fullchain.pem = all 3 CAs for easy verification
|
|
cat "$CA_DIR/intermediate/certs/intermediate.crt" \
|
|
"$CA_DIR/tsa-root/certs/tsa-root.crt" \
|
|
"$CA_DIR/root/certs/root.crt" \
|
|
> "$PUBLIC_DIR/fullchain.pem"
|
|
|
|
chmod 644 "$PUBLIC_DIR"/*.crt "$PUBLIC_DIR"/*.pem
|
|
|
|
# ── openssl-tsa.cnf ───────────────────────────────────────────
|
|
cat > "$CONFIG_PATH" << EOF
|
|
[ tsa ]
|
|
default_tsa = tsa_config
|
|
|
|
[ tsa_config ]
|
|
dir = $CA_DIR/tsa
|
|
serial = \$dir/serial
|
|
crypto_device = builtin
|
|
signer_cert = $CA_DIR/tsa/certs/tsa.crt
|
|
signer_key = $CA_DIR/tsa/private/tsa.key
|
|
signer_digest = sha256
|
|
default_policy = $TSA_POLICY_OID
|
|
# Do NOT set other_policies — an empty value crashes OpenSSL 3.x
|
|
digests = sha256, sha384, sha512
|
|
accuracy = secs:1
|
|
clock_precision_digits = 0
|
|
ordering = yes
|
|
tsa_name = yes
|
|
ess_cert_id_chain = no
|
|
EOF
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Set runtime permissions for the service user
|
|
#
|
|
# The service (service.js) runs as $SERVICE_USER:$SERVICE_GROUP
|
|
# and must be able to write serial, index.txt, and newcerts/.
|
|
# Only the TSA tier is writable at runtime — the upper CA tiers
|
|
# (Root, TSA-Root, Intermediate) remain root:root.
|
|
# ──────────────────────────────────────────────────────────────
|
|
echo -e "\n${BLUE}Setting runtime permissions for service user...${NC}"
|
|
|
|
chown "$SERVICE_USER:$SERVICE_GROUP" "$CA_DIR/tsa/serial"
|
|
chown "$SERVICE_USER:$SERVICE_GROUP" "$CA_DIR/tsa/index.txt"
|
|
chown "$SERVICE_USER:$SERVICE_GROUP" "$CA_DIR/tsa/newcerts"
|
|
chmod 644 "$CA_DIR/tsa/serial"
|
|
chmod 644 "$CA_DIR/tsa/index.txt"
|
|
chmod 755 "$CA_DIR/tsa/newcerts"
|
|
|
|
echo " serial: $(stat -c '%U:%G %a' "$CA_DIR/tsa/serial")"
|
|
echo " index.txt: $(stat -c '%U:%G %a' "$CA_DIR/tsa/index.txt")"
|
|
echo " newcerts/: $(stat -c '%U:%G %a' "$CA_DIR/tsa/newcerts")"
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Smoke test: verify serial generation actually works
|
|
# ──────────────────────────────────────────────────────────────
|
|
echo -e "\n${BLUE}Smoke test — serial generation...${NC}"
|
|
|
|
TESTFILE=$(mktemp)
|
|
echo "smoke-test-$(date +%s)" > "$TESTFILE"
|
|
|
|
# Generate TSQ as the service user (no root involvement)
|
|
sudo -u "$SERVICE_USER" openssl ts -query \
|
|
-data "$TESTFILE" -cert -sha256 -no_nonce \
|
|
-out "${TESTFILE}.tsq" 2>/dev/null
|
|
|
|
# Generate TSR. If permissions are wrong, this writes a non-Granted TSR
|
|
# with exit code 0 — we MUST verify the status explicitly.
|
|
sudo -u "$SERVICE_USER" openssl ts -reply \
|
|
-config "$CONFIG_PATH" \
|
|
-queryfile "${TESTFILE}.tsq" \
|
|
-signer "$CA_DIR/tsa/certs/tsa.crt" \
|
|
-inkey "$CA_DIR/tsa/private/tsa.key" \
|
|
-chain "$CA_DIR/tsa/certs/chain.pem" \
|
|
-out "${TESTFILE}.tsr" 2>/dev/null
|
|
|
|
STATUS=$(openssl ts -reply -in "${TESTFILE}.tsr" -text 2>/dev/null | grep "Status:" || true)
|
|
|
|
if ! echo "$STATUS" | grep -q "Granted"; then
|
|
echo -e "${RED}✗ Smoke test FAILED:${NC} $STATUS"
|
|
echo -e "${RED}Check filesystem permissions on $CA_DIR/tsa/${NC}"
|
|
rm -f "$TESTFILE" "${TESTFILE}.tsq" "${TESTFILE}.tsr"
|
|
exit 1
|
|
fi
|
|
|
|
SERIAL1=$(openssl ts -reply -in "${TESTFILE}.tsr" -text 2>/dev/null | grep "Serial" | head -1)
|
|
echo -e " ${GREEN}✓${NC} First request: $STATUS ($SERIAL1)"
|
|
|
|
# Second request — serial MUST increment
|
|
sudo -u "$SERVICE_USER" openssl ts -reply \
|
|
-config "$CONFIG_PATH" \
|
|
-queryfile "${TESTFILE}.tsq" \
|
|
-signer "$CA_DIR/tsa/certs/tsa.crt" \
|
|
-inkey "$CA_DIR/tsa/private/tsa.key" \
|
|
-chain "$CA_DIR/tsa/certs/chain.pem" \
|
|
-out "${TESTFILE}.tsr2" 2>/dev/null
|
|
|
|
SERIAL2=$(openssl ts -reply -in "${TESTFILE}.tsr2" -text 2>/dev/null | grep "Serial" | head -1)
|
|
|
|
if [ "$SERIAL1" = "$SERIAL2" ]; then
|
|
echo -e "${RED}✗ Serial counter NOT incrementing: $SERIAL1 == $SERIAL2${NC}"
|
|
echo -e "${RED}Check that $CA_DIR/tsa/serial is writable by $SERVICE_USER.${NC}"
|
|
rm -f "$TESTFILE" "${TESTFILE}.tsq" "${TESTFILE}.tsr" "${TESTFILE}.tsr2"
|
|
exit 1
|
|
fi
|
|
|
|
echo -e " ${GREEN}✓${NC} Second request: $SERIAL2"
|
|
echo -e " ${GREEN}✓ Serial counter increments correctly.${NC}"
|
|
|
|
rm -f "$TESTFILE" "${TESTFILE}.tsq" "${TESTFILE}.tsr" "${TESTFILE}.tsr2"
|
|
|
|
# ── Summary ───────────────────────────────────────────────────
|
|
echo ""
|
|
echo -e "${BLUE}============================================${NC}"
|
|
echo -e "${GREEN}Setup complete!${NC}"
|
|
echo -e "${BLUE}============================================${NC}"
|
|
echo ""
|
|
echo "Certificate hierarchy:"
|
|
echo ""
|
|
openssl x509 -noout -subject -dates -in "$CA_DIR/root/certs/root.crt"
|
|
openssl x509 -noout -subject -dates -in "$CA_DIR/tsa-root/certs/tsa-root.crt"
|
|
openssl x509 -noout -subject -dates -in "$CA_DIR/intermediate/certs/intermediate.crt"
|
|
openssl x509 -noout -subject -dates -in "$CA_DIR/tsa/certs/tsa.crt"
|
|
echo ""
|
|
echo "Public certificates: $PUBLIC_DIR"
|
|
echo "TSA config: $CONFIG_PATH"
|
|
echo "TSA endpoint (after service deployment): https://$DOMAIN"
|
|
echo ""
|
|
echo -e "${YELLOW}IMPORTANT — secure the root keys offline:${NC}"
|
|
echo " $CA_DIR/root/private/root.key"
|
|
echo " $CA_DIR/tsa-root/private/tsa-root.key"
|
|
echo ""
|
|
echo "Export them to a USB stick, verify the copy, then delete from this server."
|
|
echo "These keys are only needed every 2 years to renew the TSA signing certificate"
|
|
echo "and after disasters to recover the CA chain."
|
|
echo ""
|