]> git.ipfire.org Git - thirdparty/dehydrated.git/commitdiff
ACME v02 Support
authorMartin Strobel <arctus@crza.de>
Mon, 8 Jan 2018 11:38:01 +0000 (12:38 +0100)
committerLukas Schauer <lukas@schauer.so>
Sat, 13 Jan 2018 19:17:25 +0000 (20:17 +0100)
CHANGELOG
dehydrated
docs/examples/config

index 7de2c4f42eacc045c7ae9034dc21a0916f20f674..304576a0efc62707a4fe9e22ef41dfc0b3d94304 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,7 +6,7 @@ This file contains a log of major changes in dehydrated
 - ...
 
 ## Added
-- ...
+- Support for ACME v02
 
 ## [0.5.0] - 2018-01-13
 ## Changed
index c2af61c168b3c1247aafe98fb8ff583d6d73d1d0..00dd3213c18c057429668e0b8a111a4a75d98766 100755 (executable)
@@ -138,6 +138,7 @@ load_config() {
   AUTO_CLEANUP="no"
   DEHYDRATED_USER=
   DEHYDRATED_GROUP=
+  API=1
 
   if [[ -z "${CONFIG:-}" ]]; then
     echo "#" >&2
@@ -256,14 +257,25 @@ init_system() {
 
   # Get CA URLs
   CA_DIRECTORY="$(http_request get "${CA}")"
-  CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" &&
-  CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" &&
-  CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" &&
-  # shellcheck disable=SC2015
-  CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" ||
-  _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
-  # Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part
-  CA_REG=${CA_NEW_REG/new-reg/reg}
+  if [[ ${API} -eq 1 ]]; then
+    # shellcheck disable=SC2015
+    CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" &&
+    CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" &&
+    CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" &&
+    CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" ||
+    _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
+    # Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part
+    CA_REG=${CA_NEW_REG/new-reg/reg}
+  else
+    # shellcheck disable=SC2015
+    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_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
+    CA_ACCOUNT=${CA_NEW_ACCOUNT/new-acct/acct}
+  fi
 
   # Export some environment variables to be used in hook script
   export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND
@@ -314,17 +326,25 @@ init_system() {
     echo "+ Registering account key with ACME server..."
     FAILED=false
 
-    if [[ -z "${CA_NEW_REG}" ]]; then
+    if [[ ${API} -eq 1 && -z "${CA_NEW_REG}" ]] || [[ ${API} -eq 2 && -z "${CA_NEW_ACCOUNT}" ]]; then
       echo "Certificate authority doesn't allow registrations."
       FAILED=true
     fi
 
     # If an email for the contact has been provided then adding it to the registration request
     if [[ "${FAILED}" = "false" ]]; then
-      if [[ -n "${CONTACT_EMAIL}" ]]; then
-        (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+      if [[ ${API} -eq 1 ]]; then
+        if [[ -n "${CONTACT_EMAIL}" ]]; then
+          (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+        else
+          (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+        fi
       else
-        (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+        if [[ -n "${CONTACT_EMAIL}" ]]; then
+          (signed_request "${CA_NEW_ACCOUNT}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+        else
+          (signed_request "${CA_NEW_ACCOUNT}" '{"termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+        fi
       fi
     fi
 
@@ -380,6 +400,13 @@ get_json_string_value() {
   sed -n "${filter}"
 }
 
+# Get array value from json dictionary
+get_json_array_value() {
+  local filter
+  filter=$(printf 's/.*"%s": *\\[\([^]]*\)\\].*/\\1/p' "$1")
+  sed -n "${filter}"
+}
+
 # Get integer value from json
 get_json_int_value() {
   local filter
@@ -482,20 +509,40 @@ signed_request() {
   payload64="$(printf '%s' "${2}" | urlbase64)"
 
   # Retrieve nonce from acme-server
-  nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
+  if [[ ${API} -eq 1 ]]; then
+    nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
+  else
+    nonce="$(http_request head "${CA_NEW_NONCE}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
+  fi
 
   # Build header with just our public key and algorithm information
   header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
 
-  # Build another header which also contains the previously received nonce and encode it as urlbase64
-  protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}'
-  protected64="$(printf '%s' "${protected}" | urlbase64)"
+  if [[ ${API} -eq 1 ]]; then
+    # Build another header which also contains the previously received nonce and encode it as urlbase64
+    protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}'
+    protected64="$(printf '%s' "${protected}" | urlbase64)"
+  else
+    # Build another header which also contains the previously received nonce and url and encode it as urlbase64
+    if [[ -e "${ACCOUNT_KEY_JSON}" ]] && [[ -n "$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" ]]; then
+      REG_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)"
+      protected='{"alg": "RS256", "kid": "'"${CA_ACCOUNT}/${REG_ID}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
+    else
+      protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
+    fi
+    protected64="$(printf '%s' "${protected}" | urlbase64)"
+  fi
 
   # Sign header with nonce and our payload with our private key and encode signature as urlbase64
   signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)"
 
-  # Send header + extended header + payload + signature to the acme-server
-  data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
+  if [[ ${API} -eq 1 ]]; then
+    # Send header + extended header + payload + signature to the acme-server
+    data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
+  else
+    # Send extended header + payload + signature to the acme-server
+    data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
+  fi
 
   http_request post "${1}" "${data}"
 }
@@ -548,22 +595,54 @@ sign_csr() {
   fi
   export altnames
 
-  if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
+  if [[ ${API} -eq 1 ]]; then
+    if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
+      _exiterr "Certificate authority doesn't allow certificate signing"
+    fi
+  elif [[ ${API} -eq 2 ]] && [[ -z "${CA_NEW_ORDER}" ]]; then
     _exiterr "Certificate authority doesn't allow certificate signing"
   fi
 
-  local idx=0
   if [[ -n "${ZSH_VERSION:-}" ]]; then
-    local -A challenge_altnames challenge_uris challenge_tokens keyauths deploy_args
+    local -A challenge_altnames challenge_uris challenge_tokens keyauths deploy_args authorization
   else
-    local -a challenge_altnames challenge_uris challenge_tokens keyauths deploy_args
+    local -a challenge_altnames challenge_uris challenge_tokens keyauths deploy_args authorization
+  fi
+
+  if  [[ ${API} -eq 2 ]]; then
+    # APIv2
+    for altname in ${altnames}; do
+      challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
+    done
+    challenge_identifiers="[${challenge_identifiers%, }]"
+
+    echo " + Requesting challenges for ${altnames}..."
+    result="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}')"
+
+    authorizations="$(echo ${result} | get_json_array_value authorizations)"
+    finalize="$(echo "${result}" | get_json_string_value finalize)"
+
+    local idx=0
+    for uris in ${authorizations}; do
+      authorization[${idx}]="${uris}"
+      idx=$((idx+1))
+    done
+
+    unset challenge_identifiers authorizations
   fi
 
+  local idx=0; idy=-1
   # Request challenges
   for altname in ${altnames}; do
-    # Ask the acme-server for new challenge token and extract them from the resulting json block
-    echo " + Requesting challenge for ${altname}..."
-    response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}' | clean_json)"
+    idy=$((idy+1))
+    if [[ ${API} -eq 1 ]]; then
+      # Ask the acme-server for new challenge token and extract them from the resulting json block
+      echo " + Requesting challenge for ${altname}..."
+      response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}' | clean_json)"
+    else
+      uris="$(<<<"${authorization[${idy}]}" _sed -e 's/\"(.*)".*/\1/')"
+      response="$(http_request get "${uris}" | clean_json)"
+    fi
 
     challenge_status="$(printf '%s' "${response}" | rm_json_arrays | get_json_string_value status)"
     if [ "${challenge_status}" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
@@ -575,7 +654,11 @@ sign_csr() {
     repl=$'\n''{' # fix syntax highlighting in Vim
     challenge="$(printf "%s" "${challenges//\{/${repl}}" | grep \""${CHALLENGETYPE}"\")"
     challenge_token="$(printf '%s' "${challenge}" | get_json_string_value token | _sed 's/[^A-Za-z0-9_\-]/_/g')"
-    challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)"
+    if [[ ${API} -eq 1 ]]; then
+      challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)"
+    else
+      challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value url)"
+   fi
 
     if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then
       _exiterr "Can't retrieve challenges (${response})"
@@ -627,13 +710,21 @@ sign_csr() {
 
       # Ask the acme-server to verify our challenge and wait until it is no longer pending
       echo " + Responding to challenge for ${altname}..."
-      result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
+      if [[ ${API} -eq 1 ]]; then
+        result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
+      else
+        result="$(signed_request "${challenge_uris[${idx}]}" '{"keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
+      fi
 
       reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
 
       while [[ "${reqstatus}" = "pending" ]]; do
         sleep 1
-        result="$(http_request get "${challenge_uris[${idx}]}")"
+        if [[ ${API} -eq 1 ]]; then
+          result="$(http_request get "${challenge_uris[${idx}]}")"
+        else
+          result="$(http_request get "${challenge_uris[${idx}]}")"
+        fi
         reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
       done
 
@@ -676,8 +767,13 @@ sign_csr() {
   # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem
   echo " + Requesting certificate..."
   csr64="$( <<<"${csr}" "${OPENSSL}" req -config "${OPENSSL_CNF}" -outform DER | urlbase64)"
-  crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)"
-  crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
+  if [[ ${API} -eq 1 ]]; then
+    crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)"
+    crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
+  else
+    result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | clean_json | get_json_string_value certificate)"
+    crt="$(http_request get "${result}")"
+  fi
 
   # Try to load the certificate to detect corruption
   echo " + Checking certificate..."
@@ -755,7 +851,11 @@ sign_domain() {
   export altnames
 
   echo " + Signing domains..."
-  if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
+  if [[ ${API} -eq 1 ]]; then
+    if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
+      _exiterr "Certificate authority doesn't allow certificate signing"
+    fi
+  elif [[ ${API} -eq 2 ]] && [[ -z "${CA_NEW_ORDER}" ]]; then
     _exiterr "Certificate authority doesn't allow certificate signing"
   fi
 
@@ -917,11 +1017,20 @@ command_account() {
   fi
 
   echo "+ Updating registration id: ${REG_ID} contact information..."
-  # If an email for the contact has been provided then adding it to the registered account
-  if [[ -n "${CONTACT_EMAIL}" ]]; then
-    (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true
+  if [[ ${API} -eq 1 ]]; then
+    # If an email for the contact has been provided then adding it to the registered account
+    if [[ -n "${CONTACT_EMAIL}" ]]; then
+      (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true
+    else
+      (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true
+    fi
   else
-    (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true
+    # If an email for the contact has been provided then adding it to the registered account
+    if [[ -n "${CONTACT_EMAIL}" ]]; then
+      (signed_request "${CA_ACCOUNT}"/"${REG_ID}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true
+    else
+      (signed_request "${CA_ACCOUNT}"/"${REG_ID}" '{"contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true
+    fi
   fi
 
   if [[ "${FAILED}" = "true" ]]; then
@@ -966,7 +1075,7 @@ command_sign_domains() {
   # Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
   ORIGIFS="${IFS}"
   IFS=$'\n'
-  for line in $(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | (grep -vE '^(#|$)' || true)); do
+  for line in "$(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | (grep -vE '^(#|$)' || true))"; do
     reset_configvars
     IFS="${ORIGIFS}"
     alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)"
@@ -1194,7 +1303,11 @@ command_revoke() {
   echo "Revoking ${cert}"
 
   cert64="$("${OPENSSL}" x509 -in "${cert}" -inform PEM -outform DER | urlbase64)"
-  response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)"
+  if [[ ${API} -eq 1 ]]; then
+    response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)"
+  else
+    response="$(signed_request "${CA_REVOKE_CERT}" '{"certificate": "'"${cert64}"'"}' | clean_json)"
+  fi
   # if there is a problem with our revoke request _request (via signed_request) will report this and "exit 1" out
   # so if we are here, it is safe to assume the request was successful
   echo " + Done."
@@ -1303,6 +1416,7 @@ main() {
     fi
   }
 
+  # shellcheck disable=SC2199
   [[ -z "${@}" ]] && eval set -- "--help"
 
   while (( ${#} )); do
index 7132a68538ec3d19f34a69da157874b84d567fc9..a14abaf1e5b17ce1b0d79291fe92ca306e68334d 100644 (file)
 
 # Automatic cleanup (default: no)
 #AUTO_CLEANUP="no"
+
+# ACME API version (default: 1)
+#API=1