]> git.ipfire.org Git - thirdparty/dehydrated.git/commitdiff
implemented rfc 8738 support
authorLukas Schauer <lukas@schauer.dev>
Wed, 6 Apr 2022 20:23:43 +0000 (22:23 +0200)
committerLukas Schauer <lukas@schauer.dev>
Wed, 6 Apr 2022 20:23:43 +0000 (22:23 +0200)
CHANGELOG
dehydrated

index 2329c7ca8c678fa1089a3186ab40929ecb670673..9596b084624bd4ea77fa0ae5b2361aeccf406c73 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -10,6 +10,7 @@ This file contains a log of major changes in dehydrated
 ## Added
 - Implemented EC for account keys
 - Domain list now also read from domains.txt.d subdirectory (behaviour might change, see docs)
+- Implemented RFC 8738 (validating/signing certificates for IP addresses instead of domain names) support (this will not work with most public CAs, if any!)
 
 ## [0.7.0] - 2020-12-10
 ## Added
index fa2341c92f67a02dca4a2a0c7c1c6f046921572e..c54dc56cdee12f9da339f6aa1fff9615cefd3b3f 100755 (executable)
@@ -241,6 +241,17 @@ jsonsh() {
   tokenize | parse
 }
 
+# Convert IP addresses to their reverse dns variants.
+# Used for ALPN certs as validation for IPs uses this in SNI since IPs aren't allowed there.
+ip_to_ptr() {
+  ip="$(cat)"
+  if [[ "${ip}" =~ : ]]; then
+    printf "%sip6.arpa" "$(printf "%s" "${ip}" | awk -F: 'BEGIN {OFS=""; }{addCount = 9 - NF; for(i=1; i<=NF;i++){if(length($i) == 0){ for(j=1;j<=addCount;j++){$i = ($i "0000");} } else { $i = substr(("0000" $i), length($i)+5-4);}}; print}' | rev | sed -e "s/./&./g")"
+  else
+    printf "%s.in-addr.arpa" "$(printf "%s" "${ip}" | awk -F. '{print $4"."$3"." $2"."$1}')"
+  fi
+}
+
 # Create (identifiable) temporary files
 _mktemp() {
   mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
@@ -996,12 +1007,12 @@ extract_altnames() {
     # split to one per line:
     # shellcheck disable=SC1003
     altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
-    # we can only get DNS: ones signed
-    if grep -qEv '^(DNS|othername):' <<<"${altnames}"; then
-      _exiterr "Certificate signing request contains non-DNS Subject Alternative Names"
+    # we can only get DNS/IP: ones signed
+    if grep -qEv '^(DNS|IP( Address)*|othername):' <<<"${altnames}"; then
+      _exiterr "Certificate signing request contains non-DNS/IP Subject Alternative Names"
     fi
-    # strip away the DNS: prefix
-    altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|othername:<unsupported>)//' )"
+    # strip away the DNS/IP: prefix
+    altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|IP( Address)*:|othername:<unsupported>)//' )"
     printf "%s" "${altnames}" | tr '\n' ' '
   else
     # No SANs, extract CN
@@ -1047,7 +1058,11 @@ sign_csr() {
     # Request new order and store authorization URIs
     local challenge_identifiers=""
     for altname in ${altnames}; do
-      challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
+    if [[ "${altname}" =~ ^ip: ]]; then
+      challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${altname:3}")"
+    else
+       challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
+    fi
     done
     challenge_identifiers="[${challenge_identifiers%, }]"
 
@@ -1080,6 +1095,7 @@ sign_csr() {
       # Receive authorization ($authorization is authz uri)
       response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)"
       identifier="$(echo "${response}" | get_json_string_value -p '"identifier","value"')"
+      identifier_type="$(echo "${response}" | get_json_string_value -p '"identifier","type"')"
       echo " + Handling authorization for ${identifier}"
     else
       # Request new authorization ($authorization is altname)
@@ -1108,7 +1124,11 @@ sign_csr() {
     challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
 
     # Gather challenge information
-    challenge_names[${idx}]="${identifier}"
+    if [ "${identifier_type:-}" = "ip" ]; then
+      challenge_names[${idx}]="$(echo "${identifier}" | ip_to_ptr)"
+    else
+      challenge_names[${idx}]="${identifier}"
+    fi
     challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)"
 
     if [[ ${API} -eq 2 ]]; then
@@ -1136,12 +1156,16 @@ sign_csr() {
         ;;
       "tls-alpn-01")
         keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')"
-        generate_alpn_certificate "${identifier}" "${keyauth_hook}"
+        generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}"
         ;;
     esac
 
     keyauths[${idx}]="${keyauth}"
-    deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}"
+    if [ "${identifier_type:-}" = "ip" ]; then
+      deploy_args[${idx}]="$(echo "${identifier}" | ip_to_ptr) ${challenge_tokens[${idx}]} ${keyauth_hook}"
+    else
+      deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}"
+    fi
 
     idx=$((idx+1))
   done
@@ -1353,7 +1377,8 @@ walk_chain() {
 # Generate ALPN verification certificate
 generate_alpn_certificate() {
   local altname="${1}"
-  local acmevalidation="${2}"
+  local identifier_type="${2}"
+  local acmevalidation="${3}"
 
   local alpncertdir="${ALPNCERTDIR}"
   if [[ ! -e "${alpncertdir}" ]]; then
@@ -1364,10 +1389,17 @@ generate_alpn_certificate() {
   echo " + Generating ALPN certificate and key for ${1}..."
   tmp_openssl_cnf="$(_mktemp)"
   cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
-  printf "\n[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
+  if [[ "${identifier_type}" = "ip" ]]; then
+    printf "\n[SAN]\nsubjectAltName=IP:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
+  else
+    printf "\n[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
+  fi
   printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:%s\n" "${acmevalidation}" >> "${tmp_openssl_cnf}"
   SUBJ="/CN=${altname}/"
   [[ "${OSTYPE:0:5}" = "MINGW" ]] && SUBJ="/${SUBJ}"
+  if [[ "${identifier_type}" = "ip" ]]; then
+    altname="$(echo "${altname}" | ip_to_ptr)"
+  fi
   _openssl req -x509 -new -sha256 -nodes -newkey rsa:2048 -keyout "${alpncertdir}/${altname}.key.pem" -out "${alpncertdir}/${altname}.crt.pem" -subj "${SUBJ}" -extensions SAN -config "${tmp_openssl_cnf}"
   chmod g+r "${alpncertdir}/${altname}.key.pem" "${alpncertdir}/${altname}.crt.pem"
   rm -f "${tmp_openssl_cnf}"
@@ -1433,7 +1465,13 @@ sign_domain() {
     echo " + Generating signing request..."
     SAN=""
     for altname in ${altnames}; do
-      SAN="${SAN}DNS:${altname}, "
+      if [[ "${altname}" =~ ^ip: ]]; then
+        SAN="${SAN}IP:${altname:3}, "
+        SUBJ="/CN=${domain:3}/"
+      else
+        SAN="${SAN}DNS:${altname}, "
+        SUBJ="/CN=${domain}/"
+      fi
     done
     SAN="${SAN%%, }"
     local tmp_openssl_cnf
@@ -1443,7 +1481,6 @@ sign_domain() {
     if [ "${OCSP_MUST_STAPLE}" = "yes" ]; then
       printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}"
     fi
-    SUBJ="/CN=${domain}/"
     if [[ "${OSTYPE:0:5}" = "MINGW" ]]; then
       # The subject starts with a /, so MSYS will assume it's a path and convert
       # it unless we escape it with another one:
@@ -1754,9 +1791,9 @@ command_sign_domains() {
     if [[ -e "${cert}" && "${force_renew}" = "no" ]]; then
       printf " + Checking domain name(s) of existing cert..."
 
-      certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep DNS: | _sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
-      givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//' | _sed 's/^ //')"
-
+      certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep -E '(DNS|IP( Address*)):' | _sed 's/(DNS|IP( Address)*)://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
+      givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')"
       if [[ "${certnames}" = "${givennames}" ]]; then
         echo " unchanged."
       else