diff options
Diffstat (limited to 'letsencrypt')
-rwxr-xr-x | letsencrypt | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/letsencrypt b/letsencrypt new file mode 100755 index 0000000..50406f7 --- /dev/null +++ b/letsencrypt @@ -0,0 +1,281 @@ +#!/bin/bash + +# Dependencies nc(1), openssl(1), socat(1) + +set -ue +set -o pipefail +PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +NAME=$(basename $0) + +WWW_USER=www-data +WWW_GROUP=www-data +ACME_WEBSERVER=acme-webserver +ACME_CLIENT=acme-slave + +declare COMMAND ACCOUNTKEY +declare -l GENKEY +declare RUNAS QUIET= DEBUG= + +declare SRVCERT= CHAIN= CSR SRVKEY +declare -l HASH= +declare SUBJECT=/ +declare SAN= +# https://security.stackexchange.com/questions/24106/which-key-usages-are-required-by-each-key-exchange-method +declare KEYUSAGE='digitalSignature, keyEncipherment, keyCertSign' +declare -a NOTIFY=() + +usage() { + local msg="${1:-}" + if [ "$msg" ]; then + echo "$NAME: $msg" >&2 + echo "Try '$NAME --help' for more information." >&2 + exit 1 + fi + cat <<- EOF + Usage: $NAME [OPTIONS] new-reg ACCOUNTKEY [EMAIL ..] + or: $NAME [OPTIONS] new-cert ACCOUNTKEY --output=CERT {--csr=CSR | CSR Options } + or: $NAME [OPTIONS] revoke-cert ACCOUNTKEY CERT [CERT ..] + + ACCOUNTKEY is the private key file of the user's account. Generic options are: + --genkey[=ALGO[:BITS]] For 'new-*' commands, generate key pairs (with mode 0600) if they don't + exist already. (Default: "RSA".) RSA is the only algorithm curently supported. + --runas=USERNAME Username to run the ACME client as. (This user doesn't need access to + any private key material.) + --help, -? Display this help text and exit + --quiet, -q Be quiet + --debug Turn on debug mode + + $NAME new-reg ACCOUNTKEY [EMAIL ..] + Register a new ACCOUNTKEY; an optional list of EMAIL addresses can be supplied as contact information. + + $NAME new-cert ACCOUNTKEY --output=CERT --csr=FILE + $NAME new-cert ACCOUNTKEY --output=CERT --key=FILE [--hash=ALGO] [--subject=STRING] [--san=STRING] [--keyusage=STRING] + Request a new Certificate Issuance. The Certificate Signing Request can be supplied directly, or + generated from the server key. + + --csr=FILE Certificate Signing Request to send (alternatively, use --key to generate it) + --key=FILE Server private key (use --genkey to generate it) + --hash=DGST Message digest to sign the CSR with (in PEM format) + --subject=STRING Subject name, formatted as "/type0=value0/type1=value1/type2=..." (default: "/") + --san=STRING Comma-separated list of Subject Alternative Names formatted as "type:value" + --keyusage=STRING Comma-separated list of Key Usages, see x509v3_config(5ssl) + (default: "digitalSignature,keyEncipherment,keyCertSign") + --chain Store not only the server certificate in the file specified with --output, but + also the CA's + --output=FILE Where to store the issued (signed) X.509 certificate + --notify=COMMAND Command to run upon success. (This option can be repeated.) + + $NAME revoke-cert {ACCOUNTKEY|SVRKEY} FILE [FILE ..] + Request that the given certificate(s) FILE(s) be revoked. The first argument can be either the account + key file or the server's private key. + EOF + exit 0 +} + +# Generate a key pair with mode 0600 +genkey() { + local key="$1" genkey= bits= + case "$GENKEY" in + rsa) genkey=genrsa;; + rsa:*) genkey=genrsa; bits=${GENKEY#*:};; + *) echo "Error: invalid key type ${GENKEY%%:*}" >&2; exit 1;; + esac + install --mode=0600 /dev/null "$key" # atomic creation with mode 0600 + openssl "$genkey" $bits >"$key" +} + + +# Parse options +declare -a ARGV=() +while [ $# -gt 0 ]; do + case "$1" in + --genkey) GENKEY=RSA;; + --genkey=*) GENKEY="${1#*=}";; + --runas=*) RUNAS="${1#*=}";; + --help|-\?) usage;; + --quiet|-q) QUIET=1;; + --debug) DEBUG=1;; + + --output=*) SRVCERT="${1#*=}";; + --chain) CHAIN=1;; + --csr=*) CSR="${1#*=}";; + --key=*) SRVKEY="${1#*=}";; + --hash=*) HASH="${1#*=}";; + --subject=*) SUBJECT="${1#*=}";; + --san=*) SAN="${1#*=}";; + --keyusage=*) KEYUSAGE="${1#*=}";; + --notify=*) NOTIFY+=( "${1#*=}" );; + + --) shift; ARGV+=( "$@" ); break ;; + -*) usage "unknown option '${1%%=*}'";; + *) if [ ${ACCOUNTKEY+x} ]; then + ARGV+=( "$1" ) + else + [ ${COMMAND+x} ] && ACCOUNTKEY="$1" || COMMAND="$1" + fi + esac + shift +done + +[ "${COMMAND:-}" ] || usage "missing command" +[ "$COMMAND" = 'new-reg' -o "$COMMAND" = 'new-cert' -o "$COMMAND" = 'revoke-cert' ] || + usage "invalid command $COMMAND" +[ ${#ARGV[@]} -eq 0 -o "$COMMAND" = 'new-reg' -o "$COMMAND" = 'revoke-cert' ] || + usage "invalid argument ${ARGV[0]}" + +[ "${ACCOUNTKEY:-}" ] || usage "missing account key" +if ! [ -f "$ACCOUNTKEY" -a -s "$ACCOUNTKEY" ]; then + if [ "$COMMAND" = 'new-reg' -a "${GENKEY+x}" ]; then + genkey "$ACCOUNTKEY" + else + echo "Error: keyfile '$ACCOUNTKEY' does not exist." >&2 + [[ ! "$COMMAND" =~ ^new- ]] || echo "Use 'new-reg --genkey' to generate and register an RSA key with mode 0600." >&2 + exit 1 + fi +fi + + +declare -a TMPFILES=() +declare CHALLENGE_DIR IPTABLES_SAVE +cleanup() { + set +e + [ ! ${IPTABLES_SAVE+x} ] || iptables-restore -c <"$IPTABLES_SAVE" + pkill -TERM -P $$ + [ ${#TMPFILES[@]} -eq 0 ] || rm -${DEBUG:+v}f "${TMPFILES[@]}" + [ ! ${CHALLENGE_DIR+x} ] || rm -${DEBUG:+v}rf "$CHALLENGE_DIR" +} +trap cleanup EXIT SIGINT + + +# Extract the public part of the user key +accountpub=$(mktemp --tmpdir XXXXXX.pub) +TMPFILES+=( "$accountpub" ) +openssl pkey -pubout <"$ACCOUNTKEY" >"$accountpub" +chmod 0644 "$accountpub" + + +if [ "$COMMAND" = 'revoke-cert' ]; then + if [ ${#ARGV[@]} -eq 0 ]; then + echo "Error: Nothing to revoke" >&2 + exit 1 + fi +elif [ "$COMMAND" = 'new-cert' ]; then + if [ ! "${SRVCERT:-}" ]; then + echo "Error: Missing --output" >&2 + exit 1 + fi + + # Generate a Certificate Signing Request if need be + if [ ${CSR+x} ]; then + if [ -z "$CSR" -o ! -s "$CSR" ]; then + echo "Error: Missing Certificate Signing Request $CSR. (One of --csr or --key must be set.)" >&2 + exit 1 + fi + else + CSR=$(mktemp --tmpdir XXXXXX.csr) + config=$(mktemp --tmpdir XXXXXX.cnf) + TMPFILES+=( "$CSR" "$config" ) + chmod 0644 "$CSR" + + if [ "$HASH" ]; then + case "$HASH" in + md5|rmd160|sha1|sha224|sha256|sha384|sha512) HASH="-$HASH";; + *) echo "Invalid digest algorithm: $HASH" >&2; exit 1;; + esac + fi + if [ ! "${SRVKEY:-}" ] || [ ! -s "$SRVKEY" ]; then + if [ "${SRVKEY:-}" -a "${GENKEY+x}" ]; then + genkey "$SRVKEY" + else + echo "Error: Missing private server key. Use new-cert --genkey --key=... to generate" >&2 + exit 1 + fi + fi + + [ ! "$DEBUG" ] || echo 'Generating Certificate Signing Request...' >&2 + cat >"$config" <<- EOF + [ req ] + distinguished_name = req_distinguished_name + req_extensions = v3_req + + [ req_distinguished_name ] + + [ v3_req ] + ${SAN:+subjectAltName= $SAN} + # XXX Golang errors on extensions marked critical + # https://github.com/letsencrypt/boulder/issues/565 + #basicConstraints = critical, CA:FALSE + #keyUsage = critical${KEYUSAGE:+, $KEYUSAGE} + basicConstraints = CA:FALSE + keyUsage = $KEYUSAGE + subjectKeyIdentifier = hash + EOF + openssl req -new -batch -key "$SRVKEY" $HASH -subj "$SUBJECT" -config "$config" -reqexts v3_req >"$CSR" + fi + [ ! "$DEBUG" ] || openssl req -noout -text <"$CSR" + + CHALLENGE_DIR=$(mktemp --tmpdir -d acme-challenge.XXXXXX) + x509=$(mktemp --tmpdir XXXXXX.pem) + TMPFILES+=( "$x509" ) + + [ ! "${RUNAS:-}" ] || chown "$RUNAS" "$CHALLENGE_DIR" "$x509" + chgrp "$WWW_GROUP" "$CHALLENGE_DIR" + chmod 0750 "$CHALLENGE_DIR" + + # Make sure a webserver is configured to server ACME challenges + if nc -z 127.0.0.1 80; then + [ ! "$DEBUG" ] || echo "Using existing webserver" >&2 + ln -${DEBUG:+v}Ts "$CHALLENGE_DIR" /var/www/acme-challenge + TMPFILES+=( "/var/www/acme-challenge" ) + else + temp=$(mktemp --tmpdir) + TMPFILES+=( "$temp" ) + iptables-save -c >"$temp" + IPTABLES_SAVE="$temp" + iptables -I INPUT -p tcp -m tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT + iptables -I OUTPUT -p tcp -m tcp --sport 80 -m state --state ESTABLISHED -j ACCEPT + ( + [ ! "$DEBUG" ] || echo "Starting ACME webserver in $CHALLENGE_DIR" >&2 + cd "$CHALLENGE_DIR" || exit 1 + exec socat \ + TCP-LISTEN:80,setgid="$WWW_GROUP",setuid="$WWW_USER",reuseaddr,fork,max-children=5 \ + EXEC:"$ACME_WEBSERVER" + )& + fi + + ARGV=( "$CSR" "$CHALLENGE_DIR" "$x509" ) +fi + + +pipe=$(mktemp --tmpdir -u XXXXXX.fifo) +mkfifo -m0600 "$pipe" +TMPFILES+=( "$pipe" ) + +# Wait for signing requests from the ACME slave +acme_client() { + if [ "${RUNAS:-}" ]; then + sudo -u "$RUNAS" ${DEBUG:+DEBUG="$DEBUG"} -- $ACME_CLIENT "$@" + else + [ ${RUNAS+x} ] || echo "WARNING: Use --runas=USER to specify a user to drop permissions to." >&2 + DEBUG="$DEBUG" $ACME_CLIENT "$@" + fi +} + +acme_client "$COMMAND" "$accountpub" ${ARGV[0]+"${ARGV[@]}"} <"$pipe" | +while read data; do + echo -n "$data" | openssl dgst -sha256 -sign "$ACCOUNTKEY" -hex | sed 's/.*=\s*//' +done >"$pipe" + +if [ "$COMMAND" = 'new-cert' ]; then + # TODO + # Verify: dump and compare public keys + # Valid cert, signed by the right CA + + # Copy "$x509" to "$SRVCERT", possibly chained + # https://crt.sh/?q=cse-fresti.cse.chalmers.se&iCAID=7395 + cp "$x509" "$SRVCERT" + + for (( i=0; i<${#NOTIFY[@]}; i++ )); do + ${NOTIFY[$i]} + done +fi |