]> git.ipfire.org Git - thirdparty/dehydrated.git/commitdiff
EAB + ZeroSSL support
authorLukas Schauer <lukas@schauer.so>
Mon, 14 Sep 2020 16:22:36 +0000 (18:22 +0200)
committerLukas Schauer <lukas@schauer.so>
Mon, 14 Sep 2020 16:22:36 +0000 (18:22 +0200)
dehydrated

index cc1e115be80a825e1d8b3a753b2145aa6992424d..35183f6c68c24fc1c342822dfc3389d5d927cee9 100755 (executable)
@@ -236,7 +236,7 @@ _mktemp() {
 # Check for script dependencies
 check_dependencies() {
   # look for required binaries
-  for binary in grep mktemp diff sed awk curl cut; do 
+  for binary in grep mktemp diff sed awk curl cut; do
     bin_path="$(command -v "${binary}" 2>/dev/null)" || _exiterr "This script requires ${binary}."
     [[ -x "${bin_path}" ]] || _exiterr "${binary} found in PATH but it's not executable"
   done
@@ -318,7 +318,7 @@ load_config() {
   fi
 
   # Preset
-  CA_ZEROSSL=""
+  CA_ZEROSSL="https://acme.zerossl.com/v2/DV90"
   CA_LETSENCRYPT="https://acme-v02.api.letsencrypt.org/directory"
 
   # Default values
@@ -424,6 +424,11 @@ load_config() {
   # Check BASEDIR and set default variables
   [[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
 
+  # Check for ca cli parameter
+  if [ -n "${PARAM_CA:-}" ]; then
+    CA="${PARAM_CA}"
+  fi
+
   # Preset CAs
   if [ "${CA}" = "letsencrypt" ]; then
     CA="{$CA_LETSENCRYPT}"
@@ -527,7 +532,8 @@ init_system() {
     CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" &&
     CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" &&
     CA_NEW_ACCOUNT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newAccount)" &&
-    CA_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value termsOfService)" &&
+    CA_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value -p '"meta","termsOfService"')" &&
+    CA_REQUIRES_EAB="$(printf "%s" "${CA_DIRECTORY}" | get_json_bool_value -p '"meta","externalAccountRequired"' || echo false)" &&
     CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revokeCert)" ||
     _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
     # Since acct URI is missing from directory we will assume it is the same as CA_NEW_ACCOUNT without the new part
@@ -539,6 +545,7 @@ init_system() {
 
   # Checking for private key ...
   register_new_key="no"
+  generated="false"
   if [[ -n "${PARAM_ACCOUNT_KEY:-}" ]]; then
     # a private key was specified from the command line so use it for this run
     echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key"
@@ -557,6 +564,7 @@ init_system() {
       fi
 
       echo "+ Generating account key..."
+      generated="true"
       local tmp_account_key="$(_mktemp)"
       _openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}"
       cat "${tmp_account_key}" > "${ACCOUNT_KEY}"
@@ -582,6 +590,35 @@ init_system() {
       FAILED=true
     fi
 
+    # ZeroSSL special sauce
+    if [[ "${CA}" = "${CA_ZEROSSL}" ]]; then
+      if [[ -z "${EAB_KID:-}" ]] ||  [[ -z "${EAB_HMAC_KEY:-}" ]]; then
+        if [[ -z "${CONTACT_EMAIL}" ]]; then
+          echo "ZeroSSL requires contact email to be set or EAB_KID/EAB_HMAC_KEY to be manually configured"
+          FAILED=true
+        else
+          zeroapi="$(curl -s "https://api.zerossl.com/acme/eab-credentials-email" -d "email=${CONTACT_EMAIL}" | jsonsh)"
+          EAB_KID="$(printf "%s" "${zeroapi}" | get_json_string_value eab_kid)"
+          EAB_HMAC_KEY="$(printf "%s" "${zeroapi}" | get_json_string_value eab_hmac_key)"
+          if [[ -z "${EAB_KID:-}" ]] ||  [[ -z "${EAB_HMAC_KEY:-}" ]]; then
+            echo "Unknown error retrieving ZeroSSL API credentials"
+            echo "${zeroapi}"
+            FAILED=true
+          fi
+        fi
+      fi
+    fi
+
+    # Check if external account is required
+    if [[ "${FAILED}" = "false" ]]; then
+      if [[ "${CA_REQUIRES_EAB}" = "true" ]]; then
+        if [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then
+          FAILED=true
+          echo "This CA requires an external account but no EAB_KID/EAB_HMAC_KEY has been configured"
+        fi
+      fi
+    fi
+
     # If an email for the contact has been provided then adding it to the registration request
     if [[ "${FAILED}" = "false" ]]; then
       if [[ ${API} -eq 1 ]]; then
@@ -591,11 +628,26 @@ init_system() {
           (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"${CA_TERMS}"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
         fi
       else
-        if [[ -n "${CONTACT_EMAIL}" ]]; then
-          (signed_request "${CA_NEW_ACCOUNT}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+        if [[ -n "${EAB_KID:-}" ]] && [[ -n "${EAB_HMAC_KEY:-}" ]]; then
+          eab_url="${CA_NEW_ACCOUNT}"
+          eab_protected64="$(printf '{"alg":"HS256","kid":"%s","url":"%s"}' "${EAB_KID}" "${eab_url}" | urlbase64)"
+          eab_payload64="$(printf "%s" '{"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}' | urlbase64)"
+          eab_key="$(printf "%s" "${EAB_HMAC_KEY}" | deurlbase64)"
+          eab_signed64="$(printf '%s' "${eab_protected64}.${eab_payload64}" | "${OPENSSL}" dgst -binary -sha256 -hmac "${eab_key}" | urlbase64)"
+
+          if [[ -n "${CONTACT_EMAIL}" ]]; then
+            regjson='{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true, "externalAccountBinding": {"protected": "'"${eab_protected64}"'", "payload": "'"${eab_payload64}"'", "signature": "'"${eab_signed64}"'"}}'
+          else
+            regjson='{"termsOfServiceAgreed": true, "externalAccountBinding": {"protected": "'"${eab_protected64}"'", "payload": "'"${eab_payload64}"'", "signature": "'"${eab_signed64}"'"}}'
+          fi
         else
-          (signed_request "${CA_NEW_ACCOUNT}" '{"termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+          if [[ -n "${CONTACT_EMAIL}" ]]; then
+            regjson='{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}'
+          else
+            regjson='{"termsOfServiceAgreed": true}'
+          fi
         fi
+        (signed_request "${CA_NEW_ACCOUNT}" "${regjson}" > "${ACCOUNT_KEY_JSON}") || FAILED=true
       fi
     fi
 
@@ -603,7 +655,10 @@ init_system() {
       echo >&2
       echo >&2
       echo "Error registering account key. See message above for more information." >&2
-      rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}"
+      if [[ "${generated}" = "true" ]]; then
+        rm "${ACCOUNT_KEY}"
+      fi
+      rm -f "${ACCOUNT_KEY_JSON}"
       exit 1
     fi
   elif [[ "${COMMAND:-}" = "register" ]]; then
@@ -669,12 +724,27 @@ urlbase64() {
   "${OPENSSL}" base64 -e | tr -d '\n\r' | _sed -e 's:=*$::g' -e 'y:+/:-_:'
 }
 
+# Decode data from url-safe formatted base64
+deurlbase64() {
+  data="$(cat | tr -d ' \n\r')"
+  modlen=$((${#data} % 4))
+  padding=""
+  if [[ "${modlen}" = "2" ]]; then padding="==";
+  elif [[ "${modlen}" = "3" ]]; then padding="="; fi
+  printf "%s%s" "${data}" "${padding}" | tr -d '\n\r' | _sed -e 'y:-_:+/:' | "${OPENSSL}" base64 -d -A
+}
+
 # Convert hex string to binary data
 hex2bin() {
   # Remove spaces, add leading zero, escape as hex string and parse with printf
   printf -- "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
 }
 
+# Convert binary data to hex string
+bin2hex() {
+  hexdump | _sed 's/^[^ ]*//' | tr -d ' \n\r'
+}
+
 # OpenSSL writes to stderr/stdout even when there are no errors. So just
 # display the output if the exit code was != 0 to simplify debugging.
 _openssl() {
@@ -1873,6 +1943,15 @@ main() {
          fi
         ;;
 
+      # PARAM_Usage: --ca url/preset
+      # PARAM_Description: Use specified CA URL or preset
+      --ca)
+        shift 1
+        check_parameters "${1:-}"
+        [[ -n "${PARAM_CA:-}" ]] && _exiterr "CA can only be specified once!"
+        PARAM_CA="${1}"
+        ;;
+
       # PARAM_Usage: --alias certalias
       # PARAM_Description: Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified)
       --alias)