Requests Let's Encrypt certificates using a HTTP-01 challenge
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dehydrated 44KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271
  1. #!/usr/bin/env bash
  2. # dehydrated by lukas2511
  3. # Source: https://github.com/lukas2511/dehydrated
  4. #
  5. # This script is licensed under The MIT License (see LICENSE for more information).
  6. set -e
  7. set -u
  8. set -o pipefail
  9. [[ -n "${ZSH_VERSION:-}" ]] && set -o SH_WORD_SPLIT && set +o FUNCTION_ARGZERO
  10. umask 077 # paranoid umask, we're creating private keys
  11. # Find directory in which this script is stored by traversing all symbolic links
  12. SOURCE="${0}"
  13. while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  14. DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
  15. SOURCE="$(readlink "$SOURCE")"
  16. [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  17. done
  18. SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
  19. BASEDIR="${SCRIPTDIR}"
  20. # Create (identifiable) temporary files
  21. _mktemp() {
  22. # shellcheck disable=SC2068
  23. mktemp ${@:-} "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
  24. }
  25. # Check for script dependencies
  26. check_dependencies() {
  27. # just execute some dummy and/or version commands to see if required tools exist and are actually usable
  28. openssl version > /dev/null 2>&1 || _exiterr "This script requires an openssl binary."
  29. _sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions."
  30. command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep."
  31. command -v mktemp > /dev/null 2>&1 || _exiterr "This script requires mktemp."
  32. command -v diff > /dev/null 2>&1 || _exiterr "This script requires diff."
  33. # curl returns with an error code in some ancient versions so we have to catch that
  34. set +e
  35. curl -V > /dev/null 2>&1
  36. retcode="$?"
  37. set -e
  38. if [[ ! "${retcode}" = "0" ]] && [[ ! "${retcode}" = "2" ]]; then
  39. _exiterr "This script requires curl."
  40. fi
  41. }
  42. store_configvars() {
  43. __KEY_ALGO="${KEY_ALGO}"
  44. __OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
  45. __PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
  46. __KEYSIZE="${KEYSIZE}"
  47. __CHALLENGETYPE="${CHALLENGETYPE}"
  48. __HOOK="${HOOK}"
  49. __WELLKNOWN="${WELLKNOWN}"
  50. __HOOK_CHAIN="${HOOK_CHAIN}"
  51. __OPENSSL_CNF="${OPENSSL_CNF}"
  52. __RENEW_DAYS="${RENEW_DAYS}"
  53. __IP_VERSION="${IP_VERSION}"
  54. }
  55. reset_configvars() {
  56. KEY_ALGO="${__KEY_ALGO}"
  57. OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
  58. PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
  59. KEYSIZE="${__KEYSIZE}"
  60. CHALLENGETYPE="${__CHALLENGETYPE}"
  61. HOOK="${__HOOK}"
  62. WELLKNOWN="${__WELLKNOWN}"
  63. HOOK_CHAIN="${__HOOK_CHAIN}"
  64. OPENSSL_CNF="${__OPENSSL_CNF}"
  65. RENEW_DAYS="${__RENEW_DAYS}"
  66. IP_VERSION="${__IP_VERSION}"
  67. }
  68. # verify configuration values
  69. verify_config() {
  70. [[ "${CHALLENGETYPE}" =~ (http-01|dns-01) ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... can not continue."
  71. if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
  72. _exiterr "Challenge type dns-01 needs a hook script for deployment... can not continue."
  73. fi
  74. if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
  75. _exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
  76. fi
  77. [[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue."
  78. if [[ -n "${IP_VERSION}" ]]; then
  79. [[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... can not continue."
  80. fi
  81. }
  82. # Setup default config values, search for and load configuration files
  83. load_config() {
  84. # Check for config in various locations
  85. if [[ -z "${CONFIG:-}" ]]; then
  86. for check_config in "/etc/dehydrated" "/usr/local/etc/dehydrated" "${PWD}" "${SCRIPTDIR}"; do
  87. if [[ -f "${check_config}/config" ]]; then
  88. BASEDIR="${check_config}"
  89. CONFIG="${check_config}/config"
  90. break
  91. fi
  92. done
  93. fi
  94. # Default values
  95. CA="https://acme-v01.api.letsencrypt.org/directory"
  96. CA_TERMS="https://acme-v01.api.letsencrypt.org/terms"
  97. LICENSE=
  98. CERTDIR=
  99. ACCOUNTDIR=
  100. CHALLENGETYPE="http-01"
  101. CONFIG_D=
  102. DOMAINS_D=
  103. DOMAINS_TXT=
  104. HOOK=
  105. HOOK_CHAIN="no"
  106. RENEW_DAYS="30"
  107. KEYSIZE="4096"
  108. WELLKNOWN=
  109. PRIVATE_KEY_RENEW="yes"
  110. PRIVATE_KEY_ROLLOVER="no"
  111. KEY_ALGO=rsa
  112. OPENSSL_CNF="$(openssl version -d | cut -d\" -f2)/openssl.cnf"
  113. CONTACT_EMAIL=
  114. LOCKFILE=
  115. OCSP_MUST_STAPLE="no"
  116. IP_VERSION=
  117. if [[ -z "${CONFIG:-}" ]]; then
  118. echo "#" >&2
  119. echo "# !! WARNING !! No main config file found, using default config!" >&2
  120. echo "#" >&2
  121. elif [[ -f "${CONFIG}" ]]; then
  122. echo "# INFO: Using main config file ${CONFIG}"
  123. BASEDIR="$(dirname "${CONFIG}")"
  124. # shellcheck disable=SC1090
  125. . "${CONFIG}"
  126. else
  127. _exiterr "Specified config file doesn't exist."
  128. fi
  129. if [[ -n "${CONFIG_D}" ]]; then
  130. if [[ ! -d "${CONFIG_D}" ]]; then
  131. _exiterr "The path ${CONFIG_D} specified for CONFIG_D does not point to a directory." >&2
  132. fi
  133. for check_config_d in "${CONFIG_D}"/*.sh; do
  134. if [[ ! -e "${check_config_d}" ]]; then
  135. echo "# !! WARNING !! Extra configuration directory ${CONFIG_D} exists, but no configuration found in it." >&2
  136. break
  137. elif [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then
  138. echo "# INFO: Using additional config file ${check_config_d}"
  139. # shellcheck disable=SC1090
  140. . "${check_config_d}"
  141. else
  142. _exiterr "Specified additional config ${check_config_d} is not readable or not a file at all." >&2
  143. fi
  144. done
  145. fi
  146. # Remove slash from end of BASEDIR. Mostly for cleaner outputs, doesn't change functionality.
  147. BASEDIR="${BASEDIR%%/}"
  148. # Check BASEDIR and set default variables
  149. [[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
  150. CAHASH="$(echo "${CA}" | urlbase64)"
  151. [[ -z "${ACCOUNTDIR}" ]] && ACCOUNTDIR="${BASEDIR}/accounts"
  152. mkdir -p "${ACCOUNTDIR}/${CAHASH}"
  153. [[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
  154. ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
  155. ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
  156. if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then
  157. echo "! Moving private_key.pem to ${ACCOUNT_KEY}"
  158. mv "${BASEDIR}/private_key.pem" "${ACCOUNT_KEY}"
  159. fi
  160. if [[ -f "${BASEDIR}/private_key.json" ]] && [[ ! -f "${ACCOUNT_KEY_JSON}" ]]; then
  161. echo "! Moving private_key.json to ${ACCOUNT_KEY_JSON}"
  162. mv "${BASEDIR}/private_key.json" "${ACCOUNT_KEY_JSON}"
  163. fi
  164. [[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs"
  165. [[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt"
  166. [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/dehydrated"
  167. [[ -z "${LOCKFILE}" ]] && LOCKFILE="${BASEDIR}/lock"
  168. [[ -n "${PARAM_LOCKFILE_SUFFIX:-}" ]] && LOCKFILE="${LOCKFILE}-${PARAM_LOCKFILE_SUFFIX}"
  169. [[ -n "${PARAM_NO_LOCK:-}" ]] && LOCKFILE=""
  170. [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
  171. [[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}"
  172. [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
  173. [[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
  174. [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
  175. [[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
  176. verify_config
  177. store_configvars
  178. }
  179. # Initialize system
  180. init_system() {
  181. load_config
  182. # Lockfile handling (prevents concurrent access)
  183. if [[ -n "${LOCKFILE}" ]]; then
  184. LOCKDIR="$(dirname "${LOCKFILE}")"
  185. [[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting."
  186. ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting."
  187. remove_lock() { rm -f "${LOCKFILE}"; }
  188. trap 'remove_lock' EXIT
  189. fi
  190. # Get CA URLs
  191. CA_DIRECTORY="$(http_request get "${CA}")"
  192. CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" &&
  193. CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" &&
  194. CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" &&
  195. # shellcheck disable=SC2015
  196. CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" ||
  197. _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
  198. # Export some environment variables to be used in hook script
  199. export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND
  200. # Checking for private key ...
  201. register_new_key="no"
  202. if [[ -n "${PARAM_ACCOUNT_KEY:-}" ]]; then
  203. # a private key was specified from the command line so use it for this run
  204. echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key"
  205. ACCOUNT_KEY="${PARAM_ACCOUNT_KEY}"
  206. ACCOUNT_KEY_JSON="${PARAM_ACCOUNT_KEY}.json"
  207. else
  208. # Check if private account key exists, if it doesn't exist yet generate a new one (rsa key)
  209. if [[ ! -e "${ACCOUNT_KEY}" ]]; then
  210. REAL_LICENSE="$(http_request head "${CA_TERMS}" | (grep Location: || true) | awk -F ': ' '{print $2}' | tr -d '\n\r')"
  211. if [[ -z "${REAL_LICENSE}" ]]; then
  212. printf '\n'
  213. printf 'Error retrieving terms of service from certificate authority.\n'
  214. printf 'Please set LICENSE in config manually.\n'
  215. exit 1
  216. fi
  217. if [[ ! "${LICENSE}" = "${REAL_LICENSE}" ]]; then
  218. if [[ "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then
  219. LICENSE="${REAL_LICENSE}"
  220. else
  221. printf '\n'
  222. printf 'To use dehydrated with this certificate authority you have to agree to their terms of service which you can find here: %s\n\n' "${REAL_LICENSE}"
  223. printf 'To accept these terms of service run `%s --register --accept-terms`.\n' "${0}"
  224. exit 1
  225. fi
  226. fi
  227. echo "+ Generating account key..."
  228. _openssl genrsa -out "${ACCOUNT_KEY}" "${KEYSIZE}"
  229. register_new_key="yes"
  230. fi
  231. fi
  232. openssl rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null || _exiterr "Account key is not valid, can not continue."
  233. # Get public components from private key and calculate thumbprint
  234. pubExponent64="$(printf '%x' "$(openssl rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
  235. pubMod64="$(openssl rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
  236. thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | openssl dgst -sha256 -binary | urlbase64)"
  237. # If we generated a new private key in the step above we have to register it with the acme-server
  238. if [[ "${register_new_key}" = "yes" ]]; then
  239. echo "+ Registering account key with ACME server..."
  240. FAILED=false
  241. if [[ -z "${CA_NEW_REG}" ]]; then
  242. echo "Certificate authority doesn't allow registrations."
  243. FAILED=true
  244. fi
  245. # If an email for the contact has been provided then adding it to the registration request
  246. if [[ "${FAILED}" = "false" ]]; then
  247. if [[ -n "${CONTACT_EMAIL}" ]]; then
  248. (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
  249. else
  250. (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
  251. fi
  252. fi
  253. if [[ "${FAILED}" = "true" ]]; then
  254. echo
  255. echo
  256. echo "Error registering account key. See message above for more information."
  257. rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}"
  258. exit 1
  259. fi
  260. elif [[ "${COMMAND:-}" = "register" ]]; then
  261. echo "+ Account already registered!"
  262. exit 0
  263. fi
  264. }
  265. # Different sed version for different os types...
  266. _sed() {
  267. if [[ "${OSTYPE}" = "Linux" ]]; then
  268. sed -r "${@}"
  269. else
  270. sed -E "${@}"
  271. fi
  272. }
  273. # Print error message and exit with error
  274. _exiterr() {
  275. echo "ERROR: ${1}" >&2
  276. exit 1
  277. }
  278. # Remove newlines and whitespace from json
  279. clean_json() {
  280. tr -d '\r\n' | _sed -e 's/ +/ /g' -e 's/\{ /{/g' -e 's/ \}/}/g' -e 's/\[ /[/g' -e 's/ \]/]/g'
  281. }
  282. # Encode data as url-safe formatted base64
  283. urlbase64() {
  284. # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_'
  285. openssl base64 -e | tr -d '\n\r' | _sed -e 's:=*$::g' -e 'y:+/:-_:'
  286. }
  287. # Convert hex string to binary data
  288. hex2bin() {
  289. # Remove spaces, add leading zero, escape as hex string and parse with printf
  290. printf -- "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
  291. }
  292. # Get string value from json dictionary
  293. get_json_string_value() {
  294. local filter
  295. filter=$(printf 's/.*"%s": *"\([^"]*\)".*/\\1/p' "$1")
  296. sed -n "${filter}"
  297. }
  298. rm_json_arrays() {
  299. local filter
  300. filter='s/\[[^][]*\]/null/g'
  301. # remove three levels of nested arrays
  302. sed -e "${filter}" -e "${filter}" -e "${filter}"
  303. }
  304. # OpenSSL writes to stderr/stdout even when there are no errors. So just
  305. # display the output if the exit code was != 0 to simplify debugging.
  306. _openssl() {
  307. set +e
  308. out="$(openssl "${@}" 2>&1)"
  309. res=$?
  310. set -e
  311. if [[ ${res} -ne 0 ]]; then
  312. echo " + ERROR: failed to run $* (Exitcode: ${res})" >&2
  313. echo >&2
  314. echo "Details:" >&2
  315. echo "${out}" >&2
  316. echo >&2
  317. exit ${res}
  318. fi
  319. }
  320. # Send http(s) request with specified method
  321. http_request() {
  322. tempcont="$(_mktemp)"
  323. if [[ -n "${IP_VERSION:-}" ]]; then
  324. ip_version="-${IP_VERSION}"
  325. fi
  326. set +e
  327. if [[ "${1}" = "head" ]]; then
  328. statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
  329. curlret="${?}"
  330. elif [[ "${1}" = "get" ]]; then
  331. statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}")"
  332. curlret="${?}"
  333. elif [[ "${1}" = "post" ]]; then
  334. statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")"
  335. curlret="${?}"
  336. else
  337. set -e
  338. _exiterr "Unknown request method: ${1}"
  339. fi
  340. set -e
  341. if [[ ! "${curlret}" = "0" ]]; then
  342. _exiterr "Problem connecting to server (${1} for ${2}; curl returned with ${curlret})"
  343. fi
  344. if [[ ! "${statuscode:0:1}" = "2" ]]; then
  345. if [[ ! "${2}" = "${CA_TERMS}" ]] || [[ ! "${statuscode:0:1}" = "3" ]]; then
  346. echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
  347. echo >&2
  348. echo "Details:" >&2
  349. cat "${tempcont}" >&2
  350. echo >&2
  351. echo >&2
  352. # An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
  353. if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
  354. errtxt=`cat ${tempcont}`
  355. "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}"
  356. fi
  357. rm -f "${tempcont}"
  358. # Wait for hook script to clean the challenge if used
  359. if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then
  360. "${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}"
  361. fi
  362. # remove temporary domains.txt file if used
  363. [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}"
  364. exit 1
  365. fi
  366. fi
  367. cat "${tempcont}"
  368. rm -f "${tempcont}"
  369. }
  370. # Send signed request
  371. signed_request() {
  372. # Encode payload as urlbase64
  373. payload64="$(printf '%s' "${2}" | urlbase64)"
  374. # Retrieve nonce from acme-server
  375. nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
  376. # Build header with just our public key and algorithm information
  377. header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
  378. # Build another header which also contains the previously received nonce and encode it as urlbase64
  379. protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}'
  380. protected64="$(printf '%s' "${protected}" | urlbase64)"
  381. # Sign header with nonce and our payload with our private key and encode signature as urlbase64
  382. signed64="$(printf '%s' "${protected64}.${payload64}" | openssl dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)"
  383. # Send header + extended header + payload + signature to the acme-server
  384. data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
  385. http_request post "${1}" "${data}"
  386. }
  387. # Extracts all subject names from a CSR
  388. # Outputs either the CN, or the SANs, one per line
  389. extract_altnames() {
  390. csr="${1}" # the CSR itself (not a file)
  391. if ! <<<"${csr}" openssl req -verify -noout 2>/dev/null; then
  392. _exiterr "Certificate signing request isn't valid"
  393. fi
  394. reqtext="$( <<<"${csr}" openssl req -noout -text )"
  395. if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then
  396. # SANs used, extract these
  397. altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )"
  398. # split to one per line:
  399. # shellcheck disable=SC1003
  400. altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
  401. # we can only get DNS: ones signed
  402. if grep -qv '^DNS:' <<<"${altnames}"; then
  403. _exiterr "Certificate signing request contains non-DNS Subject Alternative Names"
  404. fi
  405. # strip away the DNS: prefix
  406. altnames="$( <<<"${altnames}" _sed -e 's/^DNS://' )"
  407. echo "${altnames}"
  408. else
  409. # No SANs, extract CN
  410. altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.* CN=([^ /,]*).*/\1/' )"
  411. echo "${altnames}"
  412. fi
  413. }
  414. # Create certificate for domain(s) and outputs it FD 3
  415. sign_csr() {
  416. csr="${1}" # the CSR itself (not a file)
  417. if { true >&3; } 2>/dev/null; then
  418. : # fd 3 looks OK
  419. else
  420. _exiterr "sign_csr: FD 3 not open"
  421. fi
  422. shift 1 || true
  423. altnames="${*:-}"
  424. if [ -z "${altnames}" ]; then
  425. altnames="$( extract_altnames "${csr}" )"
  426. fi
  427. if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
  428. _exiterr "Certificate authority doesn't allow certificate signing"
  429. fi
  430. local idx=0
  431. if [[ -n "${ZSH_VERSION:-}" ]]; then
  432. local -A challenge_altnames challenge_uris challenge_tokens keyauths deploy_args
  433. else
  434. local -a challenge_altnames challenge_uris challenge_tokens keyauths deploy_args
  435. fi
  436. # Request challenges
  437. for altname in ${altnames}; do
  438. # Ask the acme-server for new challenge token and extract them from the resulting json block
  439. echo " + Requesting challenge for ${altname}..."
  440. response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}' | clean_json)"
  441. challenge_status="$(printf '%s' "${response}" | rm_json_arrays | get_json_string_value status)"
  442. if [ "${challenge_status}" = "valid" ]; then
  443. echo " + Already validated!"
  444. continue
  445. fi
  446. challenges="$(printf '%s\n' "${response}" | sed -n 's/.*\("challenges":[^\[]*\[[^]]*]\).*/\1/p')"
  447. repl=$'\n''{' # fix syntax highlighting in Vim
  448. challenge="$(printf "%s" "${challenges//\{/${repl}}" | grep \""${CHALLENGETYPE}"\")"
  449. challenge_token="$(printf '%s' "${challenge}" | get_json_string_value token | _sed 's/[^A-Za-z0-9_\-]/_/g')"
  450. challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)"
  451. if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then
  452. _exiterr "Can't retrieve challenges (${response})"
  453. fi
  454. # Challenge response consists of the challenge token and the thumbprint of our public certificate
  455. keyauth="${challenge_token}.${thumbprint}"
  456. case "${CHALLENGETYPE}" in
  457. "http-01")
  458. # Store challenge response in well-known location and make world-readable (so that a webserver can access it)
  459. printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_token}"
  460. chmod a+r "${WELLKNOWN}/${challenge_token}"
  461. keyauth_hook="${keyauth}"
  462. ;;
  463. "dns-01")
  464. # Generate DNS entry content for dns-01 validation
  465. keyauth_hook="$(printf '%s' "${keyauth}" | openssl dgst -sha256 -binary | urlbase64)"
  466. ;;
  467. esac
  468. challenge_altnames[${idx}]="${altname}"
  469. challenge_uris[${idx}]="${challenge_uri}"
  470. keyauths[${idx}]="${keyauth}"
  471. challenge_tokens[${idx}]="${challenge_token}"
  472. # Note: assumes args will never have spaces!
  473. deploy_args[${idx}]="${altname} ${challenge_token} ${keyauth_hook}"
  474. idx=$((idx+1))
  475. done
  476. challenge_count="${idx}"
  477. # Wait for hook script to deploy the challenges if used
  478. if [[ ${challenge_count} -ne 0 ]]; then
  479. # shellcheck disable=SC2068
  480. [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "deploy_challenge" ${deploy_args[@]}
  481. fi
  482. # Respond to challenges
  483. reqstatus="valid"
  484. idx=0
  485. if [ ${challenge_count} -ne 0 ]; then
  486. for altname in "${challenge_altnames[@]:0}"; do
  487. challenge_token="${challenge_tokens[${idx}]}"
  488. keyauth="${keyauths[${idx}]}"
  489. # Wait for hook script to deploy the challenge if used
  490. # shellcheck disable=SC2086
  491. [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]}
  492. # Ask the acme-server to verify our challenge and wait until it is no longer pending
  493. echo " + Responding to challenge for ${altname}..."
  494. result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
  495. reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
  496. while [[ "${reqstatus}" = "pending" ]]; do
  497. sleep 1
  498. result="$(http_request get "${challenge_uris[${idx}]}")"
  499. reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
  500. done
  501. [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_token}"
  502. # Wait for hook script to clean the challenge if used
  503. if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token}" ]]; then
  504. # shellcheck disable=SC2086
  505. "${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
  506. fi
  507. idx=$((idx+1))
  508. if [[ "${reqstatus}" = "valid" ]]; then
  509. echo " + Challenge is valid!"
  510. else
  511. [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}"
  512. fi
  513. done
  514. fi
  515. # Wait for hook script to clean the challenges if used
  516. # shellcheck disable=SC2068
  517. if [[ ${challenge_count} -ne 0 ]]; then
  518. [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]}
  519. fi
  520. if [[ "${reqstatus}" != "valid" ]]; then
  521. # Clean up any remaining challenge_tokens if we stopped early
  522. if [[ "${CHALLENGETYPE}" = "http-01" ]] && [[ ${challenge_count} -ne 0 ]]; then
  523. while [ ${idx} -lt ${#challenge_tokens[@]} ]; do
  524. rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
  525. idx=$((idx+1))
  526. done
  527. fi
  528. _exiterr "Challenge is invalid! (returned: ${reqstatus}) (result: ${result})"
  529. fi
  530. # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem
  531. echo " + Requesting certificate..."
  532. csr64="$( <<<"${csr}" openssl req -outform DER | urlbase64)"
  533. crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | openssl base64 -e)"
  534. crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
  535. # Try to load the certificate to detect corruption
  536. echo " + Checking certificate..."
  537. _openssl x509 -text <<<"${crt}"
  538. echo "${crt}" >&3
  539. unset challenge_token
  540. echo " + Done!"
  541. }
  542. # grep issuer cert uri from certificate
  543. get_issuer_cert_uri() {
  544. certificate="${1}"
  545. openssl x509 -in "${certificate}" -noout -text | (grep 'CA Issuers - URI:' | cut -d':' -f2-) || true
  546. }
  547. # walk certificate chain, retrieving all intermediate certificates
  548. walk_chain() {
  549. local certificate
  550. certificate="${1}"
  551. local issuer_cert_uri
  552. issuer_cert_uri="${2:-}"
  553. if [[ -z "${issuer_cert_uri}" ]]; then issuer_cert_uri="$(get_issuer_cert_uri "${certificate}")"; fi
  554. if [[ -n "${issuer_cert_uri}" ]]; then
  555. # create temporary files
  556. local tmpcert
  557. local tmpcert_raw
  558. tmpcert_raw="$(_mktemp)"
  559. tmpcert="$(_mktemp)"
  560. # download certificate
  561. http_request get "${issuer_cert_uri}" > "${tmpcert_raw}"
  562. # PEM
  563. if grep -q "BEGIN CERTIFICATE" "${tmpcert_raw}"; then mv "${tmpcert_raw}" "${tmpcert}"
  564. # DER
  565. elif openssl x509 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM 2> /dev/null > /dev/null; then :
  566. # PKCS7
  567. elif openssl pkcs7 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM -print_certs 2> /dev/null > /dev/null; then :
  568. # Unknown certificate type
  569. else _exiterr "Unknown certificate type in chain"
  570. fi
  571. local next_issuer_cert_uri
  572. next_issuer_cert_uri="$(get_issuer_cert_uri "${tmpcert}")"
  573. if [[ -n "${next_issuer_cert_uri}" ]]; then
  574. printf "\n%s\n" "${issuer_cert_uri}"
  575. cat "${tmpcert}"
  576. walk_chain "${tmpcert}" "${next_issuer_cert_uri}"
  577. fi
  578. rm -f "${tmpcert}" "${tmpcert_raw}"
  579. fi
  580. }
  581. # Create certificate for domain(s)
  582. sign_domain() {
  583. domain="${1}"
  584. altnames="${*}"
  585. timestamp="$(date +%s)"
  586. echo " + Signing domains..."
  587. if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
  588. _exiterr "Certificate authority doesn't allow certificate signing"
  589. fi
  590. # If there is no existing certificate directory => make it
  591. if [[ ! -e "${CERTDIR}/${domain}" ]]; then
  592. echo " + Creating new directory ${CERTDIR}/${domain} ..."
  593. mkdir -p "${CERTDIR}/${domain}" || _exiterr "Unable to create directory ${CERTDIR}/${domain}"
  594. fi
  595. privkey="privkey.pem"
  596. # generate a new private key if we need or want one
  597. if [[ ! -r "${CERTDIR}/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
  598. echo " + Generating private key..."
  599. privkey="privkey-${timestamp}.pem"
  600. case "${KEY_ALGO}" in
  601. rsa) _openssl genrsa -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";;
  602. prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem";;
  603. esac
  604. fi
  605. # move rolloverkey into position (if any)
  606. if [[ -r "${CERTDIR}/${domain}/privkey.pem" && -r "${CERTDIR}/${domain}/privkey.roll.pem" && "${PRIVATE_KEY_RENEW}" = "yes" && "${PRIVATE_KEY_ROLLOVER}" = "yes" ]]; then
  607. echo " + Moving Rolloverkey into position.... "
  608. mv "${CERTDIR}/${domain}/privkey.roll.pem" "${CERTDIR}/${domain}/privkey-tmp.pem"
  609. mv "${CERTDIR}/${domain}/privkey-${timestamp}.pem" "${CERTDIR}/${domain}/privkey.roll.pem"
  610. mv "${CERTDIR}/${domain}/privkey-tmp.pem" "${CERTDIR}/${domain}/privkey-${timestamp}.pem"
  611. fi
  612. # generate a new private rollover key if we need or want one
  613. if [[ ! -r "${CERTDIR}/${domain}/privkey.roll.pem" && "${PRIVATE_KEY_ROLLOVER}" = "yes" && "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
  614. echo " + Generating private rollover key..."
  615. case "${KEY_ALGO}" in
  616. rsa) _openssl genrsa -out "${CERTDIR}/${domain}/privkey.roll.pem" "${KEYSIZE}";;
  617. prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${CERTDIR}/${domain}/privkey.roll.pem";;
  618. esac
  619. fi
  620. # delete rolloverkeys if disabled
  621. if [[ -r "${CERTDIR}/${domain}/privkey.roll.pem" && ! "${PRIVATE_KEY_ROLLOVER}" = "yes" ]]; then
  622. echo " + Removing Rolloverkey (feature disabled)..."
  623. rm -f "${CERTDIR}/${domain}/privkey.roll.pem"
  624. fi
  625. # Generate signing request config and the actual signing request
  626. echo " + Generating signing request..."
  627. SAN=""
  628. for altname in ${altnames}; do
  629. SAN+="DNS:${altname}, "
  630. done
  631. SAN="${SAN%%, }"
  632. local tmp_openssl_cnf
  633. tmp_openssl_cnf="$(_mktemp)"
  634. cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
  635. printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
  636. if [ "${OCSP_MUST_STAPLE}" = "yes" ]; then
  637. printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}"
  638. fi
  639. openssl req -new -sha256 -key "${CERTDIR}/${domain}/${privkey}" -out "${CERTDIR}/${domain}/cert-${timestamp}.csr" -subj "/CN=${domain}/" -reqexts SAN -config "${tmp_openssl_cnf}"
  640. rm -f "${tmp_openssl_cnf}"
  641. crt_path="${CERTDIR}/${domain}/cert-${timestamp}.pem"
  642. # shellcheck disable=SC2086
  643. sign_csr "$(< "${CERTDIR}/${domain}/cert-${timestamp}.csr" )" ${altnames} 3>"${crt_path}"
  644. # Create fullchain.pem
  645. echo " + Creating fullchain.pem..."
  646. cat "${crt_path}" > "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
  647. walk_chain "${crt_path}" > "${CERTDIR}/${domain}/chain-${timestamp}.pem"
  648. cat "${CERTDIR}/${domain}/chain-${timestamp}.pem" >> "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
  649. # Update symlinks
  650. [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${CERTDIR}/${domain}/privkey.pem"
  651. ln -sf "chain-${timestamp}.pem" "${CERTDIR}/${domain}/chain.pem"
  652. ln -sf "fullchain-${timestamp}.pem" "${CERTDIR}/${domain}/fullchain.pem"
  653. ln -sf "cert-${timestamp}.csr" "${CERTDIR}/${domain}/cert.csr"
  654. ln -sf "cert-${timestamp}.pem" "${CERTDIR}/${domain}/cert.pem"
  655. # Wait for hook script to clean the challenge and to deploy cert if used
  656. [[ -n "${HOOK}" ]] && "${HOOK}" "deploy_cert" "${domain}" "${CERTDIR}/${domain}/privkey.pem" "${CERTDIR}/${domain}/cert.pem" "${CERTDIR}/${domain}/fullchain.pem" "${CERTDIR}/${domain}/chain.pem" "${timestamp}"
  657. unset challenge_token
  658. echo " + Done!"
  659. }
  660. # Usage: --register
  661. # Description: Register account key
  662. command_register() {
  663. init_system
  664. echo "+ Done!"
  665. exit 0
  666. }
  667. # Usage: --cron (-c)
  668. # Description: Sign/renew non-existant/changed/expiring certificates.
  669. command_sign_domains() {
  670. init_system
  671. if [[ -n "${PARAM_DOMAIN:-}" ]]; then
  672. DOMAINS_TXT="$(_mktemp)"
  673. printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
  674. elif [[ -e "${DOMAINS_TXT}" ]]; then
  675. if [[ ! -r "${DOMAINS_TXT}" ]]; then
  676. _exiterr "domains.txt found but not readable"
  677. fi
  678. else
  679. _exiterr "domains.txt not found and --domain not given"
  680. fi
  681. # Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
  682. ORIGIFS="${IFS}"
  683. IFS=$'\n'
  684. 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' | (grep -vE '^(#|$)' || true)); do
  685. reset_configvars
  686. IFS="${ORIGIFS}"
  687. domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
  688. morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
  689. cert="${CERTDIR}/${domain}/cert.pem"
  690. force_renew="${PARAM_FORCE:-no}"
  691. if [[ -z "${morenames}" ]];then
  692. echo "Processing ${domain}"
  693. else
  694. echo "Processing ${domain} with alternative names: ${morenames}"
  695. fi
  696. # read cert config
  697. # for now this loads the certificate specific config in a subshell and parses a diff of set variables.
  698. # we could just source the config file but i decided to go this way to protect people from accidentally overriding
  699. # variables used internally by this script itself.
  700. if [[ -n "${DOMAINS_D}" ]]; then
  701. certconfig="${DOMAINS_D}/${domain}"
  702. else
  703. certconfig="${CERTDIR}/${domain}/config"
  704. fi
  705. if [ -f "${certconfig}" ]; then
  706. echo " + Using certificate specific config file!"
  707. ORIGIFS="${IFS}"
  708. IFS=$'\n'
  709. for cfgline in $(
  710. beforevars="$(_mktemp)"
  711. aftervars="$(_mktemp)"
  712. set > "${beforevars}"
  713. # shellcheck disable=SC1090
  714. . "${certconfig}"
  715. set > "${aftervars}"
  716. diff -u "${beforevars}" "${aftervars}" | grep -E '^\+[^+]'
  717. rm "${beforevars}"
  718. rm "${aftervars}"
  719. ); do
  720. config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
  721. config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)"
  722. case "${config_var}" in
  723. KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
  724. echo " + ${config_var} = ${config_value}"
  725. declare -- "${config_var}=${config_value}"
  726. ;;
  727. _) ;;
  728. *) echo " ! Setting ${config_var} on a per-certificate base is not (yet) supported"
  729. esac
  730. done
  731. IFS="${ORIGIFS}"
  732. fi
  733. verify_config
  734. export WELLKNOWN CHALLENGETYPE KEY_ALGO PRIVATE_KEY_ROLLOVER
  735. if [[ -e "${cert}" ]]; then
  736. printf " + Checking domain name(s) of existing cert..."
  737. certnames="$(openssl x509 -in "${cert}" -text -noout | grep DNS: | _sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
  738. givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//' | _sed 's/^ //')"
  739. if [[ "${certnames}" = "${givennames}" ]]; then
  740. echo " unchanged."
  741. else
  742. echo " changed!"
  743. echo " + Domain name(s) are not matching!"
  744. echo " + Names in old certificate: ${certnames}"
  745. echo " + Configured names: ${givennames}"
  746. echo " + Forcing renew."
  747. force_renew="yes"
  748. fi
  749. fi
  750. if [[ -e "${cert}" ]]; then
  751. echo " + Checking expire date of existing cert..."
  752. valid="$(openssl x509 -enddate -noout -in "${cert}" | cut -d= -f2- )"
  753. printf " + Valid till %s " "${valid}"
  754. if openssl x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}"; then
  755. printf "(Longer than %d days). " "${RENEW_DAYS}"
  756. if [[ "${force_renew}" = "yes" ]]; then
  757. echo "Ignoring because renew was forced!"
  758. else
  759. # Certificate-Names unchanged and cert is still valid
  760. echo "Skipping renew!"
  761. [[ -n "${HOOK}" ]] && "${HOOK}" "unchanged_cert" "${domain}" "${CERTDIR}/${domain}/privkey.pem" "${CERTDIR}/${domain}/cert.pem" "${CERTDIR}/${domain}/fullchain.pem" "${CERTDIR}/${domain}/chain.pem"
  762. continue
  763. fi
  764. else
  765. echo "(Less than ${RENEW_DAYS} days). Renewing!"
  766. fi
  767. fi
  768. # shellcheck disable=SC2086
  769. if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
  770. sign_domain ${line} &
  771. wait $! || true
  772. else
  773. sign_domain ${line}
  774. fi
  775. done
  776. # remove temporary domains.txt file if used
  777. [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}"
  778. [[ -n "${HOOK}" ]] && "${HOOK}" "exit_hook"
  779. exit 0
  780. }
  781. # Usage: --signcsr (-s) path/to/csr.pem
  782. # Description: Sign a given CSR, output CRT on stdout (advanced usage)
  783. command_sign_csr() {
  784. # redirect stdout to stderr
  785. # leave stdout over at fd 3 to output the cert
  786. exec 3>&1 1>&2
  787. init_system
  788. csrfile="${1}"
  789. if [ ! -r "${csrfile}" ]; then
  790. _exiterr "Could not read certificate signing request ${csrfile}"
  791. fi
  792. # gen cert
  793. certfile="$(_mktemp)"
  794. sign_csr "$(< "${csrfile}" )" 3> "${certfile}"
  795. # print cert
  796. echo "# CERT #" >&3
  797. cat "${certfile}" >&3
  798. echo >&3
  799. # print chain
  800. if [ -n "${PARAM_FULL_CHAIN:-}" ]; then
  801. # get and convert ca cert
  802. chainfile="$(_mktemp)"
  803. tmpchain="$(_mktemp)"
  804. http_request get "$(openssl x509 -in "${certfile}" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${tmpchain}"
  805. if grep -q "BEGIN CERTIFICATE" "${tmpchain}"; then
  806. mv "${tmpchain}" "${chainfile}"
  807. else
  808. openssl x509 -in "${tmpchain}" -inform DER -out "${chainfile}" -outform PEM
  809. rm "${tmpchain}"
  810. fi
  811. echo "# CHAIN #" >&3
  812. cat "${chainfile}" >&3
  813. rm "${chainfile}"
  814. fi
  815. # cleanup
  816. rm "${certfile}"
  817. exit 0
  818. }
  819. # Usage: --revoke (-r) path/to/cert.pem
  820. # Description: Revoke specified certificate
  821. command_revoke() {
  822. init_system
  823. [[ -n "${CA_REVOKE_CERT}" ]] || _exiterr "Certificate authority doesn't allow certificate revocation."
  824. cert="${1}"
  825. if [[ -L "${cert}" ]]; then
  826. # follow symlink and use real certificate name (so we move the real file and not the symlink at the end)
  827. local link_target
  828. link_target="$(readlink -n "${cert}")"
  829. if [[ "${link_target}" =~ ^/ ]]; then
  830. cert="${link_target}"
  831. else
  832. cert="$(dirname "${cert}")/${link_target}"
  833. fi
  834. fi
  835. [[ -f "${cert}" ]] || _exiterr "Could not find certificate ${cert}"
  836. echo "Revoking ${cert}"
  837. cert64="$(openssl x509 -in "${cert}" -inform PEM -outform DER | urlbase64)"
  838. response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)"
  839. # if there is a problem with our revoke request _request (via signed_request) will report this and "exit 1" out
  840. # so if we are here, it is safe to assume the request was successful
  841. echo " + Done."
  842. echo " + Renaming certificate to ${cert}-revoked"
  843. mv -f "${cert}" "${cert}-revoked"
  844. }
  845. # Usage: --cleanup (-gc)
  846. # Description: Move unused certificate files to archive directory
  847. command_cleanup() {
  848. load_config
  849. # Create global archive directory if not existant
  850. if [[ ! -e "${BASEDIR}/archive" ]]; then
  851. mkdir "${BASEDIR}/archive"
  852. fi
  853. # Loop over all certificate directories
  854. for certdir in "${CERTDIR}/"*; do
  855. # Skip if entry is not a folder
  856. [[ -d "${certdir}" ]] || continue
  857. # Get certificate name
  858. certname="$(basename "${certdir}")"
  859. # Create certitifaces archive directory if not existant
  860. archivedir="${BASEDIR}/archive/${certname}"
  861. if [[ ! -e "${archivedir}" ]]; then
  862. mkdir "${archivedir}"
  863. fi
  864. # Loop over file-types (certificates, keys, signing-requests, ...)
  865. for filetype in cert.csr cert.pem chain.pem fullchain.pem privkey.pem; do
  866. # Skip if symlink is broken
  867. [[ -r "${certdir}/${filetype}" ]] || continue
  868. # Look up current file in use
  869. current="$(basename "$(readlink "${certdir}/${filetype}")")"
  870. # Split filetype into name and extension
  871. filebase="$(echo "${filetype}" | cut -d. -f1)"
  872. fileext="$(echo "${filetype}" | cut -d. -f2)"
  873. # Loop over all files of this type
  874. for file in "${certdir}/${filebase}-"*".${fileext}"; do
  875. # Handle case where no files match the wildcard
  876. [[ -f "${file}" ]] || break
  877. # Check if current file is in use, if unused move to archive directory
  878. filename="$(basename "${file}")"
  879. if [[ ! "${filename}" = "${current}" ]]; then
  880. echo "Moving unused file to archive directory: ${certname}/${filename}"
  881. mv "${certdir}/${filename}" "${archivedir}/${filename}"
  882. fi
  883. done
  884. done
  885. done
  886. exit 0
  887. }
  888. # Usage: --help (-h)
  889. # Description: Show help text
  890. command_help() {
  891. printf "Usage: %s [-h] [command [argument]] [parameter [argument]] [parameter [argument]] ...\n\n" "${0}"
  892. printf "Default command: help\n\n"
  893. echo "Commands:"
  894. grep -e '^[[:space:]]*# Usage:' -e '^[[:space:]]*# Description:' -e '^command_.*()[[:space:]]*{' "${0}" | while read -r usage; read -r description; read -r command; do
  895. if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]] || [[ ! "${command}" =~ ^command_ ]]; then
  896. _exiterr "Error generating help text."
  897. fi
  898. printf " %-32s %s\n" "${usage##"# Usage: "}" "${description##"# Description: "}"
  899. done
  900. printf -- "\nParameters:\n"
  901. grep -E -e '^[[:space:]]*# PARAM_Usage:' -e '^[[:space:]]*# PARAM_Description:' "${0}" | while read -r usage; read -r description; do
  902. if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]]; then
  903. _exiterr "Error generating help text."
  904. fi
  905. printf " %-32s %s\n" "${usage##"# PARAM_Usage: "}" "${description##"# PARAM_Description: "}"
  906. done
  907. }
  908. # Usage: --env (-e)
  909. # Description: Output configuration variables for use in other scripts
  910. command_env() {
  911. echo "# dehydrated configuration"
  912. load_config
  913. typeset -p CA LICENSE CERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE
  914. }
  915. # Main method (parses script arguments and calls command_* methods)
  916. main() {
  917. COMMAND=""
  918. set_command() {
  919. [[ -z "${COMMAND}" ]] || _exiterr "Only one command can be executed at a time. See help (-h) for more information."
  920. COMMAND="${1}"
  921. }
  922. check_parameters() {
  923. if [[ -z "${1:-}" ]]; then
  924. echo "The specified command requires additional parameters. See help:" >&2
  925. echo >&2
  926. command_help >&2
  927. exit 1
  928. elif [[ "${1:0:1}" = "-" ]]; then
  929. _exiterr "Invalid argument: ${1}"
  930. fi
  931. }
  932. [[ -z "${@}" ]] && eval set -- "--help"
  933. while (( ${#} )); do
  934. case "${1}" in
  935. --help|-h)
  936. command_help
  937. exit 0
  938. ;;
  939. --env|-e)
  940. set_command env
  941. ;;
  942. --cron|-c)
  943. set_command sign_domains
  944. ;;
  945. --register)
  946. set_command register
  947. ;;
  948. # PARAM_Usage: --accept-terms
  949. # PARAM_Description: Accept CAs terms of service
  950. --accept-terms)
  951. PARAM_ACCEPT_TERMS="yes"
  952. ;;
  953. --signcsr|-s)
  954. shift 1
  955. set_command sign_csr
  956. check_parameters "${1:-}"
  957. PARAM_CSR="${1}"
  958. ;;
  959. --revoke|-r)
  960. shift 1
  961. set_command revoke
  962. check_parameters "${1:-}"
  963. PARAM_REVOKECERT="${1}"
  964. ;;
  965. --cleanup|-gc)
  966. set_command cleanup
  967. ;;
  968. # PARAM_Usage: --full-chain (-fc)
  969. # PARAM_Description: Print full chain when using --signcsr
  970. --full-chain|-fc)
  971. PARAM_FULL_CHAIN="1"
  972. ;;
  973. # PARAM_Usage: --ipv4 (-4)
  974. # PARAM_Description: Resolve names to IPv4 addresses only
  975. --ipv4|-4)
  976. PARAM_IP_VERSION="4"
  977. ;;
  978. # PARAM_Usage: --ipv6 (-6)
  979. # PARAM_Description: Resolve names to IPv6 addresses only
  980. --ipv6|-6)
  981. PARAM_IP_VERSION="6"
  982. ;;
  983. # PARAM_Usage: --domain (-d) domain.tld
  984. # PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!)
  985. --domain|-d)
  986. shift 1
  987. check_parameters "${1:-}"
  988. if [[ -z "${PARAM_DOMAIN:-}" ]]; then
  989. PARAM_DOMAIN="${1}"
  990. else
  991. PARAM_DOMAIN="${PARAM_DOMAIN} ${1}"
  992. fi
  993. ;;
  994. # PARAM_Usage: --keep-going (-g)
  995. # PARAM_Description: Keep going after encountering an error while creating/renewing multiple certificates in cron mode
  996. --keep-going|-g)
  997. PARAM_KEEP_GOING="yes"
  998. ;;
  999. # PARAM_Usage: --force (-x)
  1000. # PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS
  1001. --force|-x)
  1002. PARAM_FORCE="yes"
  1003. ;;
  1004. # PARAM_Usage: --no-lock (-n)
  1005. # PARAM_Description: Don't use lockfile (potentially dangerous!)
  1006. --no-lock|-n)
  1007. PARAM_NO_LOCK="yes"
  1008. ;;
  1009. # PARAM_Usage: --lock-suffix example.com
  1010. # PARAM_Description: Suffix lockfile name with a string (useful for with -d)
  1011. --lock-suffix)
  1012. shift 1
  1013. check_parameters "${1:-}"
  1014. PARAM_LOCKFILE_SUFFIX="${1}"
  1015. ;;
  1016. # PARAM_Usage: --ocsp
  1017. # PARAM_Description: Sets option in CSR indicating OCSP stapling to be mandatory
  1018. --ocsp)
  1019. PARAM_OCSP_MUST_STAPLE="yes"
  1020. ;;
  1021. # PARAM_Usage: --privkey (-p) path/to/key.pem
  1022. # PARAM_Description: Use specified private key instead of account key (useful for revocation)
  1023. --privkey|-p)
  1024. shift 1
  1025. check_parameters "${1:-}"
  1026. PARAM_ACCOUNT_KEY="${1}"
  1027. ;;
  1028. # PARAM_Usage: --config (-f) path/to/config
  1029. # PARAM_Description: Use specified config file
  1030. --config|-f)
  1031. shift 1
  1032. check_parameters "${1:-}"
  1033. CONFIG="${1}"
  1034. ;;
  1035. # PARAM_Usage: --hook (-k) path/to/hook.sh
  1036. # PARAM_Description: Use specified script for hooks
  1037. --hook|-k)
  1038. shift 1
  1039. check_parameters "${1:-}"
  1040. PARAM_HOOK="${1}"
  1041. ;;
  1042. # PARAM_Usage: --out (-o) certs/directory
  1043. # PARAM_Description: Output certificates into the specified directory
  1044. --out|-o)
  1045. shift 1
  1046. check_parameters "${1:-}"
  1047. PARAM_CERTDIR="${1}"
  1048. ;;
  1049. # PARAM_Usage: --challenge (-t) http-01|dns-01
  1050. # PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
  1051. --challenge|-t)
  1052. shift 1
  1053. check_parameters "${1:-}"
  1054. PARAM_CHALLENGETYPE="${1}"
  1055. ;;
  1056. # PARAM_Usage: --algo (-a) rsa|prime256v1|secp384r1
  1057. # PARAM_Description: Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
  1058. --algo|-a)
  1059. shift 1
  1060. check_parameters "${1:-}"
  1061. PARAM_KEY_ALGO="${1}"
  1062. ;;
  1063. *)
  1064. echo "Unknown parameter detected: ${1}" >&2
  1065. echo >&2
  1066. command_help >&2
  1067. exit 1
  1068. ;;
  1069. esac
  1070. shift 1
  1071. done
  1072. case "${COMMAND}" in
  1073. env) command_env;;
  1074. sign_domains) command_sign_domains;;
  1075. register) command_register;;
  1076. sign_csr) command_sign_csr "${PARAM_CSR}";;
  1077. revoke) command_revoke "${PARAM_REVOKECERT}";;
  1078. cleanup) command_cleanup;;
  1079. *) command_help; exit 1;;
  1080. esac
  1081. }
  1082. # Determine OS type
  1083. OSTYPE="$(uname)"
  1084. # Check for missing dependencies
  1085. check_dependencies
  1086. # Run script
  1087. main "${@:-}"