2026-02-22 16:18:53 +01:00
#!/bin/sh
# letsencrypt.sh - a simple shell implementation for the acme protocol
# Copyright (C) 2015 Gerhard Heift
# Copyright (C) 2016-2025 Attila Bruncsak
2026-02-22 16:43:12 +01:00
# Copyright (C) 2018-2026 Stephan Düsterhaupt, Ivo Noack
2026-02-22 16:18:53 +01:00
#
# 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