From fcb5a03b4819b95be186c4019b4cdcd7e4adbef6 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 16:27:30 +0100 Subject: [PATCH 01/13] fix pubMod64 and thumbprint calculation We must strip the trailing newline with `echo -n` before we pass it to perl to convert the hex to binary, not after. --- letsencrypt.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index bc8e206..37beb9d 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -92,9 +92,9 @@ if [ ! -e "private_key.pem" ]; then fi pubExponent64="$(printf "%06x" "$(openssl rsa -in private_key.pem -noout -text | grep publicExponent | head -1 | cut -d' ' -f2)" | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' | urlbase64)" -pubMod64="$(echo -n "$(openssl rsa -in private_key.pem -noout -modulus | cut -d'=' -f2 | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie')" | urlbase64)" +pubMod64="$(echo -n "$(openssl rsa -in private_key.pem -noout -modulus | cut -d'=' -f2)" | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' | urlbase64)" -thumbprint="$(echo -n "$(echo -n '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | sha256sum | awk '{print $1}' | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie')" | urlbase64)" +thumbprint="$(echo -n "$(echo -n '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | sha256sum | awk '{print $1}')" | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' | urlbase64)" if [ "${register}" = "1" ]; then echo "+ Registering account key with letsencrypt..." From 9fe313d88780c3f0759dce4b3093715a5f664768 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 16:29:55 +0100 Subject: [PATCH 02/13] add hex2bin helper function --- letsencrypt.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index 37beb9d..f50f40c 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -7,6 +7,9 @@ source config.sh urlbase64() { base64 -w 0 | sed -r 's/=*$//g' | tr '+/' '-_' } +hex2bin() { + perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' +} signed_request() { payload64="$(echo -n "${2}" | urlbase64)" @@ -91,10 +94,10 @@ if [ ! -e "private_key.pem" ]; then register="1" fi -pubExponent64="$(printf "%06x" "$(openssl rsa -in private_key.pem -noout -text | grep publicExponent | head -1 | cut -d' ' -f2)" | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' | urlbase64)" -pubMod64="$(echo -n "$(openssl rsa -in private_key.pem -noout -modulus | cut -d'=' -f2)" | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' | urlbase64)" +pubExponent64="$(printf "%06x" "$(openssl rsa -in private_key.pem -noout -text | grep publicExponent | head -1 | cut -d' ' -f2)" | hex2bin | urlbase64)" +pubMod64="$(echo -n "$(openssl rsa -in private_key.pem -noout -modulus | cut -d'=' -f2)" | hex2bin | urlbase64)" -thumbprint="$(echo -n "$(echo -n '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | sha256sum | awk '{print $1}')" | perl -pe 's/([0-9a-f]{2})/chr hex $1/gie' | urlbase64)" +thumbprint="$(echo -n "$(echo -n '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | sha256sum | awk '{print $1}')" | hex2bin | urlbase64)" if [ "${register}" = "1" ]; then echo "+ Registering account key with letsencrypt..." From 199b70b6512095ab5f3d9415e6bd09f19cb47641 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:15:46 +0100 Subject: [PATCH 03/13] replace variable with %s in printf format string Found by shellcheck. --- letsencrypt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index f50f40c..afedec3 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -44,7 +44,7 @@ sign_domain() { echo " + Generating private key..." openssl genrsa -out "certs/${domain}/privkey.pem" 4096 2> /dev/null > /dev/null echo " + Generating signing request..." - openssl req -new -sha256 -key "certs/${domain}/privkey.pem" -out "certs/${domain}/cert.csr" -subj "/CN=${domain}/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=${SAN}")) > /dev/null + openssl req -new -sha256 -key "certs/${domain}/privkey.pem" -out "certs/${domain}/cert.csr" -subj "/CN=${domain}/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=%s" "${SAN}")) > /dev/null fi for altname in $altnames; do From 2e9c7a8c7191fee4bb6ca57992b1899b798e59b6 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:21:32 +0100 Subject: [PATCH 04/13] add missing quotes Found by shellcheck. --- letsencrypt.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index afedec3..64cacda 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -14,7 +14,7 @@ hex2bin() { signed_request() { payload64="$(echo -n "${2}" | urlbase64)" - nonce="$(curl -s -I ${CA}/directory | grep Replay-Nonce | awk -F ': ' '{print $2}' | tr -d '\n\r')" + nonce="$(curl -s -I "${CA}"/directory | grep Replay-Nonce | awk -F ': ' '{print $2}' | tr -d '\n\r')" header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' @@ -37,7 +37,7 @@ sign_domain() { for altname in $altnames; do SAN+="DNS:${altname}, " done - SAN="$(echo -n $SAN | sed 's/,\s*$//g')" + SAN="$(echo -n "${SAN}" | sed 's/,\s*$//g')" mkdir "certs/${domain}" @@ -51,8 +51,8 @@ sign_domain() { echo " + Requesting challenge for ${altname}..." response="$(signed_request "${CA}/acme/new-authz" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}')" - challenge_token="$(echo $response | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"token":\s*"[^"]*"' | cut -d'"' -f4 | sed 's/[^A-Za-z0-9_\-]/_/g')" - challenge_uri="$(echo $response | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" + challenge_token="$(echo "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"token":\s*"[^"]*"' | cut -d'"' -f4 | sed 's/[^A-Za-z0-9_\-]/_/g')" + challenge_uri="$(echo "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" if [ "${challenge_token}" = "" ] || [ "${challenge_uri}" = "" ]; then echo " + Error: Can't retrieve challenges (${reqsponse})" From 1f65a33525b3dfd9cdf765d0ac2e286e1c93ea01 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:21:49 +0100 Subject: [PATCH 05/13] don't use assign array to string ($@ -> $*) Found by shellcheck. --- letsencrypt.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index 64cacda..15f384c 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -30,8 +30,8 @@ signed_request() { sign_domain() { domain="${1}" - altnames="${@}" - echo "Signing domain ${1} (${@})..." + altnames="${*}" + echo "Signing domain ${1} (${*})..." if [ ! -e "certs/${domain}" ]; then SAN="" for altname in $altnames; do From a1621214164240f4e6db10de04b2042ece902132 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:22:13 +0100 Subject: [PATCH 06/13] fix typo in variable name Found by shellcheck. --- letsencrypt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index 15f384c..522fc53 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -55,7 +55,7 @@ sign_domain() { challenge_uri="$(echo "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" if [ "${challenge_token}" = "" ] || [ "${challenge_uri}" = "" ]; then - echo " + Error: Can't retrieve challenges (${reqsponse})" + echo " + Error: Can't retrieve challenges (${response})" exit 1 fi From a53cd916943cf60257c7e254cb1f4a7ff56978da Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:22:28 +0100 Subject: [PATCH 07/13] remove useless cat Found by shellcheck. --- letsencrypt.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index 522fc53..e941e31 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -104,6 +104,6 @@ if [ "${register}" = "1" ]; then signed_request "${CA}/acme/new-reg" '{"resource": "new-reg", "agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"}' > /dev/null fi -cat domains.txt | sed 's/^\s*//g;s/\s*$//g' | grep -v '^#' | grep -v '^$' | while read line; do - sign_domain $line + Date: Sat, 5 Dec 2015 14:08:41 +0100 Subject: [PATCH 08/13] replace echo (-n) with printf printf is more portable and never interprets any escape characters. --- letsencrypt.sh | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index e941e31..04c150d 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -12,16 +12,16 @@ hex2bin() { } signed_request() { - payload64="$(echo -n "${2}" | urlbase64)" + payload64="$(printf '%s' "${2}" | urlbase64)" nonce="$(curl -s -I "${CA}"/directory | grep Replay-Nonce | awk -F ': ' '{print $2}' | tr -d '\n\r')" header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}' - protected64="$(echo -n "${protected}" | urlbase64)" + protected64="$(printf '%s' "${protected}" | urlbase64)" - signed64="$(echo -n "${protected64}.${payload64}" | openssl dgst -sha256 -sign private_key.pem | urlbase64)" + signed64="$(printf '%s' "${protected64}.${payload64}" | openssl dgst -sha256 -sign private_key.pem | urlbase64)" data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' @@ -37,7 +37,7 @@ sign_domain() { for altname in $altnames; do SAN+="DNS:${altname}, " done - SAN="$(echo -n "${SAN}" | sed 's/,\s*$//g')" + SAN="$(printf '%s' "${SAN}" | sed 's/,\s*$//g')" mkdir "certs/${domain}" @@ -51,8 +51,8 @@ sign_domain() { echo " + Requesting challenge for ${altname}..." response="$(signed_request "${CA}/acme/new-authz" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}')" - challenge_token="$(echo "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"token":\s*"[^"]*"' | cut -d'"' -f4 | sed 's/[^A-Za-z0-9_\-]/_/g')" - challenge_uri="$(echo "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" + challenge_token="$(printf '%s\n' "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"token":\s*"[^"]*"' | cut -d'"' -f4 | sed 's/[^A-Za-z0-9_\-]/_/g')" + challenge_uri="$(printf '%s\n' "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" if [ "${challenge_token}" = "" ] || [ "${challenge_uri}" = "" ]; then echo " + Error: Can't retrieve challenges (${response})" @@ -61,12 +61,12 @@ sign_domain() { keyauth="${challenge_token}.${thumbprint}" - echo -n "${keyauth}" > "${WELLKNOWN}/${challenge_token}" + printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_token}" echo " + Responding to challenge for ${altname}..." result="$(signed_request "${challenge_uri}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}')" - status="$(echo "${result}" | grep -Eo '"status":\s*"[^"]*"' | cut -d'"' -f4)" + status="$(printf '%s\n' "${result}" | grep -Eo '"status":\s*"[^"]*"' | cut -d'"' -f4)" if [ ! "${status}" = "pending" ] && [ ! "${status}" = "valid" ]; then echo " + Challenge is invalid! (${result})" @@ -83,7 +83,7 @@ sign_domain() { echo " + Requesting certificate..." csr64="$(openssl req -in "certs/${domain}/cert.csr" -outform DER | urlbase64)" crt64="$(signed_request "${CA}/acme/new-cert" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | base64 -w 64)" - echo -e "-----BEGIN CERTIFICATE-----\n${crt64}\n-----END CERTIFICATE-----\n" > "certs/${domain}/cert.pem" + printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" > "certs/${domain}/cert.pem" echo " + Done!" } @@ -95,9 +95,9 @@ if [ ! -e "private_key.pem" ]; then fi pubExponent64="$(printf "%06x" "$(openssl rsa -in private_key.pem -noout -text | grep publicExponent | head -1 | cut -d' ' -f2)" | hex2bin | urlbase64)" -pubMod64="$(echo -n "$(openssl rsa -in private_key.pem -noout -modulus | cut -d'=' -f2)" | hex2bin | urlbase64)" +pubMod64="$(printf '%s' "$(openssl rsa -in private_key.pem -noout -modulus | cut -d'=' -f2)" | hex2bin | urlbase64)" -thumbprint="$(echo -n "$(echo -n '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | sha256sum | awk '{print $1}')" | hex2bin | urlbase64)" +thumbprint="$(printf '%s' "$(printf '%s' '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | sha256sum | awk '{print $1}')" | hex2bin | urlbase64)" if [ "${register}" = "1" ]; then echo "+ Registering account key with letsencrypt..." From 2d6cb75f8a7fc65a3bc885d065c91bb91cf24374 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:52:26 +0100 Subject: [PATCH 09/13] use curl -sSf to display error messages on failure --- letsencrypt.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index 04c150d..eb72b3d 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -14,7 +14,8 @@ hex2bin() { signed_request() { payload64="$(printf '%s' "${2}" | urlbase64)" - nonce="$(curl -s -I "${CA}"/directory | grep Replay-Nonce | awk -F ': ' '{print $2}' | tr -d '\n\r')" + # -sSf: stay silent but report errors and exit with != 0 if they occur + nonce="$(curl -sSf -I "${CA}"/directory | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' @@ -25,7 +26,7 @@ signed_request() { data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' - curl -s -d "${data}" "${1}" + curl -sSf -d "${data}" "${1}" } sign_domain() { @@ -74,7 +75,7 @@ sign_domain() { fi while [ ! "${status}" = "valid" ]; do - status="$(curl -s "${challenge_uri}" | grep -Eo '"status":\s*"[^"]*"' | cut -d'"' -f4)" + status="$(curl -sSf "${challenge_uri}" | grep -Eo '"status":\s*"[^"]*"' | cut -d'"' -f4)" done echo " + Challenge is valid!" From 2f3ee624c548de95c0c3a2eb5aa1e154d2f09856 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:25:11 +0100 Subject: [PATCH 10/13] use [ -z .. ] instead of explicit compare with "" --- letsencrypt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index eb72b3d..361ab30 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -55,7 +55,7 @@ sign_domain() { challenge_token="$(printf '%s\n' "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"token":\s*"[^"]*"' | cut -d'"' -f4 | sed 's/[^A-Za-z0-9_\-]/_/g')" challenge_uri="$(printf '%s\n' "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]' | sed 's/{/\n{/g' | grep 'http-01' | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" - if [ "${challenge_token}" = "" ] || [ "${challenge_uri}" = "" ]; then + if [ -z "${challenge_token}" ] || [ -z "${challenge_uri}" ]; then echo " + Error: Can't retrieve challenges (${response})" exit 1 fi From 5fedf3b3ca4245f84e60053894d8ae6fe2c2c677 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:26:53 +0100 Subject: [PATCH 11/13] replace source with . source searches in $PATH which should not be necessary and might be problematic. --- letsencrypt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index 361ab30..edacf36 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -2,7 +2,7 @@ set -e -source config.sh +. ./config.sh urlbase64() { base64 -w 0 | sed -r 's/=*$//g' | tr '+/' '-_' From fb1790cdfa012939aac7861c92f5c57ba2bb7329 Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:30:32 +0100 Subject: [PATCH 12/13] use set -u to catch uninitialized variables --- letsencrypt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt.sh b/letsencrypt.sh index edacf36..8e6e231 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -eu . ./config.sh From 181dd0ff2a82dcac10c8d46981c78e3a772638dc Mon Sep 17 00:00:00 2001 From: Simon Ruderich Date: Sat, 5 Dec 2015 14:52:40 +0100 Subject: [PATCH 13/13] use umask 077 to protect private keys --- letsencrypt.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/letsencrypt.sh b/letsencrypt.sh index 8e6e231..51249f2 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -4,6 +4,9 @@ set -eu . ./config.sh +umask 077 # paranoid umask, we're creating private keys + + urlbase64() { base64 -w 0 | sed -r 's/=*$//g' | tr '+/' '-_' }