#Do less often function generateCA { openssl req -x509 -newkey rsa:4096 -keyout $1.key -out $1.crt -sha256 -days 1825 -noenc -config $2; } # $1=kaka $2=caconf #Do more often function generatePriv { openssl req -new -newkey rsa:4096 -keyout $1.key -out $1.csr -sha256 -noenc -config $2; } # $1=live $2=nuconf # Should break this into two functions actually. #... Do most often? Maybe don't need to remake privkey. function signCert { openssl x509 -req -in $1.csr -out $1.crt -CA $2.crt -CAkey $2.key -days 400 -copy_extensions copyall; rm $1.csr ; } # $1=live $2=kaka # REMINDER TO PUT DIFFERENT NAMES ON THE CERTS FROM NOW ON TO MAKE THINGS CLEARER # No. Fuck you faggot, kaka and live are obvious enough. # GENERIC UTILS function ifsrev { __temp=$(tr "$2" '\n' <<< "$1" | tac | tr '\n' "$2"); echo -n ${__temp:0:-1}; } function b64u { cat | base64 -w 0 | tr '/+' '_-' | tr -d '= '; } # u for url-safe function short256 { __temp=$(cat | sha256sum); echo -n ${__temp:0:-3}; } function hex2bin { cat | sed 's/\([0-9a-f]\{2\}\)/\\\\\\x\1/g' | xargs printf; } # UTILS function get_CA_URLs { declare -A CAs CAs[LE]=https://acme-v02.api.letsencrypt.org/directory CAs[LEtest]=https://acme-staging-v02.api.letsencrypt.org/directory CAs[buypass]=https://api.buypass.com/acme/directory CAs[buypasstest]=https://api.test4.buypass.no/acme/directory CAs[ZSSL]=https://acme.zerossl.com/v2/DV90 CAs[sslcom_rsa]=https://acme.ssl.com/sslcom-dv-rsa CAs[sslcom_ecc]=https://acme.ssl.com/sslcom-dv-ecc CAs[jewgle]=https://dv.acme-v02.api.pki.goog/directory CAs[jewgletest]=https://dv.acme-v02.test-api.pki.goog/directory CA=$1 endpoints=$(curl -s ${CAs[$CA]}) export CA export endpoints # I don't know if this works, or if it matters } # $1=LE function read_ACME { # Extract signature info from key: read hex exp <<< $(openssl rsa -in account.key -noout -text | tr -d ': \r\n' | sed -E 's/^.*modulus00([a-f0-9]+)publicExponent[0-9]+\(0x([0-9a-f]+)\).*$/\1 \2/') # And prepare it for sending to ACME server: hex=$(hex2bin <<< "$hex" | b64u) exp=$([ $((${#exp}%2)) = 0 ] || echo -n 0; echo $exp) # There's probably an easier way than this, but it works and makes sense so whatever. exp=$(hex2bin <<< "$exp" | b64u) # Apparently this one goes in URLs. OR NOT, THE OTHER ONE DOES. jerk='{"e": "'$exp'", "kty": "RSA", "n": "'$hex'"}' sig='{"alg":"RS256", "jwk": '$jerk'}' # Keys must be sorted. thumb=$(echo -n "$jerk" | tr -d ' ' | short256 | hex2bin | b64u) # How many times can you b64 a bowl of ramen before it begins questioning its existence } function load_kid { kid=$1 if [ -z "$kid" -a -e "$CA.kid" ]; then kid=$(cat $CA.kid); fi if [ -n "$kid" ]; then sig='{"alg": "RS256", "kid": "'$kid'"}' fi } function get_nonce { # Get a nonce (nigger): nonce_resp=$1 if [ -z "$nonce_resp" ]; then nonce_resp=$(curl -si $(echo $endpoints | jq -r .newNonce)) fi grep -i 'replay-nonce' <<< "$nonce_resp" | tr -d "\r" | grep -Eo '[0-9a-zA-Z_\-]+$' # Fucking Windows line endings } # ZONEFILE UTILS function czf { sed -i '/_ACME-CHALLENGE/d' "$1"; } # clear zone file function szf # setup zone file { file=$1 shift for tok in $@; do echo _ACME-CHALLENGE 300 TXT $(echo -n "$tok.$thumb" | short256 | hex2bin | b64u) >> $file done } function usoa # update SOA { inc=$(grep -Eo 'SOA [^ ]+ [^ ]+ [0-9]+' $1 | grep -Eo '[0-9]+$') sed -i 's/SOA\t\([^\t]*\)\t\([^\t]*\)\t[0-9]*/SOA \1 \2 '$((inc+1))'/' $1 } # CHALLENGES function complete_challenges { read_ACME # get thumb. It fucks up sig but I don't care, send_req fixes it. declare -A chals toks urls=$(jq -r '.[]' <<< "$1") while read url; do read dom chalurl tok <<< "$(send_req $url | jq -r '.identifier.value+" "+(.challenges | .[] | select(.type == "dns-01") | .url+" "+.token)')" chals[$dom]="${chals[$dom]}$(base64 -w 0 <<< "$chalurl");" toks[$dom]="${toks[$dom]}$(base64 -w 0 <<< "$tok");" done <<< "$urls" for dom in ${!toks[@]}; do apply_challenge $dom ${toks[$dom]} done systemctl reload named # Actually update DNS for real final7 declare -a pending pending2 for dom in ${!chals[@]}; do IFS=';' read -a chalurls <<< ${chals[$dom]} for chalurl in ${chalurls[@]}; do sleep 1 # Spam a bit less rurl=$(base64 -d <<< $chalurl) send_req $rurl '{}' > /dev/null # need empty dict to tell server to validate challenge, empty body only checks status. Just another certified ACME moment. pending+=("$rurl") done done # now we wait for validations echo "beginning validation" >&2 echo "pending list contains: ${pending[@]}" >&2 dirty=1 timer=10 backoff=1 tick=0 while [ $dirty -gt 0 ]; do dirty=0 for url in ${pending[@]}; do sleep 1 echo "checking $url" >&2 oof=$(send_req $url) oof2=$(jq -r .status <<< "$oof") nonce=$(get_nonce) # For some reason checking status doesn't come back with a nonce, or comes back with same nonce. if [ $oof2 != valid ]; then dirty=1; pending2+=("$url"); echo -n "in" >&2; fi echo "valid" >&2 done pending=("${pending2[@]}") pending2=() echo "sleeping for $timer" >&2 sleep $timer # This gets slower each iteration, to spam the server less timer=$(($timer+$backoff)) tick=$(($tick+1)) echo "tick: $tick, backoff: $backoff" >&2 if [ $tick -ge $backoff ]; then tick=0; backoff=$(($backoff+1)); fi done } function apply_challenge { echo "applying challenge for $1: $2" >&2 dnsname="/var/named/$(ifsrev $1 .).zone" czf $dnsname szf $dnsname $(tr ';' '\n' <<< $2 | base64 -d | tr '\n' ' ') # Actually update the DNS usoa $dnsname # And don't forget to update the SOA number } # CORE ACTIONS function gen_ACME_key { openssl genrsa -traditional 2048 > account.key; } function register_acc { # Create account: read_ACME url=$(echo $endpoints | jq -r .newAccount) req='{"termsOfServiceAgreed": true, "contact": ["mailto:zerglingman@fedora.email"], "externalAccountBinding":"zerglingman@fedora.email"}' out=$(send_req "$url" "$req" yes) echo $out >&2 loc=$(grep -i 'location' <<< "$out" | grep -io http.*$) load_kid $loc echo $loc } function get_cert { conf=$1 shift url=$(echo $endpoints | jq -r .newOrder) req='{"identifiers": [' for n in $@; do req=$req'{"type": "dns", "value": "'$n'"}, {"type": "dns", "value": "*.'$n'"}, ' # Do not put wildcard domains in; this will do it automatically. You still need to put them in the nuconf. done req=${req:0:-2}']}' order=$(send_req $url "$req" yes) headers=$(sed -n 1,/^$/p <<< "$order") order=$(sed 1,/^$/d <<< "$order") # I can just access the headers lol # No I can't, assfucked by subshells again orderurl=$(grep -i location <<< "$headers" | grep -io http.*$) echo "order URL is: $orderurl" >&2 auths=$(jq '.authorizations' <<< "$order") finalise=$(jq -r '.finalize' <<< "$order") complete_challenges "$auths" openssl req -new -key live.key -out live.csr -sha256 -noenc -config $conf -outform DER # I guess I should just include the conf file send_req $finalise '{"csr":"'$(cat live.csr | b64u)'"}' certurl=$(send_req $orderurl | jq -r .certificate) curl $certurl > out.crt } # SERVER COMMUNICATION function send_req { load_kid # Just to make sure the sig is set correctly. It won't overwrite it unless it should be. url=$1; if [ -n "$2" ]; then req=$(b64u <<< "$2"); else req=""; fi if [ -z "$nonce" ]; then nonce=$(get_nonce); fi data=$(jq '.+{"nonce":"'$nonce'","url":"'$url'"}' <<< "$sig" | tr -d '\r\n' | tr -s ' ' ' ' | sed -e 's/{ /{/g' -e 's/ }/}/g') # Keys don't need to be sorted here, but they still are. # I don't know why the URL has to be in the body that is sent to the URL. # ACME is a terrible protocol. body=$(b64u <<< "$data") sig2=$(echo -n "$body.$req" | openssl dgst -sha256 -sign account.key | b64u) mexican='{"protected": "'$body'", "payload": "'$req'", "signature": "'$sig2'"}' # Because it's a jose req_resp=$(curl -isH "Content-type:application/jose+json" $url --data "$mexican" | tr -d "\r") # Here is the line that does the work req_heads=$(sed -n 1,/^$/p <<< "$req_resp") req_resp=$(sed 1,/^$/d <<< "$req_resp") nonce=$(get_nonce "$req_heads") if [ -n "$3" ]; then printf "$req_heads"; echo; echo; fi echo $req_resp } # MAIN get_CA_URLs $1 shift load_kid $@