#!/bin/sh # # Utility to manage an internal certificate authority using OpenSSL. set -eu PROGNAME=pki USAGE="" BOXCONF_ROOT=$(dirname "$(readlink -f "$0")") BOXCONF_CA_PASSWORD_FILE="${BOXCONF_ROOT}/.ca_password" CA_VALID_DAYS=3650 DEFAULT_VALID_DAYS=3650 EC_CURVE=prime256v1 DIGEST=sha256 CIPHER=aes256 usage(){ printf 'usage: %s %s\n' "$PROGNAME" "$USAGE" 2>&1 exit 2 } _pki_get_ca_password(){ # Acquire the CA password. # If the BOXCONF_CA_PASSWORD environment variable is set, use that. # Next, try reading the password from the .ca_password file. # If all else fails, prompt interactively. if [ -n "${BOXCONF_CA_PASSWORD:-}" ]; then return elif [ -f "$BOXCONF_CA_PASSWORD_FILE" ]; then BOXCONF_CA_PASSWORD=$(cat "$BOXCONF_CA_PASSWORD_FILE") else _boxconf_read_password 'Enter CA password: ' BOXCONF_CA_PASSWORD fi } _pki_dn2cnf() { # Convert an LDAP DN to its OpenSSL .cnf file representation. echo "$1" \ | tr ',' '\n' \ | awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--] }' \ | sed \ -e 's/^[Cc][Nn]=/commonName=/' \ -e 's/^[Oo]=/organizationName=/' \ -e 's/^[Oo][Uu]=/organizationalUnitName=/' \ -e 's/^[Dd][Cc]=/domainComponent=/' \ -e 's/^[Cc]=/countryName=/' \ -e 's/^[Ss][Tt]=/stateOrProvinceName=/' \ -e 's/^[Ll]=/locality=/' \ -e 's/^[Ss][Nn]=/surName=/' \ -e 's/^[Gg][Nn]=/givenName=/' \ -e 's/^[Uu][Ii][Dd]=/userId=/' \ | awk '{print NR-1 "." $0}' } _pki_postsign(){ # Create symlinks and delete temp files after signing a certificate. # $1 = certificate path # Create symlink with human-readable name. serial=$(awk 'END{print $3}' "${BOXCONF_CA_DIR}/index.txt") ln -sf "../certs/${serial}.pem" "${BOXCONF_CA_DIR}/${1}.crt" # Create fullchain certificate. cat "${BOXCONF_CA_DIR}/${1}.crt" "${BOXCONF_CA_DIR}/ca.crt" > "${BOXCONF_CA_DIR}/${1}.fullchain.crt" # Delete useless files. rm -f \ "${BOXCONF_CA_DIR}/index.txt.old" \ "${BOXCONF_CA_DIR}/index.txt.attr.old" \ "${BOXCONF_CA_DIR}/serial.old" } _pki_sign(){ # Given an OpenSSL config file, generate a signed certificate keypair. # $1 = certificate path # $2 = validity time (days) # Generate encrypted private key for the server certificate. PASS="$BOXCONF_VAULT_PASSWORD" openssl genpkey \ -algorithm ec \ -pkeyopt "ec_paramgen_curve:${EC_CURVE}" \ "-${CIPHER}" \ -pass env:PASS \ -out "${BOXCONF_CA_DIR}/${1}.key" # Generate the CSR. PASS="$BOXCONF_VAULT_PASSWORD" openssl req -new \ -key "${BOXCONF_CA_DIR}/${1}.key" \ "-${DIGEST}" \ -passin env:PASS \ -config "${BOXCONF_CA_DIR}/${1}.cnf" \ -out "${BOXCONF_CA_DIR}/${1}.csr" # Sign the certificate. PASS="$BOXCONF_CA_PASSWORD" openssl ca -batch \ -config "${BOXCONF_CA_DIR}/ca.cnf" \ -passin env:PASS \ ${2:+-days $2} \ -notext \ -out /dev/null \ -outdir "${BOXCONF_CA_DIR}/certs" \ -infiles "${BOXCONF_CA_DIR}/${1}.csr" _pki_postsign "$1" } _pki_renew(){ # Re-sign an existing certificate. # $1 = certificate path # $2 = validity time (time) _pki_get_ca_password # Sign the certificate. PASS="$BOXCONF_CA_PASSWORD" openssl ca -batch \ -config "${BOXCONF_CA_DIR}/ca.cnf" \ -passin env:PASS \ ${2:+-days $2} \ -notext \ -out /dev/null \ -outdir "${BOXCONF_CA_DIR}/certs" \ -infiles "${BOXCONF_CA_DIR}/${1}.csr" _pki_postsign "$1" } pki_init(){ # Initialize the CA. Create CA cert, private key, and OpenSSL configuration. USAGE='init [-c CONSTRAINT]... DOMAIN' constraints='' while getopts :c: opt; do case $opt in c) constraints="${constraints}, permitted;${OPTARG}" ;; :) usage ;; ?) usage ;; esac done shift $((OPTIND - 1 )) [ $# -eq 1 ] || usage domain=$1 [ -d "$BOXCONF_CA_DIR" ] && die 'CA already exists' _pki_get_ca_password mkdir -p "${BOXCONF_CA_DIR}/certs" # Generate encrypted private key for CA. PASS="$BOXCONF_CA_PASSWORD" openssl genpkey \ -algorithm ec \ -pkeyopt "ec_paramgen_curve:${EC_CURVE}" \ "-${CIPHER}" \ -pass env:PASS \ -out "${BOXCONF_CA_DIR}/ca.key" # Create a config file for the CA certificate. cat > "${BOXCONF_CA_DIR}/ca.cnf" < "${BOXCONF_CA_DIR}/${hostname}/${certname}.cnf" < "${BOXCONF_CA_DIR}/${hostname}/${certname}.cnf" <