#!/bin/sh
# Sign a CSR with the intermediate CA, choosing extensions per certificate.
# The per-cert extension file is kept in ext/, so every cert's settings are on record.
set -eu
CA=/root/ca/intermediate
CONF=$CA/openssl.cnf
INTER=$CA/certs/intermediate.cert.pem
CHAIN=$CA/certs/ca-chain.cert.pem
CDP_URL=
http://pki.mydomain.com/intermediate.crl
AIA_URL=
http://pki.mydomain.com/intermediate.cer
DEFAULT_DAYS=397
MAX_DAYS=825
usage() {
cat >&2 <<EOF
usage: issue-cert -n FQDN -r CSR [-s SAN]... [-p PROFILE] [-d DAYS] [-C] [-A] [-o DIR] [-y]
-n FQDN primary name: first DNS SAN and the output file prefix
-r CSR certificate signing request (PEM)
-s SAN extra SAN, repeatable: DNS:alias.mydomain.com or IP:10.0.0.4
-p PROFILE server (default) | server-client | client
-d DAYS validity in days (default $DEFAULT_DAYS, max $MAX_DAYS)
-C add the CRL Distribution Point ($CDP_URL)
-A add AIA caIssuers ($AIA_URL)
-o DIR also copy the cert and full chain to DIR
-y don't ask to sign/commit (the passphrase is still asked)
EOF
exit 2
}
die() {
echo "issue-cert: $*" >&2
exit 1
}
NAME= CSR= EXTRA= PROFILE=server DAYS=$DEFAULT_DAYS CDP=no AIA=no OUT= BATCH=
while getopts
n:r:s:p:d:CAo:yh opt; do
case $opt in
n) NAME=$OPTARG ;;
r) CSR=$OPTARG ;;
s) EXTRA="$EXTRA,$OPTARG" ;;
p) PROFILE=$OPTARG ;;
d) DAYS=$OPTARG ;;
C) CDP=yes ;;
A) AIA=yes ;;
o) OUT=$OPTARG ;;
y) BATCH=-batch ;;
*) usage ;;
esac
done
if [ -z "$NAME" ] || [ -z "$CSR" ]; then usage; fi
# --- check the arguments ---
[ -r "$CSR" ] || die "can't read $CSR"
case $NAME in
*.*) ;;
*) die "-n must be a fully qualified name, e.g. host.mydomain.com" ;;
esac
case $DAYS in
''|*[!0-9]*) die "-d needs a whole number of days" ;;
esac
[ "$DAYS" -le "$MAX_DAYS" ] || die "$DAYS days is over the $MAX_DAYS-day limit"
case $PROFILE in
server) EKU=serverAuth ;;
server-client) EKU="serverAuth, clientAuth" ;;
client) EKU=clientAuth ;;
*) die "unknown profile '$PROFILE'" ;;
esac
for s in $(echo "$EXTRA" | tr ',' ' '); do
case $s in
DNS:?*|IP:?*|email:?*|URI:?*) ;;
*) die "bad SAN '$s' - use DNS:, IP:, email: or URI:" ;;
esac
done
if [ -n "$OUT" ] && [ ! -d "$OUT" ]; then die "no such directory $OUT"; fi
# --- check the CSR and pick key usage from its key type ---
openssl req -in "$CSR" -noout -verify >/dev/null 2>&1 || die "CSR self-signature does not verify"
TEXT=$(openssl req -in "$CSR" -noout -text)
ALG=$(echo "$TEXT" | sed -n 's/^ *Public Key Algorithm: *//p' | head -n 1)
case $ALG in
rsaEncryption)
BITS=$(echo "$TEXT" | sed -n 's/.*Public-Key: (\([0-9]*\) bit).*/\1/p' | head -n 1)
[ "${BITS:-0}" -ge 2048 ] || die "RSA key is ${BITS:-?} bits; 2048 is the minimum"
KU="critical, digitalSignature, keyEncipherment" ;;
id-ecPublicKey)
KU="critical, digitalSignature" ;;
*) die "unsupported key algorithm '$ALG' - use RSA or ECDSA" ;;
esac
CN=$(openssl req -in "$CSR" -noout -subject -nameopt multiline | sed -n 's/^ *commonName *= *//p')
if [ "$CN" != "$NAME" ]; then
echo "issue-cert: note: CSR Common Name is '$CN'; the SAN will use '$NAME'" >&2
fi
# --- output names ---
STAMP=$(date +%Y-%m-%d)
BASE=$NAME.$STAMP
EXT=$CA/ext/$BASE.ext
KEEPCSR=$CA/csr/$BASE.csr.pem
CRT=$CA/certs/$BASE.cert.pem
FULL=$CA/certs/$BASE.fullchain.pem
if [ -e "$CRT" ]; then die "$CRT already exists"; fi
mkdir -p "$CA/ext" "$CA/csr"
cp "$CSR" "$KEEPCSR"
# --- write this cert's extension file ---
{
echo "[ leaf ]"
echo "basicConstraints = critical, CA:FALSE"
echo "keyUsage = $KU"
echo "extendedKeyUsage = $EKU"
echo "subjectKeyIdentifier = hash"
echo "authorityKeyIdentifier = keyid:always"
echo "subjectAltName = DNS:$NAME$EXTRA"
if [ "$CDP" = yes ]; then echo "crlDistributionPoints = URI:$CDP_URL"; fi
if [ "$AIA" = yes ]; then echo "authorityInfoAccess = caIssuers;URI:$AIA_URL"; fi
} > "$EXT"
# --- sign ---
if ! openssl ca -config "$CONF" -extfile "$EXT" -extensions leaf \
-days "$DAYS" -notext $BATCH -in "$KEEPCSR" -out "$CRT" ||
[ ! -s "$CRT" ]; then
rm -f "$CRT" "$EXT" "$KEEPCSR"
die "certificate not issued"
fi
# --- full chain, verify, summary ---
cat "$CRT" "$INTER" > "$FULL"
chmod 444 "$CRT" "$FULL"
openssl verify -CAfile "$CHAIN" "$CRT"
openssl x509 -in "$CRT" -noout -subject -serial -enddate -ext subjectAltName,extendedKeyUsage
if [ -n "$OUT" ]; then
cp "$CRT" "$FULL" "$OUT/"
chmod 444 "$OUT/$BASE.cert.pem" "$OUT/$BASE.fullchain.pem"
fi
echo "cert: $CRT"
echo "full chain: $FULL"
echo "extensions: $EXT"