1490 lines
46 KiB
Bash
1490 lines
46 KiB
Bash
|
|
#!/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"
|
||
|
|
|
||
|
|
# 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
|
||
|
|
}
|
||
|
|
|
||
|
|
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() {
|
||
|
|
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() {
|
||
|
|
REQUIRED_COMMANDS="basename cat cp rm sed grep egrep fgrep tr mktemp expr tail xxd 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" | egrep -s -q -e '^[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(){
|
||
|
|
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
|
||
|
|
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 ! egrep -s -q -e '"'"$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 | egrep -s -q -e "$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
|
||
|
|
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"
|
||
|
|
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"
|
||
|
|
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" | egrep -i -s -q -e '\.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' | egrep -s -q -e '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;;
|
||
|
|
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 :hqvD:46a:e:p 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";;
|
||
|
|
p) SHOW_THUMBPRINT=1;;
|
||
|
|
a) ACCOUNT_KEY="$OPTARG";;
|
||
|
|
e) ACCOUNT_EMAIL="$OPTARG";;
|
||
|
|
?|:) echo "invalid arguments" >& 2; exit 1;;
|
||
|
|
esac; done;;
|
||
|
|
thumbprint)
|
||
|
|
while getopts :hqva: name; do case "$name" in
|
||
|
|
h) usage; exit 1;;
|
||
|
|
q) LOGLEVEL=0;;
|
||
|
|
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;;
|
||
|
|
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 :hqvD:46Ca:k:r:f:s:c:w:P:l:t: 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";;
|
||
|
|
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
|
||
|
|
;;
|
||
|
|
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" | egrep -s -q -e '^[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
|