From 6de7ef7cda510b6c7ec79b61108d6b76b1c3c958 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 26 Dec 2015 20:57:31 +0800 Subject: [PATCH] first public version --- le.sh | 561 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 le.sh diff --git a/le.sh b/le.sh new file mode 100644 index 0000000..b2e94f3 --- /dev/null +++ b/le.sh @@ -0,0 +1,561 @@ +#!/bin/bash + + +WORKING_DIR=~/.le + +ACCOUNT_KEY_PATH=$WORKING_DIR/account.acc + +CERT_KEY_PATH=$WORKING_DIR/domain.key + +CSR_PATH=$WORKING_DIR/domain.csr + +CERT_PATH=$WORKING_DIR/domain.cer + +DOMAIN_CONF=$WORKING_DIR/domain.conf + +CURL_HEADER="" + +HEADER="" +HEADERPLACE="" + +ACCOUNT_EMAIL="" +DEFAULT_CA="https://acme-v01.api.letsencrypt.org" + +API=$DEFAULT_CA + +DEBUG= + +_debug() { + if ! [ "DEBUG" ] ; then + return + fi + + if [ -z "$2" ] ; then + echo $1 + else + echo $1:$2 + fi +} + +_info() { + if [ -z "$2" ] ; then + echo $1 + else + echo $1:$2 + fi +} + +#domain [2048] +createAccountKey() { + if [ -z "$1" ] ; then + echo Usage: $0 account-domain [2048] + return + fi + + account=$1 + length=$2 + if [ -z "$2" ] ; then + echo Use default length 2048 + length=2048 + fi + + mkdir -p $WORKING_DIR + ACCOUNT_KEY_PATH=$WORKING_DIR/account.acc + + if [ -f "$ACCOUNT_KEY_PATH" ] ; then + echo account key exists, skip + return + else + #generate account key + openssl genrsa $length > $ACCOUNT_KEY_PATH + fi + +} + +#domain length +createDomainKey() { + if [ -z "$1" ] ; then + echo Usage: $0 domain [2048] + return + fi + + domain=$1 + length=$2 + if [ -z "$2" ] ; then + echo Use default length 2048 + length=2048 + fi + + mkdir -p $WORKING_DIR/$domain + CERT_KEY_PATH=$WORKING_DIR/$domain/$domain.key + + if [ -f "$CERT_KEY_PATH" ] ; then + echo domain key exists, skip + else + #generate account key + openssl genrsa $length > $CERT_KEY_PATH + fi + +} + +# domain domainlist +createCSR() { + if [ -z "$1" ] ; then + echo Usage: $0 domain [domainlist] + return + fi + domain=$1 + _initpath $domain + + domainlist=$2 + + if [ -f $CSR_PATH ] ; then + echo CSR exists, skip + return + fi + + if [ -z "$domainlist" ] ; then + #single domain + echo single domain + openssl req -new -sha256 -key $CERT_KEY_PATH -subj "/CN=$domain" > $CSR_PATH + else + alt=DNS:$(echo $domainlist | sed "s/,/,DNS:/g") + #multi + echo multi domain $alt + openssl req -new -sha256 -key $CERT_KEY_PATH -subj "/CN=$domain" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=$alt")) -out $CSR_PATH + fi + +} + +_b64() { + while read __line; do + __n=$__n$__line + done; + __n=$(echo $__n | sed "s|/|_|g") + __n=$(echo $__n | sed "s| ||g") + __n=$(echo $__n | sed "s|+|-|g") + __n=$(echo $__n | sed "s|=||g") + echo $__n +} + +_send_signed_request() { + url=$1 + payload=$2 + + needbas64="$3" + + _info url $url + _info payload "$payload" + + CURL_HEADER="$WORKING_DIR/curl.header" + dp="$WORKING_DIR/curl.dump" + CURL="curl --silent --dump-header $CURL_HEADER " + if [ "DEBUG" ] ; then + CURL="$CURL --trace-ascii $dp " + fi + payload64=$(echo -n $payload | base64 | _b64) + _debug payload64 $payload64 + + nonceurl="$API/directory" + nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | sed s/\\r//|sed s/\\n//| cut -d ' ' -f 2) + + _debug nonce $nonce + + protected=$(echo -n "$HEADERPLACE" | sed "s/NONCE/$nonce/" ) + _debug protected "$protected" + + protected64=$( echo -n $protected | base64 | _b64) + _debug protected64 "$protected64" + + sig=$(echo -n "$protected64.$payload64" | openssl dgst -sha256 -sign $ACCOUNT_KEY_PATH | base64| _b64) + _debug sig "$sig" + + body="{\"header\": $HEADER, \"protected\": \"$protected64\", \"payload\": \"$payload64\", \"signature\": \"$sig\"}" + _debug body "$body" + + + if [ "$needbas64" ] ; then + response=$($CURL -X POST --data "$body" $url | base64) + else + response=$($CURL -X POST --data "$body" $url) + fi + responseHeaders="$(cat $CURL_HEADER)" + + _debug responseHeaders "$responseHeaders" + _debug response "$response" + code=$(grep ^HTTP $CURL_HEADER | tail -1 | cut -d " " -f 2) + _debug code $code + +} + +_get() { + url="$1" + _info url $url + response=$(curl --silent $url) + ret=$? + _debug response "$response" + code=$(echo $response | grep -o '"status":[0-9]\+' | cut -d : -f 2) + _debug code $code + return $ret +} + +#setopt "file" "opt" "=" "value" [";"] +_setopt() { + __conf="$1" + __opt="$2" + __sep="$3" + __val="$4" + __end="$5" + if [ -z "$__opt" ] ; then + echo usage: $0 '"file" "opt" "=" "value" [";"]' + return + fi + + if grep -H -n "^$__opt$__sep" $__conf ; then + echo OK + sed -i "s|^$__opt$__sep.*$|$__opt$__sep$__val$__end|" $__conf + else + echo APP + echo "$__opt$__sep$__val$__end" >> $__conf + fi + grep -H -n "^$__opt$__sep" $__conf +} + +_initpath() { + WORKING_DIR=~/.le + domain=$1 + mkdir -p $WORKING_DIR + ACCOUNT_KEY_PATH=$WORKING_DIR/account.acc + + if [ -z "$domain" ] ; then + return 0 + fi + + mkdir -p $WORKING_DIR/$domain + + if [ -z "$CSR_PATH" ] ; then + CSR_PATH=$WORKING_DIR/$domain/$domain.csr + fi + + if [ -z "$CERT_KEY_PATH" ] ; then + CERT_KEY_PATH=$WORKING_DIR/$domain/$domain.key + fi + + if [ -z "$CERT_PATH" ] ; then + CERT_PATH=$WORKING_DIR/$domain/$domain.cer + fi + +} + +#issue webroot a.com [www.a.com,b.com,c.com] [key-length] [cert-file-path] [key-file-path] [reloadCmd] +issue() { + if [ -z "$1" ] ; then + echo "Usage: $0 webroot a.com [www.a.com,b.com,c.com] [key-length] [cert-file-path] [key-file-path] [reloadCmd]" + return 1 + fi + Le_Webroot=$1 + Le_Domain=$2 + Le_Alt=$3 + Le_Keylength=$4 + + if [ -z "$Le_Domain" ] ; then + Le_Domain="$1" + fi + + _initpath $Le_Domain + + DOMAIN_CONF=$WORKING_DIR/$Le_Domain/$Le_Domain.conf + if [ -f "$DOMAIN_CONF" ] ; then + source "$DOMAIN_CONF" + if [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then + _info "Skip, Next renwal time is: $Le_NextRenewTimeStr" + return 2 + fi + fi + + if [ -z "$Le_Webroot" ] ; then + echo Usage: $0 webroot a.com [b.com,c.com] [key-length] + return 1 + fi + + createAccountKey $Le_Domain $Le_Keylength + + createDomainKey $Le_Domain $Le_Keylength + + createCSR $Le_Domain $Le_Alt + + pub_exp=$(openssl rsa -in $ACCOUNT_KEY_PATH -noout -text | grep "^publicExponent:"| cut -d '(' -f 2 | cut -d 'x' -f 2 | cut -d ')' -f 1) + if [ "${#pub_exp}" == "5" ] ; then + pub_exp=0$pub_exp + fi + _debug pub_exp "$pub_exp" + + e=$(echo $pub_exp | xxd -r -p | base64) + _debug e "$e" + + modulus=$(openssl rsa -in $ACCOUNT_KEY_PATH -modulus -noout | cut -d '=' -f 2 ) + n=$(echo $modulus| xxd -r -p | base64 | _b64 ) + + jwk='{"e": "'$e'", "kty": "RSA", "n": "'$n'"}' + + HEADER='{"alg": "RS256", "jwk": '$jwk'}' + HEADERPLACE='{"nonce": "NONCE", "alg": "RS256", "jwk": '$jwk'}' + _debug HEADER "$HEADER" + + accountkey_json=$(echo -n "$jwk" | sed "s/ //g") + thumbprint=$(echo -n "$accountkey_json" | sha256sum | xxd -r -p | base64 | _b64) + + + _info "Registering account" + regjson='{"resource": "new-reg", "agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"}' + if [ "$ACCOUNT_EMAIL" ] ; then + regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"}' + fi + _send_signed_request "$API/acme/new-reg" "$regjson" + + if [ "$code" == "" ] || [ "$code" == '201' ] ; then + _info "Registered" + echo $response > $WORKING_DIR/account.json + elif [ "$code" == '409' ] ; then + _info "Already registered" + else + _info "Register account Error." + return 1 + fi + + # verify each domain + _info "verify each domain" + + alldomains=$(echo "$Le_Domain,$Le_Alt" | sed "s/,/ /g") + for d in $alldomains + do + _info "Verifing domain $d" + + _send_signed_request "$API/acme/new-authz" "{\"resource\": \"new-authz\", \"identifier\": {\"type\": \"dns\", \"value\": \"$d\"}}" + + if [ ! -z "$code" ] && [ ! "$code" == '201' ] ; then + _info "new-authz error: $d" + return 1 + fi + + http01=$(echo $response | egrep -o '{[^{]*"type":"http-01"[^}]*') + _debug http01 "$http01" + + token=$(echo "$http01" | sed 's/,/\n'/g| grep '"token":'| cut -d : -f 2|sed 's/"//g') + _info token $token + + uri=$(echo "$http01" | sed 's/,/\n'/g| grep '"uri":'| cut -d : -f 2,3|sed 's/"//g') + _info uri $uri + + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" + + wellknown_path="$Le_Webroot/.well-known/acme-challenge" + _debug wellknown_path "$wellknown_path" + + mkdir -p "$wellknown_path" + wellknown_path="$wellknown_path/$token" + echo -n "$keyauthorization" > $wellknown_path + + wellknown_url="http://$d/.well-known/acme-challenge/$token" + _debug wellknown_url "$wellknown_url" + + _debug challenge "$challenge" + _send_signed_request $uri "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" + + if [ ! -z "$code" ] && [ ! "$code" == '202' ] ; then + _info "challenge error: $d" + return 1 + fi + + while [ "1" ] ; do + _debug "sleep 5 secs to verify" + sleep 5 + _debug "checking" + + if ! _get $uri ; then + _info "verify error:$d" + return 1 + fi + + status=$(echo $response | egrep -o '"status":"[^"]+"' | cut -d : -f 2 | sed 's/"//g') + if [ "$status" == "valid" ] ; then + _info "verify success:$d" + break; + fi + + if [ "$status" == "invalid" ] ; then + error=$(echo $response | egrep -o '"error":{[^}]*}' | grep -o '"detail":"[^"]*"' | cut -d '"' -f 4) + _info "verify error:$d" + _debug $error + return 1; + fi + + if [ "$status" == "pending" ] ; then + _info "verify pending:$d" + else + _info "verify error:$d" + return 1 + fi + + done + done + + _info "verify finished, start to sign." + der=$(openssl req -in $CSR_PATH -outform DER | base64 | _b64) + _send_signed_request "$API/acme/new-cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbas64" + + echo -----BEGIN CERTIFICATE----- > $CERT_PATH + echo $response | sed "s/ /\n/g" >> $CERT_PATH + echo -----END CERTIFICATE----- >> $CERT_PATH + _info "Cert success." + cat $CERT_PATH + + _setopt $DOMAIN_CONF "Le_Domain" "=" "$Le_Domain" + _setopt $DOMAIN_CONF "Le_Alt" "=" "$Le_Alt" + _setopt $DOMAIN_CONF "Le_Webroot" "=" "$Le_Webroot" + _setopt $DOMAIN_CONF "Le_Keylength" "=" "$Le_Keylength" + + + + Le_LinkIssuer=$(grep -i '^Link' $CURL_HEADER | cut -d " " -f 2| cut -d ';' -f 1 | sed 's///g') + _setopt $DOMAIN_CONF "Le_LinkIssuer" "=" "$Le_LinkIssuer" + + Le_LinkCert=$(grep -i '^Location' $CURL_HEADER | cut -d " " -f 2) + _setopt $DOMAIN_CONF "Le_LinkCert" "=" "$Le_LinkCert" + + Le_CertCreateTime=$(date -u "+%s") + _setopt $DOMAIN_CONF "Le_CertCreateTime" "=" "$Le_CertCreateTime" + + Le_CertCreateTimeStr=$(date -u "+%Y-%m-%d %H:%M:%S UTC") + _setopt $DOMAIN_CONF "Le_CertCreateTimeStr" "=" "\"$Le_CertCreateTimeStr\"" + + if [ ! "$Le_RenewalDays" ] ; then + Le_RenewalDays=50 + fi + + _setopt $DOMAIN_CONF "Le_RenewalDays" "=" "$Le_RenewalDays" + + Le_NextRenewTime=$(date -u -d "+$Le_RenewalDays day" "+%s") + _setopt $DOMAIN_CONF "Le_NextRenewTime" "=" "$Le_NextRenewTime" + + Le_NextRenewTimeStr=$(date -u -d "+$Le_RenewalDays day" "+%Y-%m-%d %H:%M:%S UTC") + _setopt $DOMAIN_CONF "Le_NextRenewTimeStr" "=" "\"$Le_NextRenewTimeStr\"" + + _setopt $DOMAIN_CONF "Le_RealCertPath" "=" "\"$Le_RealCertPath\"" + if [ "$Le_RealCertPath" ] ; then + if [ -f "$Le_RealCertPath" ] ; then + rm -f $Le_RealCertPath + fi + ln -s $CERT_PATH $Le_RealCertPath + + fi + + _setopt $DOMAIN_CONF "Le_RealKeyPath" "=" "\"$Le_RealKeyPath\"" + if [ "$Le_RealKeyPath" ] ; then + if [ -f "$Le_RealKeyPath" ] ; then + rm -f $Le_RealKeyPath + fi + ln -s $CERT_KEY_PATH $Le_RealKeyPath + + fi + _setopt $DOMAIN_CONF "Le_ReloadCmd" "=" "\"$Le_ReloadCmd\"" + + if [ "Le_ReloadCmd" ] ; then + _info "Run Le_ReloadCmd: $Le_ReloadCmd" + $Le_ReloadCmd + fi + +} + + + +renew() { + Le_Domain="$1" + if [ -z "$Le_Domain" ] ; then + echo Usage: $0 domain.com + return 1 + fi + + DOMAIN_CONF=$WORKING_DIR/$Le_Domain/$Le_Domain.conf + if [ -f "$DOMAIN_CONF" ] ; then + source "$DOMAIN_CONF" + if [ "$(date -u "+%s" )" -lt "$Le_NextRenewTime" ] ; then + _info "Skip, Next renwal time is: $Le_NextRenewTimeStr" + return 2 + fi + fi + + if [ -z "$Le_Webroot" ] ; then + echo Le_Webroot can not found, please remove the conf file and issue a new cert + return 1 + fi + + issue $Le_Domain + +} + +renewAll() { + _info "renewAll" + for d in $(ls -F $WORKING_DIR | grep '/$') ; do + d=$(echo $d | cut -d '/' -f 1) + _info "renew $d" + renew "$d" + done + +} + +install() { + _initpath + _info "Installing to $WORKING_DIR" + + mkdir -p $WORKING_DIR/ + cp le.sh $WORKING_DIR/ + chmod +x $WORKING_DIR/le.sh + + if [ ! -f /bin/le.sh ] ; then + ln -s $WORKING_DIR/le.sh /bin/le.sh + ln -s $WORKING_DIR/le.sh /bin/le + fi + + _info "Installing cron job" + if ! crontab -l | grep 'le.sh renewAll' ; then + crontab -l | { cat; echo "0 0 * * * le.sh renewAll"; } | crontab - + service cron restart + fi + + + _info OK +} + +uninstall() { + _initpath + _info "Removing cron job" + crontab -l | sed "/le.sh renewAll/d" | crontab - + + _info "Removing /bin/le.sh" + rm -f /bin/le + rm -f /bin/le.sh + + _info "The keys and certs are in $WORKING_DIR, you can remove them by yourself." + +} + + +showhelp() { + echo "Usage: issue|renew|renewAll|createAccountKey|createDomainKey|createCSR|install|uninstall" + +} + + +if [ -z "$1" ] ; then + showhelp +fi + + + +$1 $2 $3 $4 $5 $6 $7 $8 + + + +