Created
April 23, 2016 11:57
-
-
Save c0psrul3/3dea3b19363edf1df0357caaf9a3a8e8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/bash | |
# shell script hardening | |
set -euf -o pipefail | |
# | |
# Lets Encrypt Certificate Generator | |
# https://calomel.org/lets_encrypt_client.html | |
# lets_encrypt.sh v0.04 | |
# | |
# The script will generate a new certificate for the domain specified and | |
# negotiate with the Lets Encrypt ACME server to save a signed certificate | |
# chain. | |
# | |
# dependency: bash, openssl, curl | |
################ options start################# | |
# The primary domain name followed by any alternative names we are requesting a | |
# certificate for. Use a space separated list. Each domain name will be tested | |
# by the ACME server. | |
#DOMAINS="example.org www.example.org mail.example.org" | |
DOMAINS="example.org" | |
# The directory the script is run from and where all certificates will be | |
# stored under. This directory should be secure and not under the web root. | |
BASEDIR="/tools/lets_encrypt" | |
# The full path to the web directory our script will write the temporary | |
# negotiation file. The Lets Encrypt service will then connect to our web | |
# server to collect this temporary file verifying we own the domain. Our web | |
# server can serve the file through http or https and 301 redirection are | |
# allowed. | |
WEBDIR="/var/www/.well-known/acme-challenge" | |
# The domain private key size in bits. Options are "2048" or "4096" bits. A | |
# 2048 bit key is seven(7) times faster for the client to processes compared to | |
# a 4096 bit key. | |
KEYSIZE="2048" | |
# The Lets Encrypt certificate authority URL | |
#CA="https://acme-staging.api.letsencrypt.org" # testing server, high rate limits. "happy hacker fake CA" | |
CA="https://acme-v01.api.letsencrypt.org" # official server, rate limited to 5 certs per 7 days | |
################ options end ################## | |
# The license file the script will automatically accept for you | |
LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" | |
# The local name of Lets Encrypt public certificate | |
ROOTCERT="public-lets-encrypt-x1-cross-signed.pem" | |
# check the path to the openssl configuration file | |
OPENSSL_CNF="$(openssl version -d | cut -d'"' -f2)/openssl.cnf" | |
urlbase64() { | |
# urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_' | |
openssl base64 -e | tr -d '\n\r' | sed 's/=*$//g' | tr '+/' '-_' | |
} | |
hex2bin() { | |
# Store hex string from stdin | |
tmphex="$(cat)" | |
# Remove spaces | |
hex='' | |
for ((i=0; i<${#tmphex}; i+=1)); do | |
test "${tmphex:$i:1}" == " " || hex="${hex}${tmphex:$i:1}" | |
done | |
# Add leading zero | |
test $((${#hex} & 1)) == 0 || hex="0${hex}" | |
# Convert to escaped string | |
escapedhex='' | |
for ((i=0; i<${#hex}; i+=2)); do | |
escapedhex=$escapedhex\\x${hex:$i:2} | |
done | |
# Convert to binary data | |
printf -- "${escapedhex}" | |
} | |
_request() { | |
tempcont="$(mktemp)" | |
case "$1" in | |
"get" ) | |
statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}")" ;; | |
"head" ) | |
statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" ;; | |
"post" ) | |
statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")" ;; | |
esac | |
if [[ ! "${statuscode:0:1}" = "2" ]]; then | |
printf '%s\n' " ERROR: sending ${1}-request to ${2} (Status ${statuscode})" >&2 | |
printf '%s\n' >&2 | |
printf '%s\n' "Details:" >&2 | |
printf '%s\n' "$(<"${tempcont}"))" >&2 | |
rm -f "${tempcont}" | |
exit 1 | |
fi | |
cat "${tempcont}" | |
rm -f "${tempcont}" | |
} | |
thumb_print() { | |
# Collect the public components from the new private key and calculate the | |
# thumbprint which the ACME server will challenge | |
pubExponent64="$(printf "%06x" "$(openssl rsa -in "${BASEDIR}/private_account_key.pem" -noout -text | grep publicExponent | head -1 | cut -d' ' -f2)" | hex2bin | urlbase64)" | |
pubMod64="$(printf '%s' "$(openssl rsa -in "${BASEDIR}/private_account_key.pem" -noout -modulus | cut -d'=' -f2)" | hex2bin | urlbase64)" | |
thumbprint="$(printf '%s' "$(printf '%s' '{"e":"'"${pubExponent64}"'","kty":"RSA","n":"'"${pubMod64}"'"}' | shasum -a 256 | awk '{print $1}')" | hex2bin | urlbase64)" | |
} | |
signed_request() { | |
# Encode payload as urlbase64 | |
payload64="$(printf '%s' "${2}" | urlbase64)" | |
# Retrieve nonce from acme-server | |
nonce="$(_request head "${CA}/directory" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" | |
# Build header with the public key and algorithm information | |
header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' | |
# Build another header containing the previously received nonce and encode the nonce as urlbase64 | |
protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}' | |
protected64="$(printf '%s' "${protected}" | urlbase64)" | |
# Sign the header with the nonce and the payload with the private key and the encode the signature as urlbase64 | |
signed64="$(printf '%s' "${protected64}.${payload64}" | openssl dgst -sha256 -sign "${BASEDIR}/private_account_key.pem" | urlbase64)" | |
# Send header + extended header + payload + signature to the acme-server | |
data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' | |
_request post "${1}" "${data}" | |
} | |
sign_domain() { | |
domain="${1}" | |
altnames="${*}" | |
# create a directory to keep the domain's certificates in | |
if [[ ! -e "${BASEDIR}/${domain}" ]]; then | |
printf " + Make directory ${BASEDIR}/${domain}\n" | |
mkdir -p "${BASEDIR}/${domain}" | |
fi | |
# Create a new private key for the domain. To add a bit of entropy to the | |
# process, a simple loop will randomly generate between five(5) and ten(10) | |
# private keys and the last key created will be used for the certificate | |
# signing request. A loop is not necessary on native hardware, but may help | |
# seed VM entropy. | |
printf " + Seed entropy by generating random keys:" | |
START=1 | |
END=$(( RANDOM % (10 - 5 + 1 ) + 5 )) | |
for (( i=$START; i<=$END; i++ )) | |
do | |
printf " $i" | |
openssl genrsa -out "${BASEDIR}/${domain}/${domain}-privatekey.pem" "${KEYSIZE}" 2> /dev/null > /dev/null | |
done | |
printf "\n + Private Key created\n" | |
# Generate a signing request | |
SAN="" | |
for altname in $altnames; do | |
SAN+="DNS:${altname}, " | |
done | |
SAN="${SAN%%, }" | |
printf " + Generate signing request\n" | |
openssl req -new -sha256 -key "${BASEDIR}/${domain}/${domain}-privatekey.pem" -out "${BASEDIR}/${domain}/${domain}-certsignrequest.csr" -subj "/CN=${domain}/" -reqexts SAN -config <(cat "${OPENSSL_CNF}" <(printf "[SAN]\nsubjectAltName=%s" "${SAN}")) > /dev/null | |
# Request and respond to challenges | |
for altname in $altnames; do | |
# Ask the acme-server for new challenge token and extract them from the resulting json block | |
printf " + Request challenge for ${altname}\n" | |
response="$(signed_request "${CA}/acme/new-authz" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}')" | |
challenges="$(printf '%s\n' "${response}" | grep -Eo '"challenges":[^\[]*\[[^]]*]')" | |
challenge="$(printf "%s" "${challenges//\{/$'\n'{}}" | grep 'http-01')" | |
challenge_token="$(printf '%s' "${challenge}" | grep -Eo '"token":\s*"[^"]*"' | cut -d'"' -f4 | sed 's/[^A-Za-z0-9_\-]/_/g')" | |
challenge_uri="$(printf '%s' "${challenge}" | grep -Eo '"uri":\s*"[^"]*"' | cut -d'"' -f4)" | |
if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then | |
printf " Error: Can't retrieve challenges (${response})\n" | |
exit 1 | |
fi | |
# Challenge response consists of the challenge token and the thumbprint of our public certificate | |
keyauth="${challenge_token}.${thumbprint}" | |
# Store challenge response in the web directory | |
printf '%s' "${keyauth}" > "${WEBDIR}/${challenge_token}" | |
chmod a+r "${WEBDIR}/${challenge_token}" | |
# Request the acme-server to verify our challenge and wait until the request is valid | |
printf " + Respond to challenge for ${altname}\n" | |
result="$(signed_request "${challenge_uri}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}')" | |
status="$(printf '%s\n' "${result}" | grep -Eo '"status":\s*"[^"]*"' | cut -d'"' -f4)" | |
# Loop until the status of the request is accepted | |
while [[ "${status}" = "pending" ]]; do | |
sleep 1 | |
status="$(_request get "${challenge_uri}" | grep -Eo '"status":\s*"[^"]*"' | cut -d'"' -f4)" | |
done | |
# Remove the temporary challenge file from the web directory | |
rm -f "${WEBDIR}/${challenge_token}" | |
# Check the status of the ACME server negotiation | |
if [[ "${status}" = "valid" ]]; then | |
printf " + Challenge accepted\n" | |
else | |
printf " Challenge is invalid ! (returned: ${status})\n" | |
exit 1 | |
fi | |
done | |
# create domain certificate | |
printf " + Create domain certificate\n" | |
csr64="$(openssl req -in "${BASEDIR}/${domain}/${domain}-certsignrequest.csr" -outform DER | urlbase64)" | |
crt64="$(signed_request "${CA}/acme/new-cert" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | openssl base64 -e)" | |
printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" > "${BASEDIR}/${domain}/${domain}-certchain.pem" | |
# add the intermediate lets encrypt public certificate to the chain | |
printf " + Add intermediate certificate to chain\n" | |
cat "${BASEDIR}/${ROOTCERT}" >> "${BASEDIR}/${domain}/${domain}-certchain.pem" | |
printf " + Complete.\n" | |
} | |
inspect() { | |
domain="${1}" | |
rootcerts="/etc/ssl/" | |
# location of FreeBSD's root certificates | |
if [ -f /etc/ssl/cert.pem ]; then | |
rootcerts="/etc/ssl/cert.pem" | |
fi | |
printf "\n\n Certificate Inspection\n" | |
printf " ------------------------\n" | |
printf "\nMD5 signatures must be equal\n\n" | |
md5privatekey="$(openssl rsa -noout -modulus -in ${BASEDIR}/${domain}/${domain}-privatekey.pem | openssl md5)" | |
md5certsignrequest="$(openssl req -noout -modulus -in ${BASEDIR}/${domain}/${domain}-certsignrequest.csr | openssl md5)" | |
md5certchain="$(openssl x509 -noout -modulus -in ${BASEDIR}/${domain}/${domain}-certchain.pem | openssl md5)" | |
printf " Private Key = $md5privatekey\n" | |
printf " Cert Sign Req = $md5certsignrequest\n" | |
printf " Cert Chain = $md5certchain\n" | |
# print warning if the all the MD5 sums do not match | |
case "$md5privatekey" in | |
"$md5certsignrequest"|"$md5certchain") | |
printf " PASSED\n" ;; | |
*) | |
printf "\n ERROR: MD5 sums do NOT match\n" ;; | |
esac | |
printf "\nLocally Inspect Certificate\n openssl x509 -in ${domain}/${domain}-certchain.pem -text -noout\n" | |
printf "\nRemotely Inspect Certificate\n openssl s_client -CApath $rootcerts -connect ${domain}:443 \n" | |
hpkp="$(openssl rsa -in ${BASEDIR}/${domain}/${domain}-privatekey.pem -outform der -pubout 2>/dev/null | openssl dgst -sha256 -binary | openssl enc -base64)" | |
printf "\nHTTP Key Pinning\n pin-sha256=\"$hpkp\";\n" | |
# check the issuer field and the full certificate path against the system's root certificate chain | |
printf "\nVerify the authority and certificate chain\n" | |
printf " "; openssl x509 -noout -in ${domain}/${domain}-certchain.pem -issuer | |
printf " "; openssl verify -CApath $rootcerts ${ROOTCERT} | |
printf " "; openssl verify -CApath $rootcerts -untrusted ${ROOTCERT} ${domain}/${domain}-certchain.pem | |
printf "\n\n" | |
} | |
## | |
## Lets Encrypt main() | |
## | |
printf "\n Lets Encrypt Certificate Generator\n" | |
printf " ------------------------------------\n" | |
printf "\nInitialize the environment\n\n" | |
# Change directory to BASEDIR | |
cd ${BASEDIR} | |
# Update the Lets Encrypt Authority X1 PEM certificate | |
printf " + Update the Lets Encrypt Authority X1 PEM certificate\n" | |
curl -sS -L -o ${BASEDIR}/${ROOTCERT} https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem | |
# Generate a new account key | |
printf " + Generate new private account key\n" | |
openssl genrsa -out "${BASEDIR}/private_account_key.pem" "4096" 2> /dev/null > /dev/null | |
# Calculate the thumbprint to be registered with the ACME server | |
printf " + Calculate key thumbprint for ACME challenge\n" | |
pubExponent64=""; pubMod64=""; thumbprint="" | |
thumb_print | |
# Register the new account key with the Lets Encrypt ACME service | |
printf " + Register private account key with ACME server\n" | |
signed_request "${CA}/acme/new-reg" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > /dev/null | |
# Generate certificate for the domain | |
printf "\nGenerate certificate for ${DOMAINS}\n\n" | |
sign_domain ${DOMAINS} | |
# Visually inspect the MD5 hashes | |
inspect ${DOMAINS} | |
# | |
## | |
### EOF ### |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment