mirror of
https://github.com/dehydrated-io/dehydrated.git
synced 2026-01-11 22:30:44 +01:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7128e6b63c | ||
|
|
861f4c733d | ||
|
|
ad3f08084c | ||
|
|
784fb806c8 | ||
|
|
b2574b16d1 | ||
|
|
da641588ce | ||
|
|
8e6ddf6286 | ||
|
|
8e5977890a | ||
|
|
3bcf0c7f5a | ||
|
|
b347bc9086 | ||
|
|
08477170e9 | ||
|
|
f4cf92bae5 | ||
|
|
93573cda3c | ||
|
|
607a6088d3 | ||
|
|
880c99aa63 | ||
|
|
7ac25358ef | ||
|
|
5733863b93 | ||
|
|
f6a84a88fa | ||
|
|
e963438c5a | ||
|
|
095165ee96 | ||
|
|
199cd59774 | ||
|
|
e17456778f | ||
|
|
71f6bc617e | ||
|
|
6ee4ae508e | ||
|
|
91cccc0c23 | ||
|
|
ab016803dd | ||
|
|
7d8573af12 | ||
|
|
fb06530097 | ||
|
|
5c1551e946 | ||
|
|
20c27b291c | ||
|
|
24f66a3473 | ||
|
|
21bff55b7c | ||
|
|
374fce0249 | ||
|
|
00941472b2 | ||
|
|
527933db24 | ||
|
|
33a421f1e4 | ||
|
|
dd0bbd2405 | ||
|
|
26660e11c7 | ||
|
|
316054ad1c | ||
|
|
29b67962ac | ||
|
|
3a7795589b |
11
CHANGELOG
11
CHANGELOG
@@ -1,6 +1,17 @@
|
||||
# Change Log
|
||||
This file contains a log of major changes in dehydrated
|
||||
|
||||
## [x.x.x] - xxxx-xx-xx
|
||||
## Changed
|
||||
- `--force` no longer forces domain name revalidation by default, a new argument `--force-validation` has been added for that
|
||||
- Added support for EC secp521r1 algorithm (works with e.g. zerossl)
|
||||
- `EC PARAMETERS` are no longer written to privkey.pem (didn't seem necessary and was causing issues with various software)
|
||||
|
||||
## 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
|
||||
- Support for external account bindings
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2018 Lukas Schauer
|
||||
Copyright (c) 2015-2021 Lukas Schauer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
31
README.md
31
README.md
@@ -1,9 +1,6 @@
|
||||
# dehydrated [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8)
|
||||
|
||||
Quick note: dehydrated moved, the license will NOT change, and I will still take care of the project.
|
||||
See https://lukas.im/2020/01/30/selling-dehydrated/index.html for more details.
|
||||
|
||||

|
||||

|
||||
|
||||
Dehydrated is a client for signing certificates with an ACME-server (e.g. Let's Encrypt) implemented as a relatively simple (zsh-compatible) bash-script.
|
||||
This client supports both ACME v1 and the new ACME v2 including support for wildcard certificates!
|
||||
@@ -17,6 +14,7 @@ Current features:
|
||||
- Signing of a custom CSR (either standalone or completely automated using hooks!)
|
||||
- Renewal if a certificate is about to expire or defined set of domains changed
|
||||
- Certificate revocation
|
||||
- and lots more..
|
||||
|
||||
Please keep in mind that this software, the ACME-protocol and all supported CA servers out there are relatively young and there might be a few issues. Feel free to report any issues you find with this script or contribute by submitting a pull request,
|
||||
but please check for duplicates first (feel free to comment on those to get things rolling).
|
||||
@@ -74,6 +72,7 @@ Parameters:
|
||||
--alias certalias Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified)
|
||||
--keep-going (-g) Keep going after encountering an error while creating/renewing multiple certificates in cron mode
|
||||
--force (-x) Force renew of certificate even if it is longer valid than value in RENEW_DAYS
|
||||
--force-validation Force revalidation of domain names (used in combination with --force)
|
||||
--no-lock (-n) Don't use lockfile (potentially dangerous!)
|
||||
--lock-suffix example.com Suffix lockfile name with a string (useful for with -d)
|
||||
--ocsp Sets option in CSR indicating OCSP stapling to be mandatory
|
||||
@@ -84,28 +83,6 @@ Parameters:
|
||||
--preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN
|
||||
--out (-o) certs/directory Output certificates into the specified directory
|
||||
--alpn alpn-certs/directory Output alpn verification certificates into the specified directory
|
||||
--challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported
|
||||
--challenge (-t) http-01|dns-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
|
||||
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
|
||||
```
|
||||
|
||||
## Donate
|
||||
|
||||
I'm a student hacker with a few (unfortunately) quite expensive hobbies (self-hosting, virtualization clusters, routing,
|
||||
high-speed networking, embedded hardware, etc.).
|
||||
I'm really having fun playing around with hard- and software and I'm steadily learning new things.
|
||||
Without those hobbies I probably would never have started working on dehydrated to begin with :)
|
||||
|
||||
I'd really appreciate if you could [donate a bit of money](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8)
|
||||
so I can buy cool stuff (while still being able to afford food :D).
|
||||
|
||||
If you have hardware laying around that you think I'd enjoy playing with (e.g. decommissioned but still modern-ish servers,
|
||||
10G networking hardware, enterprise grade routers or APs, interesting ARM/MIPS boards, etc.) and that you would be willing
|
||||
to ship to me please contact me at `donations@dehydrated.io` or on Twitter [@lukas2511](https://twitter.com/lukas2511).
|
||||
|
||||
If you want your name to be added to the [donations list](https://dehydrated.io/donations.html) please add a note or send me an
|
||||
email `donations@dehydrated.io`. I respect your privacy and won't publish your name without permission.
|
||||
|
||||
Other ways of donating:
|
||||
- [My Amazon Wishlist](http://www.amazon.de/registry/wishlist/1TUCFJK35IO4Q)
|
||||
- Monero: 4Kkf4tF4r9DakxLj37HDXLJgmpVfQoFhT7JLDvXwtUZZMTbsK9spsAPXivWPAFcDUj6jHhY8hJSHX8Cb8ndMhKeQHPSkBZZiK89Fx8NTHk
|
||||
- Bitcoin: 12487bHxcrREffTGwUDnoxF1uYxCA7ztKK
|
||||
|
||||
351
dehydrated
351
dehydrated
@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
|
||||
exec 3>&-
|
||||
exec 4>&-
|
||||
|
||||
VERSION="0.7.0"
|
||||
VERSION="0.7.1"
|
||||
|
||||
# Find directory in which this script is stored by traversing all symbolic links
|
||||
SOURCE="${0}"
|
||||
@@ -31,6 +31,22 @@ SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
BASEDIR="${SCRIPTDIR}"
|
||||
ORIGARGS=("${@}")
|
||||
|
||||
noglob_set() {
|
||||
if [[ -n "${ZSH_VERSION:-}" ]]; then
|
||||
set +o noglob
|
||||
else
|
||||
set +f
|
||||
fi
|
||||
}
|
||||
|
||||
noglob_clear() {
|
||||
if [[ -n "${ZSH_VERSION:-}" ]]; then
|
||||
set -o noglob
|
||||
else
|
||||
set -f
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate json.sh path matching string
|
||||
json_path() {
|
||||
if [ ! "${1}" = "-p" ]; then
|
||||
@@ -55,7 +71,6 @@ get_json_array_values() {
|
||||
# Get sub-dictionary from json
|
||||
get_json_dict_value() {
|
||||
local filter
|
||||
echo "$(json_path "${1:-}" "${2:-}")"
|
||||
filter="$(printf 's/.*\[%s\][[:space:]]*\(.*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")"
|
||||
sed -n "${filter}" | jsonsh
|
||||
}
|
||||
@@ -88,7 +103,7 @@ jsonsh() {
|
||||
awk_egrep () {
|
||||
local pattern_string=$1
|
||||
|
||||
gawk '{
|
||||
awk '{
|
||||
while ($0) {
|
||||
start=match($0, pattern);
|
||||
token=substr($0, start, RLENGTH);
|
||||
@@ -103,14 +118,15 @@ jsonsh() {
|
||||
local ESCAPE
|
||||
local CHAR
|
||||
|
||||
if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1
|
||||
if echo "test string" | grep -Eao --color=never "test" >/dev/null 2>&1
|
||||
then
|
||||
GREP='egrep -ao --color=never'
|
||||
GREP='grep -Eao --color=never'
|
||||
else
|
||||
GREP='egrep -ao'
|
||||
GREP='grep -Eao'
|
||||
fi
|
||||
|
||||
if echo "test string" | egrep -o "test" >/dev/null 2>&1
|
||||
# shellcheck disable=SC2196
|
||||
if echo "test string" | egrep -ao "test" >/dev/null 2>&1
|
||||
then
|
||||
ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
|
||||
CHAR='[^[:cntrl:]"\\]'
|
||||
@@ -126,10 +142,11 @@ jsonsh() {
|
||||
local SPACE='[[:space:]]+'
|
||||
|
||||
# Force zsh to expand $A into multiple words
|
||||
local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')
|
||||
if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi
|
||||
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$"
|
||||
if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi
|
||||
local is_wordsplit_disabled
|
||||
is_wordsplit_disabled="$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')"
|
||||
if [ "${is_wordsplit_disabled}" != "0" ]; then setopt shwordsplit; fi
|
||||
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | grep -Ev "^$SPACE$"
|
||||
if [ "${is_wordsplit_disabled}" != "0" ]; then unsetopt shwordsplit; fi
|
||||
}
|
||||
|
||||
parse_array () {
|
||||
@@ -194,17 +211,14 @@ jsonsh() {
|
||||
}
|
||||
|
||||
parse_value () {
|
||||
local jpath="${1:+$1,}${2:-}" isleaf=0 isempty=0 print=0
|
||||
local jpath="${1:+$1,}${2:-}"
|
||||
case "$token" in
|
||||
'{') parse_object "$jpath" ;;
|
||||
'[') parse_array "$jpath" ;;
|
||||
# At this point, the only valid single-character tokens are digits.
|
||||
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
|
||||
*) value=$token
|
||||
*) value="${token/\\\///}"
|
||||
# replace solidus ("\/") in json strings with normalized value: "/"
|
||||
value=$(echo "$value" | sed 's#\\/#/#g')
|
||||
isleaf=1
|
||||
[ "$value" = '""' ] && isempty=1
|
||||
;;
|
||||
esac
|
||||
[ "$value" = '' ] && return
|
||||
@@ -227,10 +241,20 @@ 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() {
|
||||
# shellcheck disable=SC2068
|
||||
mktemp ${@:-} "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
|
||||
mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
|
||||
}
|
||||
|
||||
# Check for script dependencies
|
||||
@@ -254,7 +278,10 @@ check_dependencies() {
|
||||
store_configvars() {
|
||||
__KEY_ALGO="${KEY_ALGO}"
|
||||
__OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
|
||||
__OCSP_FETCH="${OCSP_FETCH}"
|
||||
__OCSP_DAYS="${OCSP_DAYS}"
|
||||
__PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
|
||||
__PRIVATE_KEY_ROLLOVER="${PRIVATE_KEY_ROLLOVER}"
|
||||
__KEYSIZE="${KEYSIZE}"
|
||||
__CHALLENGETYPE="${CHALLENGETYPE}"
|
||||
__HOOK="${HOOK}"
|
||||
@@ -269,7 +296,10 @@ store_configvars() {
|
||||
reset_configvars() {
|
||||
KEY_ALGO="${__KEY_ALGO}"
|
||||
OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
|
||||
OCSP_FETCH="${__OCSP_FETCH}"
|
||||
OCSP_DAYS="${__OCSP_DAYS}"
|
||||
PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
|
||||
PRIVATE_KEY_ROLLOVER="${__PRIVATE_KEY_ROLLOVER}"
|
||||
KEYSIZE="${__KEYSIZE}"
|
||||
CHALLENGETYPE="${__CHALLENGETYPE}"
|
||||
HOOK="${__HOOK}"
|
||||
@@ -298,7 +328,7 @@ verify_config() {
|
||||
if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
|
||||
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
|
||||
fi
|
||||
[[ "${KEY_ALGO}" == "rsa" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... cannot continue."
|
||||
[[ "${KEY_ALGO}" == "rsa" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" || "${KEY_ALGO}" == "secp521r1" ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... cannot continue."
|
||||
if [[ -n "${IP_VERSION}" ]]; then
|
||||
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
|
||||
fi
|
||||
@@ -332,6 +362,8 @@ load_config() {
|
||||
CERTDIR=
|
||||
ALPNCERTDIR=
|
||||
ACCOUNTDIR=
|
||||
ACCOUNT_KEYSIZE="4096"
|
||||
ACCOUNT_KEY_ALGO=rsa
|
||||
CHALLENGETYPE="http-01"
|
||||
CONFIG_D=
|
||||
CURL_OPTS=
|
||||
@@ -379,7 +411,7 @@ load_config() {
|
||||
fi
|
||||
|
||||
# Allow globbing
|
||||
[[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f
|
||||
noglob_set
|
||||
|
||||
for check_config_d in "${CONFIG_D}"/*.sh; do
|
||||
if [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then
|
||||
@@ -392,7 +424,7 @@ load_config() {
|
||||
done
|
||||
|
||||
# Disable globbing
|
||||
[[ -n "${ZSH_VERSION:-}" ]] && set -o noglob || set -f
|
||||
noglob_clear
|
||||
fi
|
||||
|
||||
# Check for missing dependencies
|
||||
@@ -473,6 +505,7 @@ load_config() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
[[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
|
||||
ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
|
||||
ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
|
||||
@@ -512,6 +545,10 @@ load_config() {
|
||||
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
|
||||
[[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
|
||||
|
||||
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ] && [ "${PARAM_FORCE:-no}" = "no" ]; then
|
||||
_exiterr "Argument --force-validation can only be used in combination with --force (-x)"
|
||||
fi
|
||||
|
||||
if [ ! "${1:-}" = "noverify" ]; then
|
||||
verify_config
|
||||
fi
|
||||
@@ -539,8 +576,8 @@ init_system() {
|
||||
grep -q newOrder <<< "${CA_DIRECTORY}" && API=2 || API=1
|
||||
fi
|
||||
|
||||
if [[ ${API} -eq 1 ]]; then
|
||||
# shellcheck disable=SC2015
|
||||
# shellcheck disable=SC2015
|
||||
if [[ "${API}" = "1" ]]; then
|
||||
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)" &&
|
||||
@@ -551,7 +588,6 @@ init_system() {
|
||||
# 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)" &&
|
||||
@@ -559,8 +595,6 @@ init_system() {
|
||||
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
|
||||
CA_ACCOUNT=${CA_NEW_ACCOUNT/new-acct/acct}
|
||||
fi
|
||||
|
||||
# Export some environment variables to be used in hook script
|
||||
@@ -582,26 +616,63 @@ init_system() {
|
||||
if [[ ! "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then
|
||||
printf '\n' >&2
|
||||
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' "${CA_TERMS}" >&2
|
||||
printf 'To accept these terms of service run `%s --register --accept-terms`.\n' "${0}" >&2
|
||||
printf 'To accept these terms of service run "%s --register --accept-terms".\n' "${0}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "+ Generating account key..."
|
||||
generated="true"
|
||||
local tmp_account_key="$(_mktemp)"
|
||||
_openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}"
|
||||
local tmp_account_key
|
||||
tmp_account_key="$(_mktemp)"
|
||||
if [[ ${API} -eq 1 && ! "${ACCOUNT_KEY_ALGO}" = "rsa" ]]; then
|
||||
_exiterr "ACME API version 1 does not support EC account keys"
|
||||
fi
|
||||
case "${ACCOUNT_KEY_ALGO}" in
|
||||
rsa) _openssl genrsa -out "${tmp_account_key}" "${ACCOUNT_KEYSIZE}";;
|
||||
prime256v1|secp384r1|secp521r1) _openssl ecparam -genkey -name "${ACCOUNT_KEY_ALGO}" -out "${tmp_account_key}" -noout;;
|
||||
esac
|
||||
cat "${tmp_account_key}" > "${ACCOUNT_KEY}"
|
||||
rm "${tmp_account_key}"
|
||||
register_new_key="yes"
|
||||
fi
|
||||
fi
|
||||
"${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null || _exiterr "Account key is not valid, cannot continue."
|
||||
|
||||
# Get public components from private key and calculate thumbprint
|
||||
pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
|
||||
pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
|
||||
if ("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then
|
||||
# Get public components from private key and calculate thumbprint
|
||||
pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
|
||||
pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
|
||||
|
||||
thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
||||
account_key_info="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}")"
|
||||
account_key_sigalgo=RS256
|
||||
elif ("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then
|
||||
curve="$("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -noout -text 2>/dev/null | grep 'NIST CURVE' | cut -d':' -f2 | tr -d ' ')"
|
||||
pubkey="$("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -noout -text 2>/dev/null | tr -d '\n ' | grep -Eo 'pub:.*ASN1' | _sed -e 's/^pub://' -e 's/ASN1$//' | tr -d ':')"
|
||||
|
||||
if [ "${curve}" = "P-256" ]; then
|
||||
account_key_sigalgo="ES256"
|
||||
elif [ "${curve}" = "P-384" ]; then
|
||||
account_key_sigalgo="ES384"
|
||||
elif [ "${curve}" = "P-521" ]; then
|
||||
account_key_sigalgo="ES512"
|
||||
else
|
||||
_exiterr "Unknown account key curve: ${curve}"
|
||||
fi
|
||||
|
||||
ec_x_offset=2
|
||||
ec_x_len=$((${#pubkey}/2 - 1))
|
||||
ec_x="${pubkey:$ec_x_offset:$ec_x_len}"
|
||||
ec_x64="$(printf "%s" "${ec_x}" | hex2bin | urlbase64)"
|
||||
|
||||
ec_y_offset=$((ec_x_offset+ec_x_len))
|
||||
ec_y_len=$((${#pubkey}-ec_y_offset))
|
||||
ec_y="${pubkey:$ec_y_offset:$ec_y_len}"
|
||||
ec_y64="$(printf "%s" "${ec_y}" | hex2bin | urlbase64)"
|
||||
|
||||
account_key_info="$(printf '{"crv":"%s","kty":"EC","x":"%s","y":"%s"}' "${curve}" "${ec_x64}" "${ec_y64}")"
|
||||
else
|
||||
_exiterr "Account key is not valid, cannot continue."
|
||||
fi
|
||||
thumbprint="$(printf '%s' "${account_key_info}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
||||
|
||||
# If we generated a new private key in the step above we have to register it with the acme-server
|
||||
if [[ "${register_new_key}" = "yes" ]]; then
|
||||
@@ -654,7 +725,7 @@ init_system() {
|
||||
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_payload64="$(printf "%s" "${account_key_info}" | urlbase64)"
|
||||
eab_key="$(printf "%s" "${EAB_HMAC_KEY}" | deurlbase64 | bin2hex)"
|
||||
eab_signed64="$(printf '%s' "${eab_protected64}.${eab_payload64}" | "${OPENSSL}" dgst -binary -sha256 -mac HMAC -macopt "hexkey:${eab_key}" | urlbase64)"
|
||||
|
||||
@@ -692,16 +763,16 @@ init_system() {
|
||||
# Read account information or request from CA if missing
|
||||
if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then
|
||||
if [[ ${API} -eq 1 ]]; then
|
||||
ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | jsonsh | get_json_int_value id)"
|
||||
ACCOUNT_ID="$(jsonsh < "${ACCOUNT_KEY_JSON}" | get_json_int_value id)"
|
||||
ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}"
|
||||
else
|
||||
if [[ -e "${ACCOUNT_ID_JSON}" ]]; then
|
||||
ACCOUNT_URL="$(cat "${ACCOUNT_ID_JSON}" | jsonsh | get_json_string_value url)"
|
||||
ACCOUNT_URL="$(jsonsh < "${ACCOUNT_ID_JSON}" | get_json_string_value url)"
|
||||
fi
|
||||
# if account URL is not storred, fetch it from the CA
|
||||
if [[ -z "${ACCOUNT_URL:-}" ]]; then
|
||||
echo "+ Fetching account URL..."
|
||||
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
|
||||
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
|
||||
if [[ -z "${ACCOUNT_URL}" ]]; then
|
||||
_exiterr "Unknown error on fetching account information"
|
||||
fi
|
||||
@@ -713,7 +784,7 @@ init_system() {
|
||||
if [[ ${API} -eq 1 ]]; then
|
||||
_exiterr "This is not implemented for ACMEv1! Consider switching to ACMEv2 :)"
|
||||
else
|
||||
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
|
||||
ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
|
||||
ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')"
|
||||
fi
|
||||
echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}"
|
||||
@@ -734,7 +805,7 @@ _exiterr() {
|
||||
if [ -n "${1:-}" ]; then
|
||||
echo "ERROR: ${1}" >&2
|
||||
fi
|
||||
[[ "${skip_exit_hook:-no}" = "no" ]] && [[ -n "${HOOK:-}" ]] && ("${HOOK}" "exit_hook" "${1}" || echo 'exit_hook returned with non-zero exit code!' >&2)
|
||||
[[ "${skip_exit_hook:-no}" = "no" ]] && [[ -n "${HOOK:-}" ]] && ("${HOOK}" "exit_hook" "${1:-}" || echo 'exit_hook returned with non-zero exit code!' >&2)
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -762,7 +833,8 @@ deurlbase64() {
|
||||
# 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')"
|
||||
# shellcheck disable=SC2059
|
||||
printf "%b" "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
|
||||
}
|
||||
|
||||
# Convert binary data to hex string
|
||||
@@ -797,6 +869,7 @@ http_request() {
|
||||
fi
|
||||
|
||||
set +e
|
||||
# shellcheck disable=SC2086
|
||||
if [[ "${1}" = "head" ]]; then
|
||||
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
|
||||
curlret="${?}"
|
||||
@@ -836,8 +909,8 @@ http_request() {
|
||||
|
||||
# An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
|
||||
if [[ -n "${HOOK}" ]]; then
|
||||
errtxt="$(cat ${tempcont})"
|
||||
errheaders="$(cat ${tempheaders})"
|
||||
errtxt="$(cat "${tempcont}")"
|
||||
errheaders="$(cat "${tempheaders}")"
|
||||
"${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" || _exiterr 'request_failure hook returned with non-zero exit code'
|
||||
fi
|
||||
|
||||
@@ -865,14 +938,11 @@ signed_request() {
|
||||
|
||||
# Retrieve nonce from acme-server
|
||||
if [[ ${API} -eq 1 ]]; then
|
||||
nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
|
||||
nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | cut -d':' -f2- | tr -d ' \t\n\r')"
|
||||
else
|
||||
nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
|
||||
nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | cut -d':' -f2- | tr -d ' \t\n\r')"
|
||||
fi
|
||||
|
||||
# Build header with just our public key and algorithm information
|
||||
header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
|
||||
|
||||
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}"'"}'
|
||||
@@ -880,17 +950,37 @@ signed_request() {
|
||||
else
|
||||
# Build another header which also contains the previously received nonce and url and encode it as urlbase64
|
||||
if [[ -n "${ACCOUNT_URL:-}" ]]; then
|
||||
protected='{"alg": "RS256", "kid": "'"${ACCOUNT_URL}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
|
||||
protected='{"alg": "'"${account_key_sigalgo}"'", "kid": "'"${ACCOUNT_URL}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
|
||||
else
|
||||
protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "url": "'"${1}"'", "nonce": "'"${nonce}"'"}'
|
||||
protected='{"alg": "'"${account_key_sigalgo}"'", "jwk": '"${account_key_info}"', "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)"
|
||||
if [[ "${account_key_sigalgo}" = "RS256" ]]; then
|
||||
signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)"
|
||||
else
|
||||
dgstparams="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha${account_key_sigalgo:2} -sign "${ACCOUNT_KEY}" | "${OPENSSL}" asn1parse -inform DER)"
|
||||
dgst_parm_1="$(echo "$dgstparams" | head -n 2 | tail -n 1 | cut -d':' -f4)"
|
||||
dgst_parm_2="$(echo "$dgstparams" | head -n 3 | tail -n 1 | cut -d':' -f4)"
|
||||
|
||||
# zero-padding (doesn't seem to be necessary, but other clients are doing this as well...
|
||||
case "${account_key_sigalgo}" in
|
||||
"ES256") siglen=64;;
|
||||
"ES384") siglen=96;;
|
||||
"ES512") siglen=132;;
|
||||
esac
|
||||
while [[ ${#dgst_parm_1} -lt $siglen ]]; do dgst_parm_1="0${dgst_parm_1}"; done
|
||||
while [[ ${#dgst_parm_2} -lt $siglen ]]; do dgst_parm_2="0${dgst_parm_2}"; done
|
||||
|
||||
signed64="$(printf "%s%s" "${dgst_parm_1}" "${dgst_parm_2}" | hex2bin | urlbase64)"
|
||||
fi
|
||||
|
||||
if [[ ${API} -eq 1 ]]; then
|
||||
# Build header with just our public key and algorithm information
|
||||
header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
|
||||
|
||||
# Send header + extended header + payload + signature to the acme-server
|
||||
data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
|
||||
else
|
||||
@@ -917,23 +1007,23 @@ 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
|
||||
altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.* CN ?= ?([^ /,]*).*/\1/' )"
|
||||
altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.*[ /]CN ?= ?([^ /,]*).*/\1/' )"
|
||||
printf "%s" "${altnames}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get last issuer CN in certificate chain
|
||||
get_last_cn() {
|
||||
<<<"${1}" _sed 'H;/-----BEGIN CERTIFICATE-----/h;$!d;x' | "${OPENSSL}" x509 -noout -issuer | head -n1 | _sed -e 's/.* CN ?= ?([^/,]*).*/\1/'
|
||||
<<<"${1}" _sed 'H;/-----BEGIN CERTIFICATE-----/h;$!d;x' | "${OPENSSL}" x509 -noout -issuer | head -n1 | _sed -e 's/.*[ /]CN ?= ?([^/,]*).*/\1/'
|
||||
}
|
||||
|
||||
# Create certificate for domain(s) and outputs it FD 3
|
||||
@@ -968,12 +1058,16 @@ 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%, }]"
|
||||
|
||||
echo " + Requesting new certificate order from CA..."
|
||||
order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')"
|
||||
order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
|
||||
result="$(signed_request "${order_location}" "" | jsonsh)"
|
||||
|
||||
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
|
||||
@@ -1001,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)
|
||||
@@ -1010,9 +1105,13 @@ sign_csr() {
|
||||
fi
|
||||
|
||||
# Check if authorization has already been validated
|
||||
if [ "$(echo "${response}" | _sed 's/"challenges": \[\{.*\}\]//' | get_json_string_value status)" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
|
||||
echo " + Found valid authorization for ${identifier}"
|
||||
continue
|
||||
if [ "$(echo "${response}" | get_json_string_value status)" = "valid" ]; then
|
||||
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ]; then
|
||||
echo " + A valid authorization has been found but will be ignored"
|
||||
else
|
||||
echo " + Found valid authorization for ${identifier}"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Find challenge in authorization
|
||||
@@ -1025,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" ] && [ "${CHALLENGETYPE}" = "tls-alpn-01" ] ; 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
|
||||
@@ -1052,13 +1155,17 @@ sign_csr() {
|
||||
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
||||
;;
|
||||
"tls-alpn-01")
|
||||
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $2}')"
|
||||
generate_alpn_certificate "${identifier}" "${keyauth_hook}"
|
||||
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')"
|
||||
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" ] && [ "${CHALLENGETYPE}" = "tls-alpn-01" ]; 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
|
||||
@@ -1069,11 +1176,13 @@ sign_csr() {
|
||||
if [[ ${num_pending_challenges} -ne 0 ]]; then
|
||||
echo " + Deploying challenge tokens..."
|
||||
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then
|
||||
# shellcheck disable=SC2068
|
||||
"${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
|
||||
elif [[ -n "${HOOK}" ]]; then
|
||||
# Run hook script to deploy the challenge token
|
||||
local idx=0
|
||||
while [ ${idx} -lt ${num_pending_challenges} ]; do
|
||||
# shellcheck disable=SC2086
|
||||
"${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
|
||||
idx=$((idx+1))
|
||||
done
|
||||
@@ -1120,6 +1229,7 @@ sign_csr() {
|
||||
echo " + Cleaning challenge tokens..."
|
||||
|
||||
# Clean challenge tokens using chained hook
|
||||
# shellcheck disable=SC2068
|
||||
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
|
||||
|
||||
# Clean remaining challenge tokens if validation has failed
|
||||
@@ -1130,6 +1240,7 @@ sign_csr() {
|
||||
# Delete alpn verification certificates
|
||||
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
|
||||
# Clean challenge token using non-chained hook
|
||||
# shellcheck disable=SC2086
|
||||
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
|
||||
idx=$((idx+1))
|
||||
done
|
||||
@@ -1177,8 +1288,8 @@ sign_csr() {
|
||||
if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then
|
||||
foundaltchain=1
|
||||
fi
|
||||
if [ "${foundaltchain}" = "0" ]; then
|
||||
while read altcrturl; do
|
||||
if [ "${foundaltchain}" = "0" ] && (grep -Ei '^link:' "${resheaders}" | grep -q -Ei 'rel="alternate"'); then
|
||||
while read -r altcrturl; do
|
||||
if [ "${foundaltchain}" = "0" ]; then
|
||||
altcrt="$(signed_request "${altcrturl}" "")"
|
||||
altcn="$(get_last_cn "${altcrt}")"
|
||||
@@ -1266,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
|
||||
@@ -1277,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 "[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
|
||||
printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:${acmevalidation}\n" >> "${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}"
|
||||
@@ -1312,10 +1431,11 @@ sign_domain() {
|
||||
if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
|
||||
echo " + Generating private key..."
|
||||
privkey="privkey-${timestamp}.pem"
|
||||
local tmp_privkey="$(_mktemp)"
|
||||
local tmp_privkey
|
||||
tmp_privkey="$(_mktemp)"
|
||||
case "${KEY_ALGO}" in
|
||||
rsa) _openssl genrsa -out "${tmp_privkey}" "${KEYSIZE}";;
|
||||
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}";;
|
||||
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}" -noout;;
|
||||
esac
|
||||
cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem"
|
||||
rm "${tmp_privkey}"
|
||||
@@ -1332,7 +1452,7 @@ sign_domain() {
|
||||
echo " + Generating private rollover key..."
|
||||
case "${KEY_ALGO}" in
|
||||
rsa) _openssl genrsa -out "${certdir}/privkey.roll.pem" "${KEYSIZE}";;
|
||||
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey.roll.pem";;
|
||||
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey.roll.pem" -noout;;
|
||||
esac
|
||||
fi
|
||||
# delete rolloverkeys if disabled
|
||||
@@ -1345,17 +1465,25 @@ 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}, "
|
||||
else
|
||||
SAN="${SAN}DNS:${altname}, "
|
||||
fi
|
||||
done
|
||||
if [[ "${domain}" =~ ^ip: ]]; then
|
||||
SUBJ="/CN=${domain:3}/"
|
||||
else
|
||||
SUBJ="/CN=${domain}/"
|
||||
fi
|
||||
SAN="${SAN%%, }"
|
||||
local tmp_openssl_cnf
|
||||
tmp_openssl_cnf="$(_mktemp)"
|
||||
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
|
||||
printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
|
||||
printf "\n[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
|
||||
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:
|
||||
@@ -1426,20 +1554,21 @@ command_version() {
|
||||
revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")"
|
||||
echo "GIT-Revision: ${revision}"
|
||||
echo ""
|
||||
if [[ "${OSTYPE}" =~ "BSD" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then
|
||||
echo "OS: $(uname -sr)"
|
||||
elif [[ -e /etc/os-release ]]; then
|
||||
( . /etc/os-release && echo "OS: $PRETTY_NAME" )
|
||||
elif [[ -e /usr/lib/os-release ]]; then
|
||||
( . /usr/lib/os-release && echo "OS: $PRETTY_NAME" )
|
||||
else
|
||||
echo "OS: $(cat /etc/issue | grep -v ^$ | head -n1 | _sed 's/\\(r|n|l) .*//g')"
|
||||
echo "OS: $(grep -v '^$' /etc/issue | head -n1 | _sed 's/\\(r|n|l) .*//g')"
|
||||
fi
|
||||
echo "Used software:"
|
||||
[[ -n "${BASH_VERSION:-}" ]] && echo " bash: ${BASH_VERSION}"
|
||||
[[ -n "${ZSH_VERSION:-}" ]] && echo " zsh: ${ZSH_VERSION}"
|
||||
echo " curl: ${CURL_VERSION}"
|
||||
if [[ "${OSTYPE}" =~ "BSD" ]]; then
|
||||
if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then
|
||||
echo " awk, sed, mktemp, grep, diff: BSD base system versions"
|
||||
else
|
||||
echo " awk: $(awk -W version 2>&1 | head -n1)"
|
||||
@@ -1518,6 +1647,20 @@ command_account() {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse contents of domains.txt and domains.txt.d
|
||||
parse_domains_txt() {
|
||||
# Allow globbing temporarily
|
||||
noglob_set
|
||||
local inputs=("${DOMAINS_TXT}" "${DOMAINS_TXT}.d"/*.txt)
|
||||
noglob_clear
|
||||
|
||||
cat "${inputs[@]}" |
|
||||
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)
|
||||
}
|
||||
|
||||
# Usage: --cron (-c)
|
||||
# Description: Sign/renew non-existent/changed/expiring certificates.
|
||||
command_sign_domains() {
|
||||
@@ -1535,9 +1678,9 @@ command_sign_domains() {
|
||||
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
|
||||
DOMAINS_TXT="$(_mktemp)"
|
||||
if [[ -n "${PARAM_ALIAS:-}" ]]; then
|
||||
printf -- "${PARAM_DOMAIN} > ${PARAM_ALIAS}" > "${DOMAINS_TXT}"
|
||||
printf "%s > %s" "${PARAM_DOMAIN}" "${PARAM_ALIAS}" > "${DOMAINS_TXT}"
|
||||
else
|
||||
printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
|
||||
printf "%s" "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
|
||||
fi
|
||||
elif [[ -e "${DOMAINS_TXT}" ]]; then
|
||||
if [[ ! -r "${DOMAINS_TXT}" ]]; then
|
||||
@@ -1550,17 +1693,17 @@ 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 $(parse_domains_txt); do
|
||||
reset_configvars
|
||||
IFS="${ORIGIFS}"
|
||||
alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)"
|
||||
line="$(_sed -e 's/>[^ ]+[ ]*//g' <<< "${line}")"
|
||||
aliascount="$(grep -Eo '>' <<< "${alias}" | awk 'END {print NR}' || true )"
|
||||
[ ${aliascount} -gt 1 ] && _exiterr "Only one alias per line is allowed in domains.txt!"
|
||||
[ "${aliascount}" -gt 1 ] && _exiterr "Only one alias per line is allowed in domains.txt!"
|
||||
|
||||
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
|
||||
morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
|
||||
[ ${aliascount} -lt 1 ] && alias="${domain}" || alias="${alias#>}"
|
||||
[ "${aliascount}" -lt 1 ] && alias="${domain}" || alias="${alias#>}"
|
||||
export alias
|
||||
|
||||
if [[ -z "${morenames}" ]];then
|
||||
@@ -1614,6 +1757,8 @@ command_sign_domains() {
|
||||
); do
|
||||
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
|
||||
config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")"
|
||||
# All settings that are allowed here should also be stored and
|
||||
# restored in store_configvars() and reset_configvars()
|
||||
case "${config_var}" in
|
||||
KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
|
||||
echo " + ${config_var} = ${config_value}"
|
||||
@@ -1646,12 +1791,12 @@ command_sign_domains() {
|
||||
fi
|
||||
|
||||
# Check domain names of existing certificate
|
||||
if [[ -e "${cert}" ]]; then
|
||||
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
|
||||
@@ -1692,13 +1837,14 @@ command_sign_domains() {
|
||||
if [[ ! "${skip}" = "yes" ]]; then
|
||||
update_ocsp="yes"
|
||||
[[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
|
||||
# shellcheck disable=SC2086
|
||||
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
|
||||
skip_exit_hook=yes
|
||||
sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} &
|
||||
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} &
|
||||
wait $! || exit_with_errorcode=1
|
||||
skip_exit_hook=no
|
||||
else
|
||||
sign_domain "${certdir}" ${timestamp} ${domain} ${morenames}
|
||||
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames}
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1744,12 +1890,12 @@ command_sign_domains() {
|
||||
# Usage: --signcsr (-s) path/to/csr.pem
|
||||
# Description: Sign a given CSR, output CRT on stdout (advanced usage)
|
||||
command_sign_csr() {
|
||||
init_system
|
||||
|
||||
# redirect stdout to stderr
|
||||
# leave stdout over at fd 3 to output the cert
|
||||
exec 3>&1 1>&2
|
||||
|
||||
init_system
|
||||
|
||||
# load csr
|
||||
csrfile="${1}"
|
||||
if [ ! -r "${csrfile}" ]; then
|
||||
@@ -1762,6 +1908,7 @@ command_sign_csr() {
|
||||
|
||||
# gen cert
|
||||
certfile="$(_mktemp)"
|
||||
# shellcheck disable=SC2086
|
||||
sign_csr "${csr}" ${altnames} 3> "${certfile}"
|
||||
|
||||
# print cert
|
||||
@@ -1866,7 +2013,7 @@ command_cleanup() {
|
||||
fi
|
||||
|
||||
# Allow globbing
|
||||
[[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f
|
||||
noglob_set
|
||||
|
||||
# Loop over all certificate directories
|
||||
for certdir in "${CERTDIR}/"*; do
|
||||
@@ -1907,7 +2054,6 @@ command_cleanup() {
|
||||
# Check if current file is in use, if unused move to archive directory
|
||||
filename="$(basename "${file}")"
|
||||
if [[ ! "${filename}" = "${current}" ]] && [[ -f "${certdir}/${filename}" ]]; then
|
||||
echo "${filename}"
|
||||
if [[ "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then
|
||||
echo "Deleting unused file: ${certname}/${filename}"
|
||||
rm "${certdir}/${filename}"
|
||||
@@ -1980,8 +2126,7 @@ main() {
|
||||
fi
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2199
|
||||
[[ -z "${@}" ]] && eval set -- "--help"
|
||||
[[ -z "${*}" ]] && eval set -- "--help"
|
||||
|
||||
while (( ${#} )); do
|
||||
case "${1}" in
|
||||
@@ -2107,6 +2252,12 @@ main() {
|
||||
PARAM_FORCE="yes"
|
||||
;;
|
||||
|
||||
# PARAM_Usage: --force-validation
|
||||
# PARAM_Description: Force revalidation of domain names (used in combination with --force)
|
||||
--force-validation)
|
||||
PARAM_FORCE_VALIDATION="yes"
|
||||
;;
|
||||
|
||||
# PARAM_Usage: --no-lock (-n)
|
||||
# PARAM_Description: Don't use lockfile (potentially dangerous!)
|
||||
--no-lock|-n)
|
||||
@@ -2183,8 +2334,8 @@ main() {
|
||||
PARAM_ALPNCERTDIR="${1}"
|
||||
;;
|
||||
|
||||
# PARAM_Usage: --challenge (-t) http-01|dns-01
|
||||
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
|
||||
# PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01
|
||||
# PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
|
||||
--challenge|-t)
|
||||
shift 1
|
||||
check_parameters "${1:-}"
|
||||
|
||||
@@ -34,6 +34,30 @@ under your `CERTDIR`.
|
||||
example.net www.example.net wiki.example.net > certalias
|
||||
```
|
||||
|
||||
This allows to set per certificates options. The options you can change are
|
||||
explained in [Per Certificate Config](per-certificate-config.md).
|
||||
|
||||
If you want to create different certificate types for the same domain
|
||||
you can use:
|
||||
|
||||
```text
|
||||
*.service.example.org service.example.org > star_service_example_org_rsa
|
||||
*.service.example.org service.example.org > star_service_example_org_ecdsa
|
||||
```
|
||||
|
||||
Then add a config file `certs/star_service_example_org_rsa/config` with
|
||||
the value
|
||||
|
||||
```
|
||||
KEY_ALGO="rsa"
|
||||
```
|
||||
|
||||
or respectively
|
||||
|
||||
```
|
||||
KEY_ALGO="ecdsa"
|
||||
```
|
||||
|
||||
### Wildcards
|
||||
|
||||
Support for wildcards was added by the ACME v2 protocol.
|
||||
@@ -70,3 +94,14 @@ This creates two certificates one for `service.example.com` with an
|
||||
**Note:** The first certificate is valid for both `service.example.com` and for
|
||||
`*.service.example.com` which can be a useful way to create wildcard
|
||||
certificates.
|
||||
|
||||
### Drop-in directory
|
||||
|
||||
If a directory named `domains.txt.d` exists in the same location as
|
||||
`domains.txt`, the contents of `*.txt` files in that directory are appended to
|
||||
the list of domains, in alphabetical order of the filenames. This is useful for
|
||||
automation, as it doesn't require editing an existing file to add new domains.
|
||||
|
||||
Warning: Behaviour of this might change as the naming between `domains.txt.d`
|
||||
and the `DOMAINS_D` config variable (which is used for per-certificate
|
||||
configuration) is a bit confusing.
|
||||
|
||||
@@ -24,6 +24,15 @@ example.net www.example.net > certalias
|
||||
# NOTE: It is a certificate for 'service.example.org'
|
||||
*.service.example.org service.example.org > star_service_example_org
|
||||
|
||||
# Optionally you can also append the certificate algorithm here to create
|
||||
# multiple certificate types for the same domain.
|
||||
#
|
||||
# This allows to set per certificates options. How to do this is
|
||||
# explained in [domains.txt documentation](domains_txt.md).
|
||||
#
|
||||
*.service.example.org service.example.org > star_service_example_org_rsa
|
||||
*.service.example.org service.example.org > star_service_example_org_ecdsa
|
||||
|
||||
# Create a certificate for 'service.example.net' with an alternative name of
|
||||
# '*.service.example.net' (which is a wildcard domain) and store it in the
|
||||
# directory ${CERTDIR}/service.example.net
|
||||
|
||||
@@ -1,199 +1,199 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
deploy_challenge() {
|
||||
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
|
||||
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
|
||||
|
||||
# This hook is called once for every domain that needs to be
|
||||
# validated, including any alternative names you may have listed.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The domain name (CN or subject alternative name) being
|
||||
# validated.
|
||||
# - TOKEN_FILENAME
|
||||
# The name of the file containing the token to be served for HTTP
|
||||
# validation. Should be served by your web server as
|
||||
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
|
||||
# - TOKEN_VALUE
|
||||
# The token value that needs to be served for validation. For DNS
|
||||
# validation, this is what you want to put in the _acme-challenge
|
||||
# TXT record. For HTTP validation it is the value that is expected
|
||||
# be found in the $TOKEN_FILENAME file.
|
||||
# This hook is called once for every domain that needs to be
|
||||
# validated, including any alternative names you may have listed.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The domain name (CN or subject alternative name) being
|
||||
# validated.
|
||||
# - TOKEN_FILENAME
|
||||
# The name of the file containing the token to be served for HTTP
|
||||
# validation. Should be served by your web server as
|
||||
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
|
||||
# - TOKEN_VALUE
|
||||
# The token value that needs to be served for validation. For DNS
|
||||
# validation, this is what you want to put in the _acme-challenge
|
||||
# TXT record. For HTTP validation it is the value that is expected
|
||||
# be found in the $TOKEN_FILENAME file.
|
||||
|
||||
# Simple example: Use nsupdate with local named
|
||||
# printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
|
||||
# Simple example: Use nsupdate with local named
|
||||
# printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
|
||||
}
|
||||
|
||||
clean_challenge() {
|
||||
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
|
||||
local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}"
|
||||
|
||||
# This hook is called after attempting to validate each domain,
|
||||
# whether or not validation was successful. Here you can delete
|
||||
# files or DNS records that are no longer needed.
|
||||
#
|
||||
# The parameters are the same as for deploy_challenge.
|
||||
# This hook is called after attempting to validate each domain,
|
||||
# whether or not validation was successful. Here you can delete
|
||||
# files or DNS records that are no longer needed.
|
||||
#
|
||||
# The parameters are the same as for deploy_challenge.
|
||||
|
||||
# Simple example: Use nsupdate with local named
|
||||
# printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
|
||||
# Simple example: Use nsupdate with local named
|
||||
# printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key
|
||||
}
|
||||
|
||||
sync_cert() {
|
||||
local KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}"
|
||||
local KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}"
|
||||
|
||||
# This hook is called after the certificates have been created but before
|
||||
# they are symlinked. This allows you to sync the files to disk to prevent
|
||||
# creating a symlink to empty files on unexpected system crashes.
|
||||
#
|
||||
# This hook is not intended to be used for further processing of certificate
|
||||
# files, see deploy_cert for that.
|
||||
#
|
||||
# Parameters:
|
||||
# - KEYFILE
|
||||
# The path of the file containing the private key.
|
||||
# - CERTFILE
|
||||
# The path of the file containing the signed certificate.
|
||||
# - FULLCHAINFILE
|
||||
# The path of the file containing the full certificate chain.
|
||||
# - CHAINFILE
|
||||
# The path of the file containing the intermediate certificate(s).
|
||||
# - REQUESTFILE
|
||||
# The path of the file containing the certificate signing request.
|
||||
# This hook is called after the certificates have been created but before
|
||||
# they are symlinked. This allows you to sync the files to disk to prevent
|
||||
# creating a symlink to empty files on unexpected system crashes.
|
||||
#
|
||||
# This hook is not intended to be used for further processing of certificate
|
||||
# files, see deploy_cert for that.
|
||||
#
|
||||
# Parameters:
|
||||
# - KEYFILE
|
||||
# The path of the file containing the private key.
|
||||
# - CERTFILE
|
||||
# The path of the file containing the signed certificate.
|
||||
# - FULLCHAINFILE
|
||||
# The path of the file containing the full certificate chain.
|
||||
# - CHAINFILE
|
||||
# The path of the file containing the intermediate certificate(s).
|
||||
# - REQUESTFILE
|
||||
# The path of the file containing the certificate signing request.
|
||||
|
||||
# Simple example: sync the files before symlinking them
|
||||
# sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
|
||||
# Simple example: sync the files before symlinking them
|
||||
# sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
|
||||
}
|
||||
|
||||
deploy_cert() {
|
||||
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
|
||||
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}"
|
||||
|
||||
# This hook is called once for each certificate that has been
|
||||
# produced. Here you might, for instance, copy your new certificates
|
||||
# to service-specific locations and reload the service.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - KEYFILE
|
||||
# The path of the file containing the private key.
|
||||
# - CERTFILE
|
||||
# The path of the file containing the signed certificate.
|
||||
# - FULLCHAINFILE
|
||||
# The path of the file containing the full certificate chain.
|
||||
# - CHAINFILE
|
||||
# The path of the file containing the intermediate certificate(s).
|
||||
# - TIMESTAMP
|
||||
# Timestamp when the specified certificate was created.
|
||||
# This hook is called once for each certificate that has been
|
||||
# produced. Here you might, for instance, copy your new certificates
|
||||
# to service-specific locations and reload the service.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - KEYFILE
|
||||
# The path of the file containing the private key.
|
||||
# - CERTFILE
|
||||
# The path of the file containing the signed certificate.
|
||||
# - FULLCHAINFILE
|
||||
# The path of the file containing the full certificate chain.
|
||||
# - CHAINFILE
|
||||
# The path of the file containing the intermediate certificate(s).
|
||||
# - TIMESTAMP
|
||||
# Timestamp when the specified certificate was created.
|
||||
|
||||
# Simple example: Copy file to nginx config
|
||||
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
||||
# systemctl reload nginx
|
||||
# Simple example: Copy file to nginx config
|
||||
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
||||
# systemctl reload nginx
|
||||
}
|
||||
|
||||
deploy_ocsp() {
|
||||
local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
|
||||
local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
|
||||
|
||||
# This hook is called once for each updated ocsp stapling file that has
|
||||
# been produced. Here you might, for instance, copy your new ocsp stapling
|
||||
# files to service-specific locations and reload the service.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - OCSPFILE
|
||||
# The path of the ocsp stapling file
|
||||
# - TIMESTAMP
|
||||
# Timestamp when the specified ocsp stapling file was created.
|
||||
# This hook is called once for each updated ocsp stapling file that has
|
||||
# been produced. Here you might, for instance, copy your new ocsp stapling
|
||||
# files to service-specific locations and reload the service.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - OCSPFILE
|
||||
# The path of the ocsp stapling file
|
||||
# - TIMESTAMP
|
||||
# Timestamp when the specified ocsp stapling file was created.
|
||||
|
||||
# Simple example: Copy file to nginx config
|
||||
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
||||
# systemctl reload nginx
|
||||
# Simple example: Copy file to nginx config
|
||||
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
||||
# systemctl reload nginx
|
||||
}
|
||||
|
||||
|
||||
unchanged_cert() {
|
||||
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
|
||||
local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}"
|
||||
|
||||
# This hook is called once for each certificate that is still
|
||||
# valid and therefore wasn't reissued.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - KEYFILE
|
||||
# The path of the file containing the private key.
|
||||
# - CERTFILE
|
||||
# The path of the file containing the signed certificate.
|
||||
# - FULLCHAINFILE
|
||||
# The path of the file containing the full certificate chain.
|
||||
# - CHAINFILE
|
||||
# The path of the file containing the intermediate certificate(s).
|
||||
# This hook is called once for each certificate that is still
|
||||
# valid and therefore wasn't reissued.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - KEYFILE
|
||||
# The path of the file containing the private key.
|
||||
# - CERTFILE
|
||||
# The path of the file containing the signed certificate.
|
||||
# - FULLCHAINFILE
|
||||
# The path of the file containing the full certificate chain.
|
||||
# - CHAINFILE
|
||||
# The path of the file containing the intermediate certificate(s).
|
||||
}
|
||||
|
||||
invalid_challenge() {
|
||||
local DOMAIN="${1}" RESPONSE="${2}"
|
||||
local DOMAIN="${1}" RESPONSE="${2}"
|
||||
|
||||
# This hook is called if the challenge response has failed, so domain
|
||||
# owners can be aware and act accordingly.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - RESPONSE
|
||||
# The response that the verification server returned
|
||||
# This hook is called if the challenge response has failed, so domain
|
||||
# owners can be aware and act accordingly.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain name, i.e. the certificate common
|
||||
# name (CN).
|
||||
# - RESPONSE
|
||||
# The response that the verification server returned
|
||||
|
||||
# Simple example: Send mail to root
|
||||
# printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root
|
||||
# Simple example: Send mail to root
|
||||
# printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root
|
||||
}
|
||||
|
||||
request_failure() {
|
||||
local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"
|
||||
local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}"
|
||||
|
||||
# This hook is called when an HTTP request fails (e.g., when the ACME
|
||||
# server is busy, returns an error, etc). It will be called upon any
|
||||
# response code that does not start with '2'. Useful to alert admins
|
||||
# about problems with requests.
|
||||
#
|
||||
# Parameters:
|
||||
# - STATUSCODE
|
||||
# The HTML status code that originated the error.
|
||||
# - REASON
|
||||
# The specified reason for the error.
|
||||
# - REQTYPE
|
||||
# The kind of request that was made (GET, POST...)
|
||||
# - HEADERS
|
||||
# HTTP headers returned by the CA
|
||||
# This hook is called when an HTTP request fails (e.g., when the ACME
|
||||
# server is busy, returns an error, etc). It will be called upon any
|
||||
# response code that does not start with '2'. Useful to alert admins
|
||||
# about problems with requests.
|
||||
#
|
||||
# Parameters:
|
||||
# - STATUSCODE
|
||||
# The HTML status code that originated the error.
|
||||
# - REASON
|
||||
# The specified reason for the error.
|
||||
# - REQTYPE
|
||||
# The kind of request that was made (GET, POST...)
|
||||
# - HEADERS
|
||||
# HTTP headers returned by the CA
|
||||
|
||||
# Simple example: Send mail to root
|
||||
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
|
||||
# Simple example: Send mail to root
|
||||
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
|
||||
}
|
||||
|
||||
generate_csr() {
|
||||
local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}"
|
||||
local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}"
|
||||
|
||||
# This hook is called before any certificate signing operation takes place.
|
||||
# It can be used to generate or fetch a certificate signing request with external
|
||||
# tools.
|
||||
# The output should be just the certificate signing request formatted as PEM.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain as specified in domains.txt. This does not need to
|
||||
# match with the domains in the CSR, it's basically just the directory name.
|
||||
# - CERTDIR
|
||||
# Certificate output directory for this particular certificate. Can be used
|
||||
# for storing additional files.
|
||||
# - ALTNAMES
|
||||
# All domain names for the current certificate as specified in domains.txt.
|
||||
# Again, this doesn't need to match with the CSR, it's just there for convenience.
|
||||
# This hook is called before any certificate signing operation takes place.
|
||||
# It can be used to generate or fetch a certificate signing request with external
|
||||
# tools.
|
||||
# The output should be just the certificate signing request formatted as PEM.
|
||||
#
|
||||
# Parameters:
|
||||
# - DOMAIN
|
||||
# The primary domain as specified in domains.txt. This does not need to
|
||||
# match with the domains in the CSR, it's basically just the directory name.
|
||||
# - CERTDIR
|
||||
# Certificate output directory for this particular certificate. Can be used
|
||||
# for storing additional files.
|
||||
# - ALTNAMES
|
||||
# All domain names for the current certificate as specified in domains.txt.
|
||||
# Again, this doesn't need to match with the CSR, it's just there for convenience.
|
||||
|
||||
# Simple example: Look for pre-generated CSRs
|
||||
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
|
||||
# cat "${CERTDIR}/pre-generated.csr"
|
||||
# fi
|
||||
# Simple example: Look for pre-generated CSRs
|
||||
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
|
||||
# cat "${CERTDIR}/pre-generated.csr"
|
||||
# fi
|
||||
}
|
||||
|
||||
startup_hook() {
|
||||
|
||||
BIN
docs/logo.jpg
BIN
docs/logo.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
BIN
docs/logo.png
Normal file
BIN
docs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -11,6 +11,8 @@ Currently supported options:
|
||||
- KEY_ALGO
|
||||
- KEYSIZE
|
||||
- OCSP_MUST_STAPLE
|
||||
- OCSP_FETCH
|
||||
- OCSP_DAYS
|
||||
- CHALLENGETYPE
|
||||
- HOOK
|
||||
- HOOK_CHAIN
|
||||
|
||||
@@ -8,10 +8,7 @@ you will quickly hit these limits and find yourself locked out.
|
||||
To avoid this, please set the CA property to the Let’s Encrypt staging server URL in your config file:
|
||||
|
||||
```bash
|
||||
CA="https://acme-staging.api.letsencrypt.org/directory"
|
||||
CA="https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
```
|
||||
|
||||
# ACMEv2 staging
|
||||
|
||||
You can use `CA="https://acme-staging-v02.api.letsencrypt.org/directory"` to test dehydrated with
|
||||
the ACMEv2 staging endpoint.
|
||||
Alternatively you can define the CA using the CLI argument `--ca letsencrypt-test` (`letsencrypt-test` is an integrated preset-CA corresponding to the URL above).
|
||||
|
||||
@@ -6,6 +6,26 @@ It will do that for any (sub-)domain you want to sign a certificate for.
|
||||
|
||||
Dehydrated generates the required verification certificates, but the delivery is out of its scope.
|
||||
|
||||
### Example lighttpd config
|
||||
|
||||
lighttpd can be configured to recognize ALPN `acme-tls/1` and to respond to such
|
||||
requests using the specially crafted TLS certificates generated by dehydrated.
|
||||
Configure lighttpd and dehydrated to use the same path for these certificates.
|
||||
(Be sure to allow read access to the user account under which the lighttpd
|
||||
server is running.) `mkdir -p /etc/dehydrated/alpn-certs`
|
||||
|
||||
lighttpd.conf:
|
||||
```
|
||||
ssl.acme-tls-1 = "/etc/dehydrated/alpn-certs"
|
||||
```
|
||||
|
||||
When renewing certificates, specify `-t tls-alpn-01` and `--alpn /etc/dehydrated/alpn-certs` to dehydrated, e.g.
|
||||
```
|
||||
dehydrated -t tls-alpn-01 --alpn /etc/dehydrated/alpn-certs -c --out /etc/lighttpd/certs -d www.example.com
|
||||
# gracefully reload lighttpd to use the new certificates by sending lighttpd pid SIGUSR1
|
||||
systemctl reload lighttpd
|
||||
```
|
||||
|
||||
### Example nginx config
|
||||
|
||||
On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls
|
||||
|
||||
Reference in New Issue
Block a user