#!/bin/sh # letsencrypt.sh - a simple shell implementation for the acme protocol # Copyright (C) 2015 Gerhard Heift # Copyright (C) 2016-2025 Attila Bruncsak # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. #CADIR="https://api.test4.buypass.no/acme/directory" #CADIR="https://acme-staging-v02.api.letsencrypt.org/directory" # Prefix the following line with "# letsencrypt-production-server #", to use # the staging server of letsencrypt. The staging server has lower rate limits, # but does not issue valid certificates. To automatically remove the comment # again on commiting the file, add the filter to your git config by running # git config filter.production-server.clean misc/filter-production-server #CADIR="https://api.buypass.com/acme/directory" #CADIR="https://acme-v02.api.letsencrypt.org/directory" CA_STAGING="https://acme-staging-v02.api.letsencrypt.org/directory" CA_FINAL="https://acme-v02.api.letsencrypt.org/directory" ## The following line defines the default server to use. ## The staging server has lower rate limits, but does not issue valid certificates. ## There is an option "-s" for 'stating' or 'simulating' to use the staging server ## instead. # CADIR=$CA_FINAL # global variables: # base64url encoded JSON nonce, generated from Replay-Nonce header # see gen_protected() PROTECTED= # base64url encoded JSON request object PAYLOAD= # base64url encoded signature of PROTECTED and PAYLOAD # see also gen_signature() SIGNATURE= # the account key used to send the requests and to verify the domain challenges ACCOUNT_KEY= # the JSON Web Key is the representation of the key as JSON object ACCOUNT_JWK= # the JSON object to specify the signature format ACCOUNT_ID= # the thumbprint is the checksum of the JWK and is used for the challenges ACCOUNT_THUMB= # the private key, which should be signed by the CA SERVER_KEY= # the certificate signing request, which sould be used SERVER_CSR= # the location, where the certificate should be stored SERVER_CERT= # the location, where the certificate with the signing certificate(s) should be stored SERVER_FULL_CHAIN= # the location, where the signing certificate(s) should be stored SERVER_SIGNING_CHAIN= # selection of the signing chain SIGNING_CHAIN_SELECTION=0 # the e-mail address to be used with the account key, only needed if account # key is not yet registred ACCOUNT_EMAIL= # a list of domains, which should be assigned to the certificate DOMAINS= # a list of domains, challenge uri, token and authorization uri DOMAIN_DATA= # the directory, where to push the response # $DOMAIN or ${DOMAIN} will be replaced with the actual domain WEBDIR= # the script to be called to push the response to a remote server PUSH_TOKEN= # the script to be called to push the response to a remote server needs the commit feature PUSH_TOKEN_COMMIT= # set the option of the preferred IP family for connecting to the ACME server IPV_OPTION= # the challenge type, can be dns-01 or http-01 (default) CHALLENGE_TYPE="http-01" # the date of the that version VERSION_DATE="2025-12-01" # The meaningful User-Agent to help finding related log entries in the ACME server log USER_AGENT="bruncsak/ght-acme.sh $VERSION_DATE" LOGLEVEL=1 # utility functions tolower() { printf '%s' "$*" | tr A-Z a-z } # SDuesterhaupt: 2026-02-22 - Determine whether echo understands -e # # default: na echo 'x\040x' | grep -E -s -q 'x x' && ECHOESCFLAG='' || ECHOESCFLAG='-e' HexadecimalStringToOctalEscapeSequence() { tr '[A-F]' '[a-f]' "$@" | tr -d '\r\n' | sed -e 's/[^0-9a-f]//g; s/^\(\(..\)\{0,\}\).$/\1/; s/\([0-9a-f]\)\([0-9a-f]\)/\1_\2/g; s/$/\\c/; s/_0/o0/g; s/_1/o1/g; s/_2/o2/g; s/_3/o3/g; s/_4/o4/g; s/_5/o5/g; s/_6/o6/g; s/_7/o7/g; s/_8/i0/g; s/_9/i1/g; s/_a/i2/g; s/_b/i3/g; s/_c/i4/g; s/_d/i5/g; s/_e/i6/g; s/_f/i7/g; s/0o/\\000/g; s/0i/\\001/g; s/1o/\\002/g; s/1i/\\003/g; s/2o/\\004/g; s/2i/\\005/g; s/3o/\\006/g; s/3i/\\007/g; s/4o/\\010/g; s/4i/\\011/g; s/5o/\\012/g; s/5i/\\013/g; s/6o/\\014/g; s/6i/\\015/g; s/7o/\\016/g; s/7i/\\017/g; s/8o/\\020/g; s/8i/\\021/g; s/9o/\\022/g; s/9i/\\023/g; s/ao/\\024/g; s/ai/\\025/g; s/bo/\\026/g; s/bi/\\027/g; s/co/\\030/g; s/ci/\\031/g; s/do/\\032/g; s/di/\\033/g; s/eo/\\034/g; s/ei/\\035/g; s/fo/\\036/g; s/fi/\\037/g; ' } hex2bin() { # SDuesterhaupt: 2026-02-22 - No xxd required # # default: xxd -r -p #xxd -r -p echo $ECHOESCFLAG "`HexadecimalStringToOctalEscapeSequence`" } base64url() { openssl base64 | tr '+/' '-_' | tr -d '\r\n=' } log() { if [ "$LOGLEVEL" -gt 0 ]; then echo "$@" >& 2 fi } dbgmsg() { if [ "$LOGLEVEL" -gt 1 ]; then echo "$@" >& 2 fi } errmsg() { echo "$@" >& 2 } err_exit() { RETCODE="$?" [ -n "$2" ] && RETCODE="$2" [ -n "$1" ] && printf "%s\n" "$1" >& 2 exit "$RETCODE" } required_commands() { # SDuesterhaupt: 2026-02-22 - No egrep/xxd required # # default: REQUIRED_COMMANDS="basename cat cp rm sed grep egrep fgrep tr mktemp expr tail xxd openssl" REQUIRED_COMMANDS="basename cat cp rm sed grep fgrep tr mktemp expr tail openssl" if [ "$USE_WGET" = yes ] ;then REQUIRED_COMMANDS="$REQUIRED_COMMANDS wget" else REQUIRED_COMMANDS="$REQUIRED_COMMANDS curl" fi for command in $REQUIRED_COMMANDS ;do command -v $command > /dev/null || err_exit "The command '$command' is required to run $PROGNAME" done } validate_domain() { DOMAIN_IN="$1" if [ "$DOMAIN_IN" = _ ]; then return 1 fi DOMAIN_OUT="`printf "%s\n" "$DOMAIN_IN" | sed -e 's/^...$/!/; s/^.\{254,\}$/!/; s/^'"$DOMAIN_EXTRA_PAT"'\([a-zA-Z0-9]\([-a-zA-Z0-9]\{0,61\}[a-zA-Z0-9]\)\{0,1\}\.\)\{1,\}\([a-zA-Z]\([-a-zA-Z0-9]\{0,61\}[a-zA-Z]\)\)$/_/;'`" if [ "$DOMAIN_OUT" = _ ]; then return 0 else return 1 fi } handle_wget_exit() { WGET_EXIT="$1" WGET_URI="$2" if [ "$WGET_EXIT" -ne 0 -a "$WGET_EXIT" -ne 8 -o -s "$WGET_OUT" ]; then echo "error while making a web request to \"$WGET_URI\"" >& 2 echo "wget exit status: $WGET_EXIT" >& 2 case "$WGET_EXIT" in # see man wget "EXIT STATUS" 4) echo " Network failure" >& 2;; 5) echo " SSL verification failure" >& 2;; 8) echo " Server issued an error response" >& 2;; esac cat "$WGET_OUT" >& 2 cat "$RESP_HEABOD" >& 2 exit 1 elif [ "$WGET_EXIT" -eq 8 -a ! -s "$RESP_HEABOD" ] ;then echo "error while making a web request to \"$WGET_URI\"" >& 2 echo "wget exit status: $WGET_EXIT" >& 2 err_exit "Server issued an error response and no error document returned and no --content-on-error flag available. Upgrade your wget or use curl instead." 1 fi tr -d '\r' < "$RESP_HEABOD" | sed -e '/^$/,$d' > "$RESP_HEADER" tr -d '\r' < "$RESP_HEABOD" | sed -e '1,/^$/d' > "$RESP_BODY" } curl_return_text() { CURL_EXIT="$1" case "$CURL_EXIT" in # see man curl "EXIT CODES" 3) TXT=", malformed URI" ;; 6) TXT=", could not resolve host" ;; 7) TXT=", failed to connect" ;; 28) TXT=", operation timeout" ;; 35) TXT=", SSL connect error" ;; 52) TXT=", the server did not reply anything" ;; 56) TXT=", failure in receiving network data" ;; *) TXT="" ;; esac printf "curl return status: %d%s" "$1" "$TXT" } curl_loop() { CURL_ACTION="$1"; shift loop_count=0 pluriel="" while : ;do dbgmsg "About making a web request to \"$CURL_ACTION\"" curl "$@" CURL_RETURN_CODE="$?" [ "$loop_count" -ge 20 ] && break case "$CURL_RETURN_CODE" in 6) ;; 7) ;; 28) ;; 35) ;; 52) ;; 56) ;; *) break ;; esac loop_count=$((loop_count + 1 )) dbgmsg "While making a web request to \"$CURL_ACTION\" sleeping $loop_count second$pluriel before retry due to `curl_return_text $CURL_RETURN_CODE`" sleep "$loop_count" pluriel="s" done if [ "$CURL_RETURN_CODE" -ne 0 ] ;then errmsg "While making a web request to \"$CURL_ACTION\" error exiting due to `curl_return_text $CURL_RETURN_CODE` (retry number: $loop_count)" exit "$CURL_RETURN_CODE" else dbgmsg "While making a web request to \"$CURL_ACTION\" continuing due to `curl_return_text $CURL_RETURN_CODE`" fi } handle_openssl_exit() { OPENSSL_EXIT=$1 OPENSSL_ACTION=$2 if [ "$OPENSSL_EXIT" "!=" 0 ]; then echo "error while $OPENSSL_ACTION" >& 2 echo "openssl exit status: $OPENSSL_EXIT" >& 2 cat "$OPENSSL_ERR" >& 2 exit 1 fi } fetch_http_status() { HTTP_STATUS="`sed -e '/^HTTP\// !d; s/^HTTP\/[0-9.]\{1,\} *\([^ ]*\).*$/\1/' "$RESP_HEADER" | tail -n 1`" } check_http_status() { [ "$HTTP_STATUS" = "$1" ] } check_acme_error() { fgrep -q "urn:ietf:params:acme:error:$1" "$RESP_BODY" } unhandled_response() { echo "unhandled response while $1" >& 2 echo >& 2 cat "$RESP_HEADER" "$RESP_BODY" >& 2 echo >& 2 exit 1 } show_error() { if [ -n "$1" ]; then echo "error while $1" >& 2 fi ERR_TYPE="`tr -d '\r\n' < "$RESP_BODY" | sed -e 's/.*"type": *"\([^"]*\)".*/\1/'`" ERR_DETAILS="`tr -d '\r\n' < "$RESP_BODY" | sed -e 's/.*"detail": *"\([^"]*\)".*/\1/'`" echo " $ERR_DETAILS ($ERR_TYPE)" >& 2 } header_field_value() { grep -i -e "^$1:.*$2" "$RESP_HEADER" | sed -e 's/^[^:]*: *//' | tr -d '\r\n' } fetch_next_link() { header_field_value Link ';rel="next"' | sed -s 's/^.*<\(.*\)>.*$/\1/' } fetch_alternate_link() { header_field_value Link ';rel="alternate"' | sed -s 's/^.*<\(.*\)>.*$/\1/' } fetch_location() { header_field_value Location } # retrieve the nonce from the response header of the actual request for the forthcomming POST request extract_nonce() { new_nonce="`header_field_value Replay-Nonce`" if [ -n "$new_nonce" ] ;then # Log if we had unnecesseraily multiple nonces, but use always the latest nonce [ -n "$NONCE" ] && log "droping unused nonce: $NONCE" NONCE="$new_nonce" dbgmsg " new nonce: $NONCE" else dbgmsg "no new nonce" fi } retry_after() { header_field_value Retry-After } sleep_retryafter() { RETRY_AFTER="`retry_after`" if printf '%s' "$RETRY_AFTER" | grep -E -s -q '^[1-9][0-9]*$' ;then if [ "$RETRY_AFTER" -gt 61 ] ;then log "Too big Retry-After header field value: $RETRY_AFTER" RETRY_AFTER=61 fi [ "$RETRY_AFTER" -eq 1 ] && pluriel="" || pluriel="s" log "sleeping $RETRY_AFTER second$pluriel" sleep $RETRY_AFTER else log "Could not retrieve expected Retry-After header field value: $RETRY_AFTER" sleep 1 fi } server_overload() { if check_http_status 503 && check_acme_error rateLimited ;then log "busy server rate limit condition" sleep_retryafter return 0 else return 1 fi } server_request() { dbgmsg "server_request: $1 $2" if [ "$USE_WGET" != yes ] ;then if [ -n "$2" ] ;then curl_loop "$1" $CURLEXTRAFLAG -s $IPV_OPTION -A "$USER_AGENT" -D "$RESP_HEADER" -o "$RESP_BODY" -H "Content-type: application/jose+json" -d "$2" "$1" else curl_loop "$1" $CURLEXTRAFLAG -s $IPV_OPTION -A "$USER_AGENT" -D "$RESP_HEADER" -o "$RESP_BODY" "$1" fi else if [ -n "$2" ] ;then wget $WGETEXTRAFLAG -q $IPV_OPTION -U "$USER_AGENT" --retry-connrefused --save-headers $WGETCOEFLAG -O "$RESP_HEABOD" --header="Content-type: application/jose+json" --post-data="$2" "$1" > "$WGET_OUT" 2>& 1 else wget $WGETEXTRAFLAG -q $IPV_OPTION -U "$USER_AGENT" --retry-connrefused --save-headers $WGETCOEFLAG -O "$RESP_HEABOD" "$1" > "$WGET_OUT" 2>& 1 fi handle_wget_exit $? "$1" fi fetch_http_status } request_acme_server() { while : ;do server_request "$1" "$2" extract_nonce server_overload || return done } # generate the PROTECTED variable, which contains a nonce retrieved from the # server in the Replay-Nonce header gen_protected(){ if [ -z "$NONCE" ]; then dbgmsg "fetch new nonce" send_get_req "$NEWNONCEURL" [ -n "$NONCE" ] || err_exit "could not fetch new nonce" fi printf '%s' '{"alg":"RS256",'"$ACCOUNT_ID"',"nonce":"'"$NONCE"'","url":"'"$1"'"}' } # generate the signature for the request gen_signature() { printf '%s' "$1" | openssl dgst -sha256 -binary -sign "$ACCOUNT_KEY" 2> "$OPENSSL_ERR" handle_openssl_exit "$?" "signing request" } # helper functions to create the json web key object key_get_modulus(){ openssl rsa -in "$1" -modulus -noout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" handle_openssl_exit $? "extracting account key modulus" sed -e 's/^Modulus=//' < "$OPENSSL_OUT" \ | hex2bin \ | base64url } key_get_exponent(){ openssl rsa -in "$1" -text -noout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" handle_openssl_exit $? "extracting account key exponent" sed -e '/^publicExponent: / !d; s/^publicExponent: [0-9]* \{1,\}(\(.*\)).*$/\1/;s/^0x\([0-9a-fA-F]\)\(\([0-9a-fA-F][0-9a-fA-F]\)*\)$/0x0\1\2/;s/^0x\(\([0-9a-fA-F][0-9a-fA-F]\)*\)$/\1/' \ < "$OPENSSL_OUT" \ | hex2bin \ | base64url } # make a request to the specified URI # the payload is signed by the ACCOUNT_KEY # the response header is stored in the file $RESP_HEADER, the body in the file $RESP_BODY send_req_no_kid(){ URI="$1" PAYLOAD="`printf '%s' "$2" | base64url`" while : ;do PROTECTED="`gen_protected "$URI" | base64url`" SIGNATURE="`gen_signature $PROTECTED.$PAYLOAD | base64url`" DATA='{"protected":"'"$PROTECTED"'","payload":"'"$PAYLOAD"'","signature":"'"$SIGNATURE"'"}' # Use only once a nonce NONCE="" request_acme_server "$URI" "$DATA" if ! check_http_status 400; then return elif ! check_acme_error badNonce ;then return fi if [ -z "$BAD_NONCE_MSG" ] ;then BAD_NONCE_MSG=yes echo "badNonce warning: other than extrem load on the ACME server," >& 2 echo "this is mostly due to multiple client egress IP addresses," >& 2 echo "including working IPv4 and IPv6 addresses on dual family systems." >& 2 echo "In that case as a workaround please try to restrict the egress" >& 2 echo "IP address with the -4 or -6 command line option on the script." >& 2 echo "This message is just a warning, continuing safely." >& 2 fi # Bad nonce condition. Here we do not sleep to be nice, just loop immediately. # The error cannot be on the client side, since it is guaranted that we used the latest available nonce. done } send_req(){ URI="$1" [ -z "$KID" ] && register_account_key retrieve_kid send_req_no_kid "$1" "$2" } send_get_req(){ request_acme_server "$1" } pwncheck(){ [ "$PWNEDKEY_CHECK" = yes ] || return 0 server_request "https://v1.pwnedkeys.com/$1" if check_http_status 404; then log "pwnedkeys.com claims: $2 is not compromised" return 0 elif check_http_status 200; then echo "pwnedkeys.com claims: $2 is compromised, fingerprint: $1" >& 2 return 1 else log "pwncheck unavailable (HTTP $HTTP_STATUS), skipping check" return 0 fi #unhandled_response "pwncheck" } pkey_hex_digest(){ openssl dgst -sha256 -hex "$1" > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" handle_openssl_exit $? "public key DER hexdigest" sed -e 's/^.*= *//' "$OPENSSL_OUT" } pwnedkey_req_check(){ [ "$PWNEDKEY_CHECK" = no ] && return openssl req -in "$1" -noout -pubkey > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" handle_openssl_exit $? "extracting request public key" cp "$OPENSSL_OUT" "$OPENSSL_IN" if ! openssl pkey -in "$OPENSSL_IN" -pubin -outform der -pubout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" ;then # On old openssl there is no EC key. There we default to RSA. openssl rsa -in "$OPENSSL_IN" -pubin -outform der -pubout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" fi handle_openssl_exit $? "request public key to DER" cp "$OPENSSL_OUT" "$OPENSSL_IN" pwncheck "`pkey_hex_digest "$OPENSSL_IN"`" "$2" } pwnedkey_key_check(){ [ "$PWNEDKEY_CHECK" = no ] && return if ! openssl ec -in "$1" -outform der -pubout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" ;then openssl rsa -in "$1" -outform der -pubout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" fi handle_openssl_exit $? "public key to DER" cp "$OPENSSL_OUT" "$OPENSSL_IN" pwncheck "`pkey_hex_digest "$OPENSSL_IN"`" "$2" } # account key handling load_account_key(){ [ -n "$ACCOUNT_KEY" ] || err_exit "no account key specified" [ -r "$ACCOUNT_KEY" ] || err_exit "could not read account key" openssl rsa -in "$ACCOUNT_KEY" -noout > "$OPENSSL_OUT" 2> "$OPENSSL_ERR" handle_openssl_exit $? "opening account key" ACCOUNT_JWK='{"e":"'"`key_get_exponent $ACCOUNT_KEY`"'","kty":"RSA","n":"'"`key_get_modulus $ACCOUNT_KEY`"'"}' ACCOUNT_ID='"jwk":'"$ACCOUNT_JWK" ACCOUNT_THUMB="`printf '%s' "$ACCOUNT_JWK" | openssl dgst -sha256 -binary | base64url`" if [ -z "$1" ] ;then if [ "$ACCOUNT_KEY" = "$SERVER_KEY" ] ;then # We should allow revoking with compromised certificate key too pwnedkey_key_check "$ACCOUNT_KEY" "server key as account key" || log "revoking certificate with compromised key" else pwnedkey_key_check "$ACCOUNT_KEY" "account key" || exit fi fi } get_one_url(){ if ! grep -E -s -q '"'"$1"'"' "$RESP_BODY" ;then cat "$RESP_BODY" >& 2 err_exit "Cannot retrieve URL for $1 ACME protocol function from the directory $CADIR" 1 fi tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*"'"$1"'":"\([^"]*\)".*/\1/' } get_urls(){ if [ "$USE_WGET" = yes ] ;then WGETCOEFLAG='--content-on-error' wget --help | grep -E -s -q "$WGETCOEFLAG" || WGETCOEFLAG='' fi send_get_req "$CADIR" if ! check_http_status 200 ;then unhandled_response "fetching directory URLs" fi NEWACCOUNTURL="`get_one_url newAccount`" REVOKECERTURL="`get_one_url revokeCert`" KEYCHANGEURL="`get_one_url keyChange`" NEWNONCEURL="`get_one_url newNonce`" NEWORDERURL="`get_one_url newOrder`" } orders_url() { tr -d ' \r\n' < "$RESP_BODY" | sed -e '/"orders":"/ !d; s/.*"orders":"\([^"]*\)".*/\1/' } orders_list() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/^.*"orders":\[\([^]]*\)\].*$/\1/' | tr -d '"' | tr ',' ' ' } register_account_key(){ [ -n "$NEWACCOUNTURL" ] || get_urls if [ -n "$ACCOUNT_EMAIL" ] ;then NEW_REG='{"termsOfServiceAgreed":true,"contact":["mailto:'"$ACCOUNT_EMAIL"'"]}' else NEW_REG='{"onlyReturnExisting":true}' fi send_req_no_kid "$NEWACCOUNTURL" "$NEW_REG" if check_http_status 200; then [ "$1" = "retrieve_kid" ] || err_exit "account already registered" KID="`fetch_location`" ACCOUNT_ID='"kid":"'"$KID"'"' ORDERS_URL="`orders_url`" return elif check_http_status 201; then KID="`fetch_location`" ACCOUNT_ID='"kid":"'"$KID"'"' ORDERS_URL="`orders_url`" return elif check_http_status 409; then [ "$1" = "nodie" ] || err_exit "account already exists" elif check_http_status 400 && check_acme_error accountDoesNotExist ;then show_error "fetching account information" exit 1 else unhandled_response "registering account" fi } clrpenda() { ORDERS_LIST="" while [ -n "$ORDERS_URL" ]; do send_req "$ORDERS_URL" "" if check_http_status 200; then ORDERS_LIST="$ORDERS_LIST `orders_list`" else unhandled_response "retrieving orders list" fi ORDERS_URL="`fetch_next_link`" done DOMAIN_AUTHZ_LIST="" set -- $ORDERS_LIST for ORDER do send_req "$ORDER" "" if check_http_status 200; then ORDER_STATUS="`order_status`" if [ "$ORDER_STATUS" = pending ] ;then DOMAIN_AUTHZ_LIST="$DOMAIN_AUTHZ_LIST `domain_authz_list`" fi else unhandled_response "retrieving order" fi done # All domain should have that challenge type, even wildcard one CHALLENGE_TYPE=dns-01 set -- $DOMAIN_AUTHZ_LIST for DOMAIN_AUTHZ do send_req "$DOMAIN_AUTHZ" "" if check_http_status 200; then DOMAIN="`authz_domain`" AUTHZ_STATUS="`authz_status`" if [ "$AUTHZ_STATUS" = pending ] ;then DOMAIN_URI="`authz_domain_uri`" log "retrieve challenge for $DOMAIN" request_domain_verification fi else unhandled_response "retrieve challenge for URL: $DOMAIN_AUTHZ" fi done } delete_account_key(){ log "delete account" REG='{"resource":"reg","delete":"true"}' send_req "$REGISTRATION_URI" "$REG" if check_http_status 200; then return else unhandled_response "deleting account" fi } check_server_domain() { if [ "$2" = true ] ;then SERVER_DOMAIN="*.$1" else SERVER_DOMAIN="$1" fi SERVER_DOMAIN_LOWER="`tolower $SERVER_DOMAIN`" set -- $DOMAINS for REQ_DOMAIN do if [ "$SERVER_DOMAIN_LOWER" = "`tolower $REQ_DOMAIN`" ] ;then return fi done err_exit "ACME server requested authorization for a rogue domain: $SERVER_DOMAIN" 1 } authz_status() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*"status":"\([^"]*\)".*/\1/' } authz_domain() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*"identifier":{"type":"dns","value":"\([^"]*\)"}.*/\1/' } wildcard_domain() { tr -d ' \r\n' < "$RESP_BODY" | sed -e '/"wildcard":/ !d; s/^.*"wildcard":\([a-z]*\).*$/\1/' } authz_domain_token() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*{\([^}]*"type":"'"$CHALLENGE_TYPE"'"[^}]*\)}.*/\1/; s/.*"token":"\([^"]*\)".*/\1/' } authz_domain_uri() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*{\([^}]*"type":"'"$CHALLENGE_TYPE"'"[^}]*\)}.*/\1/; s/.*"url":"\([^"]*\)".*/\1/' } request_challenge_domain(){ send_req "$DOMAIN_AUTHZ" "" if check_http_status 200; then DOMAIN="`authz_domain`" AUTHZ_STATUS="`authz_status`" case "$AUTHZ_STATUS" in valid) log "authorization is valid for $DOMAIN" ;; pending) check_server_domain "$DOMAIN" "`wildcard_domain`" DOMAIN_TOKEN="`authz_domain_token`" DOMAIN_URI="`authz_domain_uri`" DOMAIN_DATA="$DOMAIN_DATA $DOMAIN $DOMAIN_URI $DOMAIN_TOKEN $DOMAIN_AUTHZ" log "retrieve challenge for $DOMAIN" ;; *) echo authorization status: "$AUTHZ_STATUS" >& 2 unhandled_response "checking authorization status for domain $DOMAIN" ;; esac elif check_http_status 400; then # account not registred? show_error "retrieve challenge for URL: $DOMAIN_AUTHZ" exit 1 elif check_http_status 403; then # account not registred? show_error "retrieve challenge for URL: $DOMAIN_AUTHZ" exit 1 else unhandled_response "retrieve challenge for URL: $DOMAIN_AUTHZ" fi } domain_authz_list() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/^.*"authorizations":\[\([^]]*\)\].*$/\1/' | tr -d '"' | tr ',' ' ' } finalize() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/^.*"finalize":"\([^"]*\).*$/\1/' } request_challenge(){ log "creating new order" set -- $DOMAINS for DOMAIN do [ -n "$DOMAIN_ORDERS" ] && DOMAIN_ORDERS="$DOMAIN_ORDERS," DOMAIN_ORDERS="$DOMAIN_ORDERS"'{"type":"dns","value":"'"$DOMAIN"'"}' done [ -n "$NEWORDERURL" ] || get_urls NEW_ORDER='{"identifiers":['"$DOMAIN_ORDERS"']}' send_req "$NEWORDERURL" "$NEW_ORDER" if check_http_status 201; then DOMAIN_AUTHZ_LIST="`domain_authz_list`" FINALIZE="`finalize`" CURRENT_ORDER="`fetch_location`" else unhandled_response "requesting new order for $DOMAINS" fi set -- $DOMAIN_AUTHZ_LIST for DOMAIN_AUTHZ do request_challenge_domain done } domain_commit() { if [ -n "$PUSH_TOKEN" ] && [ -n "$PUSH_TOKEN_COMMIT" ]; then log "calling $PUSH_TOKEN commit" $PUSH_TOKEN commit || err_exit "$PUSH_TOKEN could not commit" # We cannot know how long the execution of an external command will take. # Safer to force fetching a new nonce to avoid fatal badNonce error due to nonce validity timeout. NONCE="" fi } domain_dns_challenge() { DNS_CHALLENGE="`printf '%s' "$DOMAIN_TOKEN.$ACCOUNT_THUMB" | openssl dgst -sha256 -binary | base64url`" if [ -n "$PUSH_TOKEN" ]; then $PUSH_TOKEN "$1" _acme-challenge."$DOMAIN" "$DNS_CHALLENGE" || err_exit "Could not $1 $CHALLENGE_TYPE type challenge token with value $DNS_CHALLENGE for domain $DOMAIN via $PUSH_TOKEN" else # SDuesterhaupt: 2019-12-19 - 'nsupdate' without further options can run only on the dns directly # External accesses are refused generally. # # Additional options: File with TSIG key (DNS_TSIG) # DNS server (DNS_SERVER) # Zone which shall be updated (DNS_ZONE) #printf 'update %s _acme-challenge.%s. 300 IN TXT "%s"\n\n' "$1" "$DOMAIN" "$DNS_CHALLENGE" | # nsupdate || err_exit "Could not $1 $CHALLENGE_TYPE type challenge token with value $DNS_CHALLENGE for domain $DOMAIN via nsupdate" # local Bind/nsupdate integration (adapt paths/names as needed) DNS_ZONE=${DNS_ZONE:-"example.com."} DNS_SERVER=${DNS_SERVER:-"127.0.0.1"} DNS_TSIG=${DNS_TSIG:-"/etc/named.tsig"} # build nsupdate script MyDNSChallengeContent="${MyDNSChallengeContent}server ${DNS_SERVER}\n" #MyDNSChallengeContent="${MyDNSChallengeContent}debug yes\n" MyDNSChallengeContent="${MyDNSChallengeContent}zone ${DNS_ZONE}\n" # Change DNSSEC from 'secure' to 'insecure' #MyDNSChallengeContent="${MyDNSChallengeContent}prereq yxrrset $DNS_ZONE. DNS_KEY\n" #MyDNSChallengeContent="${MyDNSChallengeContent}; Move $DNS_ZONE.zone from secure to insecure temporarily\n" #MyDNSChallengeContent="${MyDNSChallengeContent}update delete $DNS_ZONE. DNS_KEY\nsend\n" case "$1" in add) MyDNSChallengeContent="${MyDNSChallengeContent}update add _acme-challenge.${DOMAIN}. 300 IN TXT \"${DNS_CHALLENGE}\"\n" ;; delete) MyDNSChallengeContent="${MyDNSChallengeContent}update delete _acme-challenge.${DOMAIN}. TXT\n" ;; *) err_exit "Unsupported dns-01 action '$1'" ;; esac #MyDNSChallengeContent="${MyDNSChallengeContent}show\n" MyDNSChallengeContent="${MyDNSChallengeContent}send\n" # DNSSEC freeze/thaw and zone reload if command -v rndc >/dev/null 2>&1; then rndc -s "$DNS_SERVER" -k "$DNS_TSIG" freeze "$DNS_ZONE" >/dev/null 2>&1 || true fi printf "%b" "${MyDNSChallengeContent}" > nsupdate.txt nsupdate -k "$DNS_TSIG" nsupdate.txt \ || err_exit "Could not $1 $CHALLENGE_TYPE type challenge token with value '$DNS_CHALLENGE' for domain '$DOMAIN' via nsupdate" if command -v rndc >/dev/null 2>&1; then rndc -s "$DNS_SERVER" -k "$DNS_TSIG" thaw "$DNS_ZONE" >/dev/null 2>&1 || true fi rm -f nsupdate.txt fi } push_domain_response() { log "push response for $DOMAIN" # do something with DOMAIN, DOMAIN_TOKEN and DOMAIN_RESPONSE # echo "$DOMAIN_RESPONSE" > "/writeable/location/$DOMAIN/$DOMAIN_TOKEN" if [ "$CHALLENGE_TYPE" = "http-01" ]; then if [ -n "$WEBDIR" ]; then TOKEN_DIR="`printf "%s" $WEBDIR | sed -e 's/\$DOMAIN/'"$DOMAIN"'/g; s/${DOMAIN}/'"$DOMAIN"'/g'`" SAVED_UMASK="`umask`" umask 0022 printf "%s\n" "$DOMAIN_TOKEN.$ACCOUNT_THUMB" > "$TOKEN_DIR/$DOMAIN_TOKEN" || exit 1 umask "$SAVED_UMASK" # Adjust token owner/group and permissions if command -v stat >/dev/null 2>&1; then DYNTLS_HTTP_TOKEN_USER=$(stat -c "%U" "$TOKEN_DIR" 2>/dev/null || echo "") DYNTLS_HTTP_TOKEN_GROUP=$(stat -c "%G" "$TOKEN_DIR" 2>/dev/null || echo "") if [ -n "$DYNTLS_HTTP_TOKEN_USER" ] && [ -n "$DYNTLS_HTTP_TOKEN_GROUP" ]; then chown "$DYNTLS_HTTP_TOKEN_USER:$DYNTLS_HTTP_TOKEN_GROUP" "$TOKEN_DIR/$DOMAIN_TOKEN" 2>/dev/null || true fi chmod 660 "$TOKEN_DIR/$DOMAIN_TOKEN" 2>/dev/null || true fi elif [ -n "$PUSH_TOKEN" ]; then $PUSH_TOKEN install "$DOMAIN" "$DOMAIN_TOKEN" "$ACCOUNT_THUMB" || err_exit "could not install token for $DOMAIN" fi elif [ "$CHALLENGE_TYPE" = "dns-01" ]; then domain_dns_challenge "add" else echo "unsupported challenge type for install token: $CHALLENGE_TYPE" >& 2; exit 1 fi return } remove_domain_response() { log "remove response for $DOMAIN" # do something with DOMAIN and DOMAIN_TOKEN # rm "/writeable/location/$DOMAIN/$DOMAIN_TOKEN" if [ "$CHALLENGE_TYPE" = "http-01" ]; then if [ -n "$WEBDIR" ]; then TOKEN_DIR="`printf "%s" $WEBDIR | sed -e 's/\$DOMAIN/'"$DOMAIN"'/g; s/${DOMAIN}/'"$DOMAIN"'/g'`" rm -f "$TOKEN_DIR/$DOMAIN_TOKEN" elif [ -n "$PUSH_TOKEN" ]; then $PUSH_TOKEN remove "$DOMAIN" "$DOMAIN_TOKEN" "$ACCOUNT_THUMB" || exit 1 fi elif [ "$CHALLENGE_TYPE" = "dns-01" ]; then domain_dns_challenge "delete" else echo "unsupported challenge type for remove token: $CHALLENGE_TYPE" >& 2; exit 1 fi return } push_response() { set -- $DOMAIN_DATA while [ -n "$1" ]; do DOMAIN="$1" DOMAIN_URI="$2" DOMAIN_TOKEN="$3" DOMAIN_AUTHZ="$4" shift 4 push_domain_response done domain_commit } request_domain_verification() { log request verification of $DOMAIN send_req $DOMAIN_URI '{}' dbgmsg "Retry-After value in request_domain_verification: `retry_after`" if check_http_status 200; then return else unhandled_response "requesting verification of challenge of $DOMAIN" fi } request_verification() { set -- $DOMAIN_DATA while [ -n "$1" ]; do DOMAIN="$1" DOMAIN_URI="$2" DOMAIN_TOKEN="$3" DOMAIN_AUTHZ="$4" shift 4 request_domain_verification done } domain_status() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*"type":"'"$CHALLENGE_TYPE"'",[^{}]*"status":"\([^"]*\)".*/\1/' } check_verification() { ALL_VALID=true while [ -n "$DOMAIN_DATA" ]; do sleep 1 set -- $DOMAIN_DATA DOMAIN_DATA="" while [ -n "$1" ]; do DOMAIN="$1" DOMAIN_URI="$2" DOMAIN_TOKEN="$3" DOMAIN_AUTHZ="$4" shift 4 log check verification of $DOMAIN send_req "$DOMAIN_AUTHZ" "" dbgmsg "Retry-After value in check_verification: `retry_after`" if check_http_status 200; then DOMAIN_STATUS="`domain_status`" case "$DOMAIN_STATUS" in valid) log $DOMAIN is valid remove_domain_response ;; invalid) echo $DOMAIN: invalid >& 2 show_error remove_domain_response ALL_VALID=false ;; pending) log $DOMAIN is pending DOMAIN_DATA="$DOMAIN_DATA $DOMAIN $DOMAIN_URI $DOMAIN_TOKEN $DOMAIN_AUTHZ" ;; *) unhandled_response "checking verification status of $DOMAIN: $DOMAIN_STATUS" ;; esac else unhandled_response "checking verification status of $DOMAIN" fi done done domain_commit $ALL_VALID || exit 1 log checking order while : ;do send_req "$CURRENT_ORDER" "" if check_http_status 200; then ORDER_STATUS="`order_status`" case "$ORDER_STATUS" in ready) log order is ready break ;; pending) echo order: "$ORDER_STATUS" >& 2 sleep 1 continue ;; *) unhandled_response "checking verification status of order" ;; esac else unhandled_response "requesting order status verification" fi done } # this function generates the csr from the private server key and list of domains gen_csr_with_private_key() { log generate certificate request set -- $DOMAINS FIRST_DOM="$1" validate_domain "$FIRST_DOM" || err_exit "invalid domain: $FIRST_DOM" ALT_NAME="subjectAltName=DNS:$1" shift for DOMAIN do validate_domain "$DOMAIN" || err_exit "invalid domain: $DOMAIN" ALT_NAME="$ALT_NAME,DNS:$DOMAIN" done if [ -r /etc/ssl/openssl.cnf ]; then cat /etc/ssl/openssl.cnf > "$OPENSSL_CONFIG" else cat /etc/pki/tls/openssl.cnf > "$OPENSSL_CONFIG" fi echo '[SAN]' >> "$OPENSSL_CONFIG" echo "$ALT_NAME" >> "$OPENSSL_CONFIG" openssl req -new -sha512 -key "$SERVER_KEY" -subj "/CN=$FIRST_DOM" -reqexts SAN -config $OPENSSL_CONFIG \ > "$TMP_SERVER_CSR" \ 2> "$OPENSSL_ERR" handle_openssl_exit $? "creating certificate request" pwnedkey_key_check "$SERVER_KEY" "server key" || exit } subject_domain() { sed -n '/Subject:/ {s/^.*CN=//; s/[,/ ].*$//; p}' "$OPENSSL_OUT" } san_domains() { sed -n '/X509v3 Subject Alternative Name:/ { n; s/^[ ]*DNS[ ]*:[ ]*//; s/[ ]*,[ ]*DNS[ ]*:[ ]*/ /g; p; q; }' "$OPENSSL_OUT" } csr_extract_domains() { log "extract domains from certificate signing request" if echo "$CADIR" | grep -E -i -s -q '\.buypass\.(com|no)/' -e '\.letsencrypt\.org/' ;then # Known ACME servers supporting commonName in the Subject of the CSR Subject_commonName_support=yes else # ACME server(s) do not supporting commonName in the Subject of the CSR # Typically pebble's case, see https://github.com/letsencrypt/pebble/issues/304 Subject_commonName_support=no fi openssl req -in "$TMP_SERVER_CSR" -noout -text \ > "$OPENSSL_OUT" \ 2> "$OPENSSL_ERR" handle_openssl_exit $? "reading certificate signing request" ALTDOMAINS="`san_domains`" SUBJDOMAIN="`subject_domain`" if [ "$Subject_commonName_support" = yes ] ;then DOMAINS="$SUBJDOMAIN $ALTDOMAINS" else DOMAINS="$ALTDOMAINS" fi pwnedkey_req_check "$TMP_SERVER_CSR" "certificate signing request key" || exit } certificate_extract_domains() { log "extract domains from certificate" openssl x509 -in "$SERVER_CERT" -noout -text \ > "$OPENSSL_OUT" \ 2> "$OPENSSL_ERR" handle_openssl_exit $? "reading certificate" DOMAINS="`san_domains`" if [ -z "$DOMAINS" ]; then DOMAINS="`subject_domain`" fi } new_cert() { sed -e 's/-----BEGIN\( NEW\)\{0,1\} CERTIFICATE REQUEST-----/{"csr":"/; s/-----END\( NEW\)\{0,1\} CERTIFICATE REQUEST-----/"}/;s/+/-/g;s!/!_!g;s/=//g' "$TMP_SERVER_CSR" | tr -d ' \t\r\n' } order_status() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*"status":"\([^"]*\)".*/\1/' } certificate_url() { tr -d ' \r\n' < "$RESP_BODY" | sed -e 's/.*"certificate":"\([^"]*\)".*/\1/' } request_certificate(){ log finalize order NEW_CERT="`new_cert`" send_req "$FINALIZE" "$NEW_CERT" while : ;do if check_http_status 200; then ORDER_STATUS="`order_status`" case "$ORDER_STATUS" in valid) log order is valid CERTIFICATE="`certificate_url`" break ;; processing) log order: "$ORDER_STATUS" sleep 1 send_req "$CURRENT_ORDER" "" continue ;; invalid|pending|ready) echo order: "$ORDER_STATUS" >& 2 exit 1 ;; *) unhandled_response "checking finalization status of order" ;; esac else unhandled_response "requesting order finalization" fi done log request certificate CUR_CHAIN=0 while [ -n "$CERTIFICATE" ] ;do send_req "$CERTIFICATE" "" if check_http_status 200; then if [ "$CUR_CHAIN" = "$SIGNING_CHAIN_SELECTION" ] ;then if [ -n "$SERVER_FULL_CHAIN" ] ;then tr -d '\r' < "$RESP_BODY" | sed -e '/^$/d' > "$SERVER_FULL_CHAIN" fi tr -d '\r' < "$RESP_BODY" | sed -e '1,/^-----END CERTIFICATE-----$/ !d' | sed -e '/^$/d' > "$SERVER_CERT" tr -d '\r' < "$RESP_BODY" | sed -e '1,/^-----END CERTIFICATE-----$/d' | sed -e '/^$/d' > "$SERVER_SIGNING_CHAIN" break else CERTIFICATE="`fetch_alternate_link`" CUR_CHAIN="`expr $CUR_CHAIN + 1`" if [ -z "$CERTIFICATE" ] ;then err_exit "No such alternate chain: $SIGNING_CHAIN_SELECTION" 1 fi fi else unhandled_response "retrieveing certificate" fi done } old_cert() { sed -e 's/-----BEGIN CERTIFICATE-----/{"certificate":"/; s/-----END CERTIFICATE-----/"}/;s/+/-/g;s!/!_!g;s/=//g' "$SERVER_CERT" | tr -d ' \t\r\n' } revoke_certificate(){ log revoke certificate [ -n "$REVOKECERTURL" ] || get_urls OLD_CERT="`old_cert`" if [ "$ACCOUNT_KEY" = "$SERVER_KEY" ] ;then send_req_no_kid "$REVOKECERTURL" "$OLD_CERT" if check_http_status 400; then show_error "revoking certificate via server key" exit 1 elif check_http_status 200; then log certificate is revoked via server key exit 0 else unhandled_response "revoking certificate" fi else send_req "$REVOKECERTURL" "$OLD_CERT" if check_http_status 403 || check_http_status 401; then if check_acme_error unauthorized ;then return 1 else unhandled_response "revoking certificate" fi elif check_http_status 400; then show_error "revoking certificate via account key" exit 1 elif check_http_status 200; then log certificate is revoked via account key else unhandled_response "revoking certificate" fi fi } usage() { cat << EOT $PROGNAME register [-p] -a account_key -e email $PROGNAME delete -a account_key $PROGNAME clrpenda -a account_key $PROGNAME accountid -a account_key $PROGNAME thumbprint -a account_key $PROGNAME revoke {-a account_key|-k server_key} -c signed_crt $PROGNAME sign -a account_key -k server_key (chain_options) -c signed_crt domain ... $PROGNAME sign -a account_key -r server_csr (chain_options) -c signed_crt -a account_key the private key -e email the email address assigned to the account key during the registration -k server_key the private key of the server certificate -r server_csr a certificate signing request, which includes the domains, use e.g. gen-csr.sh to create one -c signed_crt the location where to store the signed certificate or retrieve for revocation Options for sign operation: -t selection signing chain selection (number only, default: 0) -s signing_crt the location, where the intermediate signing certificate(s) should be stored default location: {signed_crt}_chain -f full_chain the location, where the signed certificate with the intermediate signing certificate(s) should be stored ACME server options: -D URL ACME server directory URL -4 the connection to the server should use IPv4 -6 the connection to the server should use IPv6 generic flags: -h this help page -q quiet operation -v increase verbosity revoke and sign: -l challenge_type can be http-01 (default) or dns-01 -w webdir the directory, where the response should be stored \$DOMAIN will be replaced by the actual domain the directory will not be created -P exec the command to call to install the token on a remote server -C the command to call to install the token on a remote server needs the commit feature clrpenda: clear pending authorizations for the given account accountid: print account id URI for the given account EOT } # Here starts the program PROGNAME="`basename $0`" required_commands # temporary files to store input/output of curl or openssl trap 'rm -f "$RESP_HEABOD" "$WGET_OUT" "$RESP_HEADER" "$RESP_BODY" "$OPENSSL_CONFIG" "$OPENSSL_IN" "$OPENSSL_OUT" "$OPENSSL_ERR" "$TMP_SERVER_CSR"' 0 1 2 3 13 15 # file to store header and body of http response RESP_HEABOD="`mktemp -t le.$$.resp-heabod.XXXXXX`" # file to store the output of the wget WGET_OUT="`mktemp -t le.$$.resp-out.XXXXXX`" # file to store header of http request RESP_HEADER="`mktemp -t le.$$.resp-header.XXXXXX`" # file to store body of http request RESP_BODY="`mktemp -t le.$$.resp-body.XXXXXX`" # tmp config for openssl for addional domains OPENSSL_CONFIG="`mktemp -t le.$$.openssl.cnf.XXXXXX`" # file to store openssl output OPENSSL_IN="`mktemp -t le.$$.openssl.in.XXXXXX`" OPENSSL_OUT="`mktemp -t le.$$.openssl.out.XXXXXX`" OPENSSL_ERR="`mktemp -t le.$$.openssl.err.XXXXXX`" # file to store the CSR TMP_SERVER_CSR="`mktemp -t le.$$.server.csr.XXXXXX`" echo 'x\0040x' | grep -E -s -q 'x x' && ECHOESCFLAG='' || ECHOESCFLAG='-e' [ $# -gt 0 ] || err_exit "no action given" ACTION="$1" shift SHOW_THUMBPRINT=0 case "$ACTION" in clrpenda|accountid) while getopts :hqvD:46a: name; do case "$name" in h) usage; exit 1;; q) LOGLEVEL=0;; v) LOGLEVEL="`expr $LOGLEVEL + 1`";; D) CADIR="$OPTARG";; 4) IPV_OPTION="-4";; 6) IPV_OPTION="-6";; a) ACCOUNT_KEY="$OPTARG";; ?|:) echo "invalid arguments" >& 2; exit 1;; esac; done;; delete) while getopts :hqvD:46a: name; do case "$name" in h) usage; exit 1;; q) LOGLEVEL=0;; # s) CADIR="$CA_STAGING";; v) LOGLEVEL="`expr $LOGLEVEL + 1`";; D) CADIR="$OPTARG";; 4) IPV_OPTION="-4";; 6) IPV_OPTION="-6";; a) ACCOUNT_KEY="$OPTARG";; ?|:) echo "invalid arguments" >& 2; exit 1;; esac; done;; register) while getopts :hqsvD:46a:e:p name; do case "$name" in h) usage; exit 1;; q) LOGLEVEL=0;; s) CADIR="$CA_STAGING" echo "Use staging server" ;; v) LOGLEVEL="`expr $LOGLEVEL + 1`";; D) CADIR="$OPTARG";; 4) IPV_OPTION="-4";; 6) IPV_OPTION="-6";; p) SHOW_THUMBPRINT=1;; a) ACCOUNT_KEY="$OPTARG";; e) ACCOUNT_EMAIL="$OPTARG";; ?|:) echo "invalid arguments" >& 2; exit 1;; esac; done;; thumbprint) while getopts :hqsva: name; do case "$name" in h) usage; exit 1;; q) LOGLEVEL=0;; s) CADIR="$CA_STAGING" echo "Use staging server" ;; v) LOGLEVEL="`expr $LOGLEVEL + 1`";; a) ACCOUNT_KEY="$OPTARG";; ?|:) echo "invalid arguments" >& 2; exit 1;; esac; done;; revoke) while getopts :hqvD:46Ca:k:c:w:P:l: name; do case "$name" in h) usage; exit 1;; q) LOGLEVEL=0;; # s) CADIR="$CA_STAGING";; v) LOGLEVEL="`expr $LOGLEVEL + 1`";; D) CADIR="$OPTARG";; 4) IPV_OPTION="-4";; 6) IPV_OPTION="-6";; C) PUSH_TOKEN_COMMIT=1;; a) ACCOUNT_KEY="$OPTARG";; k) SERVER_KEY="$OPTARG";; c) SERVER_CERT="$OPTARG";; w) WEBDIR="$OPTARG";; P) PUSH_TOKEN="$OPTARG";; l) CHALLENGE_TYPE="$OPTARG";; ?|:) echo "invalid arguments" >& 2; exit 1;; esac; done;; sign) while getopts :hqsvD:46Ca:k:r:f:s:c:w:P:l:t: name; do case "$name" in h) usage; exit 1;; q) LOGLEVEL=0;; s) CADIR="$CA_STAGING" echo "Use staging server" ;; v) LOGLEVEL="`expr $LOGLEVEL + 1`";; D) CADIR="$OPTARG";; 4) IPV_OPTION="-4";; 6) IPV_OPTION="-6";; C) PUSH_TOKEN_COMMIT=1;; a) ACCOUNT_KEY="$OPTARG";; k) if [ -n "$SERVER_CSR" ]; then echo "server key and server certificate signing request are mutual exclusive" >& 2 exit 1 fi SERVER_KEY="$OPTARG" ACTION=sign-key ;; r) if [ -n "$SERVER_KEY" ]; then echo "server key and server certificate signing request are mutual exclusive" >& 2 exit 1 fi SERVER_CSR="$OPTARG" ACTION=sign-csr ;; dns-server|d) DNS_SERVER="$OPTARG" ;; dns-tsig|t) DNS_TSIG="$OPTARG" ;; dns-zone|z) DNS_ZONE="$OPTARG" ;; f) SERVER_FULL_CHAIN="$OPTARG";; s) SERVER_SIGNING_CHAIN="$OPTARG";; c) SERVER_CERT="$OPTARG";; w) WEBDIR="$OPTARG";; P) PUSH_TOKEN="$OPTARG";; l) CHALLENGE_TYPE="$OPTARG";; t) SIGNING_CHAIN_SELECTION="$OPTARG";; ?|:) echo "invalid arguments" >& 2; exit 1;; esac; done;; -h|--help|-?) usage exit 1 ;; *) err_exit "invalid action: $ACTION" 1 ;; esac shift $((OPTIND - 1)) case "$CHALLENGE_TYPE" in http-01) DOMAIN_EXTRA_PAT='' ;; dns-01) DOMAIN_EXTRA_PAT='\(\*\.\)\{0,1\}' ;; *) echo "unsupported challenge type: $CHALLENGE_TYPE" >& 2; exit 1 ;; esac printf '%s\n' "$SIGNING_CHAIN_SELECTION" | grep -E -s -q '^[0-9]+$' || err_exit "Unsupported signing chain selection" 1 case "$ACTION" in accountid) load_account_key register_account_key retrieve_kid printf "account id URI: %s\n" "$KID" exit;; clrpenda) load_account_key register_account_key retrieve_kid clrpenda exit;; delete) load_account_key register_account_key nodie REGISTRATION_URI="`fetch_location`" delete_account_key exit 0;; register) load_account_key [ -z "$ACCOUNT_EMAIL" ] && echo "account email address not given" >& 2 && exit 1 log "register account" register_account_key [ $SHOW_THUMBPRINT -eq 1 ] && printf "account thumbprint: %s\n" "$ACCOUNT_THUMB" exit 0;; thumbprint) load_account_key no_pwnd_check printf "account thumbprint: %s\n" "$ACCOUNT_THUMB" exit 0;; revoke) [ -n "$SERVER_CERT" ] || err_exit "no certificate file given to revoke" [ -z "$ACCOUNT_KEY" -a -z "$SERVER_KEY" ] && echo "either account key or server key must be given" >& 2 && exit 1 [ -n "$ACCOUNT_KEY" ] || { log "using server key as account key" ; ACCOUNT_KEY="$SERVER_KEY" ; } load_account_key revoke_certificate && exit 0 certificate_extract_domains;; sign) err_exit "neither server key nor server csr given" 1 ;; sign-key) load_account_key [ -r "$SERVER_KEY" ] || err_exit "could not read server key" [ -n "$SERVER_CERT" ] || err_exit "no output file given" [ "$#" -gt 0 ] || err_exit "domains needed" DOMAINS=$* gen_csr_with_private_key ;; sign-csr) load_account_key [ -r "$SERVER_CSR" ] || err_exit "could not read certificate signing request" [ -n "$SERVER_CERT" ] || err_exit "no output file given" [ "$#" -eq 0 ] || err_exit "no domains needed" # load domains from csr openssl req -in "$SERVER_CSR" > "$TMP_SERVER_CSR" 2> "$OPENSSL_ERR" handle_openssl_exit "$?" "copying csr" csr_extract_domains ;; *) err_exit "invalid action: $ACTION" 1 ;; esac [ -n "$WEBDIR" ] && [ "$CHALLENGE_TYPE" = "dns-01" ] && err_exit "webdir option and dns-01 challenge type are mutual exclusive" 1 if [ "$CHALLENGE_TYPE" = "http-01" ] ;then [ -n "$WEBDIR" ] && [ -n "$PUSH_TOKEN" ] && err_exit "webdir option and command to install the token are mutual exclusive" 1 [ -z "$WEBDIR" ] && [ -z "$PUSH_TOKEN" ] && err_exit "either webdir option or command to install the token must be specified" 1 fi [ -z "$PUSH_TOKEN" ] && [ -n "$PUSH_TOKEN_COMMIT" ] && err_exit "commit feature without command to install the token makes no sense" 1 if [ -z "$SERVER_SIGNING_CHAIN" ] ;then SERVER_SIGNING_CHAIN="$SERVER_CERT"_chain fi request_challenge push_response request_verification check_verification if [ "$ACTION" = "revoke" ] ;then revoke_certificate || { show_error "revoking certificate via account key" ; exit 1 ; } else request_certificate fi