mirror of
https://github.com/dehydrated-io/dehydrated.git
synced 2026-03-13 05:35:16 +01:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fb8eba56a | ||
|
|
19c7fbbf47 | ||
|
|
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
|
# Change Log
|
||||||
This file contains a log of major changes in dehydrated
|
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
|
## [0.7.0] - 2020-12-10
|
||||||
## Added
|
## Added
|
||||||
- Support for external account bindings
|
- Support for external account bindings
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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)
|
# 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.
|
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!
|
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!)
|
- 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
|
- Renewal if a certificate is about to expire or defined set of domains changed
|
||||||
- Certificate revocation
|
- 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,
|
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).
|
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)
|
--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
|
--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 (-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!)
|
--no-lock (-n) Don't use lockfile (potentially dangerous!)
|
||||||
--lock-suffix example.com Suffix lockfile name with a string (useful for with -d)
|
--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
|
--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
|
--preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN
|
||||||
--out (-o) certs/directory Output certificates into the specified directory
|
--out (-o) certs/directory Output certificates into the specified directory
|
||||||
--alpn alpn-certs/directory Output alpn verification 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
|
--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
|
|
||||||
|
|||||||
372
dehydrated
372
dehydrated
@@ -17,7 +17,7 @@ umask 077 # paranoid umask, we're creating private keys
|
|||||||
exec 3>&-
|
exec 3>&-
|
||||||
exec 4>&-
|
exec 4>&-
|
||||||
|
|
||||||
VERSION="0.7.0"
|
VERSION="0.7.1"
|
||||||
|
|
||||||
# Find directory in which this script is stored by traversing all symbolic links
|
# Find directory in which this script is stored by traversing all symbolic links
|
||||||
SOURCE="${0}"
|
SOURCE="${0}"
|
||||||
@@ -31,6 +31,22 @@ SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
|||||||
BASEDIR="${SCRIPTDIR}"
|
BASEDIR="${SCRIPTDIR}"
|
||||||
ORIGARGS=("${@}")
|
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
|
# Generate json.sh path matching string
|
||||||
json_path() {
|
json_path() {
|
||||||
if [ ! "${1}" = "-p" ]; then
|
if [ ! "${1}" = "-p" ]; then
|
||||||
@@ -55,7 +71,6 @@ get_json_array_values() {
|
|||||||
# Get sub-dictionary from json
|
# Get sub-dictionary from json
|
||||||
get_json_dict_value() {
|
get_json_dict_value() {
|
||||||
local filter
|
local filter
|
||||||
echo "$(json_path "${1:-}" "${2:-}")"
|
|
||||||
filter="$(printf 's/.*\[%s\][[:space:]]*\(.*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")"
|
filter="$(printf 's/.*\[%s\][[:space:]]*\(.*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")"
|
||||||
sed -n "${filter}" | jsonsh
|
sed -n "${filter}" | jsonsh
|
||||||
}
|
}
|
||||||
@@ -88,7 +103,7 @@ jsonsh() {
|
|||||||
awk_egrep () {
|
awk_egrep () {
|
||||||
local pattern_string=$1
|
local pattern_string=$1
|
||||||
|
|
||||||
gawk '{
|
awk '{
|
||||||
while ($0) {
|
while ($0) {
|
||||||
start=match($0, pattern);
|
start=match($0, pattern);
|
||||||
token=substr($0, start, RLENGTH);
|
token=substr($0, start, RLENGTH);
|
||||||
@@ -103,14 +118,15 @@ jsonsh() {
|
|||||||
local ESCAPE
|
local ESCAPE
|
||||||
local CHAR
|
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
|
then
|
||||||
GREP='egrep -ao --color=never'
|
GREP='grep -Eao --color=never'
|
||||||
else
|
else
|
||||||
GREP='egrep -ao'
|
GREP='grep -Eao'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if echo "test string" | egrep -o "test" >/dev/null 2>&1
|
# shellcheck disable=SC2196
|
||||||
|
if echo "test string" | grep -Eao "test" >/dev/null 2>&1
|
||||||
then
|
then
|
||||||
ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
|
ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})'
|
||||||
CHAR='[^[:cntrl:]"\\]'
|
CHAR='[^[:cntrl:]"\\]'
|
||||||
@@ -126,10 +142,11 @@ jsonsh() {
|
|||||||
local SPACE='[[:space:]]+'
|
local SPACE='[[:space:]]+'
|
||||||
|
|
||||||
# Force zsh to expand $A into multiple words
|
# Force zsh to expand $A into multiple words
|
||||||
local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')
|
local is_wordsplit_disabled
|
||||||
if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi
|
is_wordsplit_disabled="$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')"
|
||||||
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$"
|
if [ "${is_wordsplit_disabled}" != "0" ]; then setopt shwordsplit; fi
|
||||||
if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi
|
$GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | grep -Ev "^$SPACE$"
|
||||||
|
if [ "${is_wordsplit_disabled}" != "0" ]; then unsetopt shwordsplit; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_array () {
|
parse_array () {
|
||||||
@@ -194,17 +211,14 @@ jsonsh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parse_value () {
|
parse_value () {
|
||||||
local jpath="${1:+$1,}${2:-}" isleaf=0 isempty=0 print=0
|
local jpath="${1:+$1,}${2:-}"
|
||||||
case "$token" in
|
case "$token" in
|
||||||
'{') parse_object "$jpath" ;;
|
'{') parse_object "$jpath" ;;
|
||||||
'[') parse_array "$jpath" ;;
|
'[') parse_array "$jpath" ;;
|
||||||
# At this point, the only valid single-character tokens are digits.
|
# At this point, the only valid single-character tokens are digits.
|
||||||
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
|
''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;;
|
||||||
*) value=$token
|
*) value="${token/\\\///}"
|
||||||
# replace solidus ("\/") in json strings with normalized value: "/"
|
# replace solidus ("\/") in json strings with normalized value: "/"
|
||||||
value=$(echo "$value" | sed 's#\\/#/#g')
|
|
||||||
isleaf=1
|
|
||||||
[ "$value" = '""' ] && isempty=1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
[ "$value" = '' ] && return
|
[ "$value" = '' ] && return
|
||||||
@@ -227,10 +241,20 @@ jsonsh() {
|
|||||||
tokenize | parse
|
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
|
# Create (identifiable) temporary files
|
||||||
_mktemp() {
|
_mktemp() {
|
||||||
# shellcheck disable=SC2068
|
mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
|
||||||
mktemp ${@:-} "${TMPDIR:-/tmp}/dehydrated-XXXXXX"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check for script dependencies
|
# Check for script dependencies
|
||||||
@@ -254,7 +278,10 @@ check_dependencies() {
|
|||||||
store_configvars() {
|
store_configvars() {
|
||||||
__KEY_ALGO="${KEY_ALGO}"
|
__KEY_ALGO="${KEY_ALGO}"
|
||||||
__OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
|
__OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
|
||||||
|
__OCSP_FETCH="${OCSP_FETCH}"
|
||||||
|
__OCSP_DAYS="${OCSP_DAYS}"
|
||||||
__PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
|
__PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
|
||||||
|
__PRIVATE_KEY_ROLLOVER="${PRIVATE_KEY_ROLLOVER}"
|
||||||
__KEYSIZE="${KEYSIZE}"
|
__KEYSIZE="${KEYSIZE}"
|
||||||
__CHALLENGETYPE="${CHALLENGETYPE}"
|
__CHALLENGETYPE="${CHALLENGETYPE}"
|
||||||
__HOOK="${HOOK}"
|
__HOOK="${HOOK}"
|
||||||
@@ -269,7 +296,10 @@ store_configvars() {
|
|||||||
reset_configvars() {
|
reset_configvars() {
|
||||||
KEY_ALGO="${__KEY_ALGO}"
|
KEY_ALGO="${__KEY_ALGO}"
|
||||||
OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
|
OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
|
||||||
|
OCSP_FETCH="${__OCSP_FETCH}"
|
||||||
|
OCSP_DAYS="${__OCSP_DAYS}"
|
||||||
PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
|
PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
|
||||||
|
PRIVATE_KEY_ROLLOVER="${__PRIVATE_KEY_ROLLOVER}"
|
||||||
KEYSIZE="${__KEYSIZE}"
|
KEYSIZE="${__KEYSIZE}"
|
||||||
CHALLENGETYPE="${__CHALLENGETYPE}"
|
CHALLENGETYPE="${__CHALLENGETYPE}"
|
||||||
HOOK="${__HOOK}"
|
HOOK="${__HOOK}"
|
||||||
@@ -298,7 +328,7 @@ verify_config() {
|
|||||||
if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
|
if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
|
||||||
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
|
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
|
||||||
fi
|
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
|
if [[ -n "${IP_VERSION}" ]]; then
|
||||||
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
|
[[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue."
|
||||||
fi
|
fi
|
||||||
@@ -332,6 +362,8 @@ load_config() {
|
|||||||
CERTDIR=
|
CERTDIR=
|
||||||
ALPNCERTDIR=
|
ALPNCERTDIR=
|
||||||
ACCOUNTDIR=
|
ACCOUNTDIR=
|
||||||
|
ACCOUNT_KEYSIZE="4096"
|
||||||
|
ACCOUNT_KEY_ALGO=rsa
|
||||||
CHALLENGETYPE="http-01"
|
CHALLENGETYPE="http-01"
|
||||||
CONFIG_D=
|
CONFIG_D=
|
||||||
CURL_OPTS=
|
CURL_OPTS=
|
||||||
@@ -379,7 +411,7 @@ load_config() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Allow globbing
|
# Allow globbing
|
||||||
[[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f
|
noglob_set
|
||||||
|
|
||||||
for check_config_d in "${CONFIG_D}"/*.sh; do
|
for check_config_d in "${CONFIG_D}"/*.sh; do
|
||||||
if [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then
|
if [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then
|
||||||
@@ -392,7 +424,7 @@ load_config() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Disable globbing
|
# Disable globbing
|
||||||
[[ -n "${ZSH_VERSION:-}" ]] && set -o noglob || set -f
|
noglob_clear
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check for missing dependencies
|
# Check for missing dependencies
|
||||||
@@ -473,6 +505,7 @@ load_config() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
[[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
|
[[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
|
||||||
ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
|
ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
|
||||||
ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
|
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_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
|
||||||
[[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
|
[[ -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
|
if [ ! "${1:-}" = "noverify" ]; then
|
||||||
verify_config
|
verify_config
|
||||||
fi
|
fi
|
||||||
@@ -539,8 +576,8 @@ init_system() {
|
|||||||
grep -q newOrder <<< "${CA_DIRECTORY}" && API=2 || API=1
|
grep -q newOrder <<< "${CA_DIRECTORY}" && API=2 || API=1
|
||||||
fi
|
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_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_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" &&
|
||||||
CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" &&
|
CA_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
|
# 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}
|
CA_REG=${CA_NEW_REG/new-reg/reg}
|
||||||
else
|
else
|
||||||
# shellcheck disable=SC2015
|
|
||||||
CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" &&
|
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_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" &&
|
||||||
CA_NEW_ACCOUNT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newAccount)" &&
|
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_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)" ||
|
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."
|
_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
|
fi
|
||||||
|
|
||||||
# Export some environment variables to be used in hook script
|
# Export some environment variables to be used in hook script
|
||||||
@@ -582,26 +616,63 @@ init_system() {
|
|||||||
if [[ ! "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then
|
if [[ ! "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then
|
||||||
printf '\n' >&2
|
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 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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "+ Generating account key..."
|
echo "+ Generating account key..."
|
||||||
generated="true"
|
generated="true"
|
||||||
local tmp_account_key="$(_mktemp)"
|
local tmp_account_key
|
||||||
_openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}"
|
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}"
|
cat "${tmp_account_key}" > "${ACCOUNT_KEY}"
|
||||||
rm "${tmp_account_key}"
|
rm "${tmp_account_key}"
|
||||||
register_new_key="yes"
|
register_new_key="yes"
|
||||||
fi
|
fi
|
||||||
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
|
if ("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then
|
||||||
pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
|
# Get public components from private key and calculate thumbprint
|
||||||
pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
|
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 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
|
if [[ "${register_new_key}" = "yes" ]]; then
|
||||||
@@ -654,7 +725,7 @@ init_system() {
|
|||||||
if [[ -n "${EAB_KID:-}" ]] && [[ -n "${EAB_HMAC_KEY:-}" ]]; then
|
if [[ -n "${EAB_KID:-}" ]] && [[ -n "${EAB_HMAC_KEY:-}" ]]; then
|
||||||
eab_url="${CA_NEW_ACCOUNT}"
|
eab_url="${CA_NEW_ACCOUNT}"
|
||||||
eab_protected64="$(printf '{"alg":"HS256","kid":"%s","url":"%s"}' "${EAB_KID}" "${eab_url}" | urlbase64)"
|
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_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)"
|
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
|
# Read account information or request from CA if missing
|
||||||
if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then
|
if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then
|
||||||
if [[ ${API} -eq 1 ]]; 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}"
|
ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}"
|
||||||
else
|
else
|
||||||
if [[ -e "${ACCOUNT_ID_JSON}" ]]; then
|
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
|
fi
|
||||||
# if account URL is not storred, fetch it from the CA
|
# if account URL is not storred, fetch it from the CA
|
||||||
if [[ -z "${ACCOUNT_URL:-}" ]]; then
|
if [[ -z "${ACCOUNT_URL:-}" ]]; then
|
||||||
echo "+ Fetching account URL..."
|
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
|
if [[ -z "${ACCOUNT_URL}" ]]; then
|
||||||
_exiterr "Unknown error on fetching account information"
|
_exiterr "Unknown error on fetching account information"
|
||||||
fi
|
fi
|
||||||
@@ -713,7 +784,7 @@ init_system() {
|
|||||||
if [[ ${API} -eq 1 ]]; then
|
if [[ ${API} -eq 1 ]]; then
|
||||||
_exiterr "This is not implemented for ACMEv1! Consider switching to ACMEv2 :)"
|
_exiterr "This is not implemented for ACMEv1! Consider switching to ACMEv2 :)"
|
||||||
else
|
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}" '{}')"
|
ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')"
|
||||||
fi
|
fi
|
||||||
echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}"
|
echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}"
|
||||||
@@ -734,7 +805,7 @@ _exiterr() {
|
|||||||
if [ -n "${1:-}" ]; then
|
if [ -n "${1:-}" ]; then
|
||||||
echo "ERROR: ${1}" >&2
|
echo "ERROR: ${1}" >&2
|
||||||
fi
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,7 +833,8 @@ deurlbase64() {
|
|||||||
# Convert hex string to binary data
|
# Convert hex string to binary data
|
||||||
hex2bin() {
|
hex2bin() {
|
||||||
# Remove spaces, add leading zero, escape as hex string and parse with printf
|
# 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
|
# Convert binary data to hex string
|
||||||
@@ -797,6 +869,7 @@ http_request() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
|
# shellcheck disable=SC2086
|
||||||
if [[ "${1}" = "head" ]]; then
|
if [[ "${1}" = "head" ]]; then
|
||||||
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
|
statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
|
||||||
curlret="${?}"
|
curlret="${?}"
|
||||||
@@ -826,6 +899,10 @@ http_request() {
|
|||||||
elif [[ -n "${CA_REVOKE_CERT:-}" ]] && [[ "${2}" = "${CA_REVOKE_CERT:-}" ]] && [[ "${statuscode}" = "409" ]]; then
|
elif [[ -n "${CA_REVOKE_CERT:-}" ]] && [[ "${2}" = "${CA_REVOKE_CERT:-}" ]] && [[ "${statuscode}" = "409" ]]; then
|
||||||
grep -q "Certificate already revoked" "${tempcont}" && return
|
grep -q "Certificate already revoked" "${tempcont}" && return
|
||||||
else
|
else
|
||||||
|
if grep -q "urn:ietf:params:acme:error:badNonce" "${tempcont}"; then
|
||||||
|
printf "badnonce %s" "$(grep -Eoi "^replay-nonce:.*$" "${tempheaders}" | sed 's/ //' | cut -d: -f2)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
|
echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
|
||||||
echo >&2
|
echo >&2
|
||||||
echo "Details:" >&2
|
echo "Details:" >&2
|
||||||
@@ -836,8 +913,8 @@ http_request() {
|
|||||||
|
|
||||||
# An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
|
# An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
|
||||||
if [[ -n "${HOOK}" ]]; then
|
if [[ -n "${HOOK}" ]]; then
|
||||||
errtxt="$(cat ${tempcont})"
|
errtxt="$(cat "${tempcont}")"
|
||||||
errheaders="$(cat ${tempheaders})"
|
errheaders="$(cat "${tempheaders}")"
|
||||||
"${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" || _exiterr 'request_failure hook returned with non-zero exit code'
|
"${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" || _exiterr 'request_failure hook returned with non-zero exit code'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -863,16 +940,17 @@ signed_request() {
|
|||||||
# Encode payload as urlbase64
|
# Encode payload as urlbase64
|
||||||
payload64="$(printf '%s' "${2}" | urlbase64)"
|
payload64="$(printf '%s' "${2}" | urlbase64)"
|
||||||
|
|
||||||
# Retrieve nonce from acme-server
|
if [ -n "${3:-}" ]; then
|
||||||
if [[ ${API} -eq 1 ]]; then
|
nonce="$(printf "%s" "${3}" | tr -d ' \t\n\r')"
|
||||||
nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
|
|
||||||
else
|
else
|
||||||
nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
|
# Retrieve nonce from acme-server
|
||||||
|
if [[ ${API} -eq 1 ]]; then
|
||||||
|
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: | cut -d':' -f2- | tr -d ' \t\n\r')"
|
||||||
|
fi
|
||||||
fi
|
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
|
if [[ ${API} -eq 1 ]]; then
|
||||||
# Build another header which also contains the previously received nonce and encode it as urlbase64
|
# 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}"'"}'
|
protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}'
|
||||||
@@ -880,17 +958,37 @@ signed_request() {
|
|||||||
else
|
else
|
||||||
# Build another header which also contains the previously received nonce and url and encode it as urlbase64
|
# Build another header which also contains the previously received nonce and url and encode it as urlbase64
|
||||||
if [[ -n "${ACCOUNT_URL:-}" ]]; then
|
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
|
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
|
fi
|
||||||
protected64="$(printf '%s' "${protected}" | urlbase64)"
|
protected64="$(printf '%s' "${protected}" | urlbase64)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sign header with nonce and our payload with our private key and encode signature as urlbase64
|
# 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
|
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
|
# Send header + extended header + payload + signature to the acme-server
|
||||||
data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
|
data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
|
||||||
else
|
else
|
||||||
@@ -898,7 +996,14 @@ signed_request() {
|
|||||||
data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
|
data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
http_request post "${1}" "${data}"
|
output="$(http_request post "${1}" "${data}")"
|
||||||
|
|
||||||
|
if grep -qE "^badnonce " <<< "${output}"; then
|
||||||
|
echo " ! Request failed (badNonce), retrying request..." >&2
|
||||||
|
signed_request "${1:-}" "${2:-}" "$(printf "%s" "${output}" | cut -d' ' -f2)"
|
||||||
|
else
|
||||||
|
printf "%s" "${output}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extracts all subject names from a CSR
|
# Extracts all subject names from a CSR
|
||||||
@@ -917,23 +1022,23 @@ extract_altnames() {
|
|||||||
# split to one per line:
|
# split to one per line:
|
||||||
# shellcheck disable=SC1003
|
# shellcheck disable=SC1003
|
||||||
altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
|
altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
|
||||||
# we can only get DNS: ones signed
|
# we can only get DNS/IP: ones signed
|
||||||
if grep -qEv '^(DNS|othername):' <<<"${altnames}"; then
|
if grep -qEv '^(DNS|IP( Address)*|othername):' <<<"${altnames}"; then
|
||||||
_exiterr "Certificate signing request contains non-DNS Subject Alternative Names"
|
_exiterr "Certificate signing request contains non-DNS/IP Subject Alternative Names"
|
||||||
fi
|
fi
|
||||||
# strip away the DNS: prefix
|
# strip away the DNS/IP: prefix
|
||||||
altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|othername:<unsupported>)//' )"
|
altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|IP( Address)*:|othername:<unsupported>)//' )"
|
||||||
printf "%s" "${altnames}" | tr '\n' ' '
|
printf "%s" "${altnames}" | tr '\n' ' '
|
||||||
else
|
else
|
||||||
# No SANs, extract CN
|
# 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}"
|
printf "%s" "${altnames}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get last issuer CN in certificate chain
|
# Get last issuer CN in certificate chain
|
||||||
get_last_cn() {
|
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
|
# Create certificate for domain(s) and outputs it FD 3
|
||||||
@@ -968,12 +1073,16 @@ sign_csr() {
|
|||||||
# Request new order and store authorization URIs
|
# Request new order and store authorization URIs
|
||||||
local challenge_identifiers=""
|
local challenge_identifiers=""
|
||||||
for altname in ${altnames}; do
|
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
|
done
|
||||||
challenge_identifiers="[${challenge_identifiers%, }]"
|
challenge_identifiers="[${challenge_identifiers%, }]"
|
||||||
|
|
||||||
echo " + Requesting new certificate order from CA..."
|
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)"
|
result="$(signed_request "${order_location}" "" | jsonsh)"
|
||||||
|
|
||||||
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
|
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
|
||||||
@@ -1001,6 +1110,7 @@ sign_csr() {
|
|||||||
# Receive authorization ($authorization is authz uri)
|
# Receive authorization ($authorization is authz uri)
|
||||||
response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)"
|
response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)"
|
||||||
identifier="$(echo "${response}" | get_json_string_value -p '"identifier","value"')"
|
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}"
|
echo " + Handling authorization for ${identifier}"
|
||||||
else
|
else
|
||||||
# Request new authorization ($authorization is altname)
|
# Request new authorization ($authorization is altname)
|
||||||
@@ -1010,9 +1120,13 @@ sign_csr() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if authorization has already been validated
|
# Check if authorization has already been validated
|
||||||
if [ "$(echo "${response}" | _sed 's/"challenges": \[\{.*\}\]//' | get_json_string_value status)" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
|
if [ "$(echo "${response}" | get_json_string_value status)" = "valid" ]; then
|
||||||
echo " + Found valid authorization for ${identifier}"
|
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ]; then
|
||||||
continue
|
echo " + A valid authorization has been found but will be ignored"
|
||||||
|
else
|
||||||
|
echo " + Found valid authorization for ${identifier}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Find challenge in authorization
|
# Find challenge in authorization
|
||||||
@@ -1025,7 +1139,11 @@ sign_csr() {
|
|||||||
challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
|
challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")"
|
||||||
|
|
||||||
# Gather challenge information
|
# 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)"
|
challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)"
|
||||||
|
|
||||||
if [[ ${API} -eq 2 ]]; then
|
if [[ ${API} -eq 2 ]]; then
|
||||||
@@ -1052,13 +1170,17 @@ sign_csr() {
|
|||||||
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
|
||||||
;;
|
;;
|
||||||
"tls-alpn-01")
|
"tls-alpn-01")
|
||||||
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $2}')"
|
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')"
|
||||||
generate_alpn_certificate "${identifier}" "${keyauth_hook}"
|
generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
keyauths[${idx}]="${keyauth}"
|
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))
|
idx=$((idx+1))
|
||||||
done
|
done
|
||||||
@@ -1069,11 +1191,13 @@ sign_csr() {
|
|||||||
if [[ ${num_pending_challenges} -ne 0 ]]; then
|
if [[ ${num_pending_challenges} -ne 0 ]]; then
|
||||||
echo " + Deploying challenge tokens..."
|
echo " + Deploying challenge tokens..."
|
||||||
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then
|
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'
|
"${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
|
||||||
elif [[ -n "${HOOK}" ]]; then
|
elif [[ -n "${HOOK}" ]]; then
|
||||||
# Run hook script to deploy the challenge token
|
# Run hook script to deploy the challenge token
|
||||||
local idx=0
|
local idx=0
|
||||||
while [ ${idx} -lt ${num_pending_challenges} ]; do
|
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'
|
"${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code'
|
||||||
idx=$((idx+1))
|
idx=$((idx+1))
|
||||||
done
|
done
|
||||||
@@ -1120,6 +1244,7 @@ sign_csr() {
|
|||||||
echo " + Cleaning challenge tokens..."
|
echo " + Cleaning challenge tokens..."
|
||||||
|
|
||||||
# Clean challenge tokens using chained hook
|
# 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')
|
[[ -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
|
# Clean remaining challenge tokens if validation has failed
|
||||||
@@ -1130,6 +1255,7 @@ sign_csr() {
|
|||||||
# Delete alpn verification certificates
|
# Delete alpn verification certificates
|
||||||
[[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem"
|
[[ "${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
|
# 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')
|
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code')
|
||||||
idx=$((idx+1))
|
idx=$((idx+1))
|
||||||
done
|
done
|
||||||
@@ -1177,8 +1303,8 @@ sign_csr() {
|
|||||||
if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then
|
if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then
|
||||||
foundaltchain=1
|
foundaltchain=1
|
||||||
fi
|
fi
|
||||||
if [ "${foundaltchain}" = "0" ]; then
|
if [ "${foundaltchain}" = "0" ] && (grep -Ei '^link:' "${resheaders}" | grep -q -Ei 'rel="alternate"'); then
|
||||||
while read altcrturl; do
|
while read -r altcrturl; do
|
||||||
if [ "${foundaltchain}" = "0" ]; then
|
if [ "${foundaltchain}" = "0" ]; then
|
||||||
altcrt="$(signed_request "${altcrturl}" "")"
|
altcrt="$(signed_request "${altcrturl}" "")"
|
||||||
altcn="$(get_last_cn "${altcrt}")"
|
altcn="$(get_last_cn "${altcrt}")"
|
||||||
@@ -1266,7 +1392,8 @@ walk_chain() {
|
|||||||
# Generate ALPN verification certificate
|
# Generate ALPN verification certificate
|
||||||
generate_alpn_certificate() {
|
generate_alpn_certificate() {
|
||||||
local altname="${1}"
|
local altname="${1}"
|
||||||
local acmevalidation="${2}"
|
local identifier_type="${2}"
|
||||||
|
local acmevalidation="${3}"
|
||||||
|
|
||||||
local alpncertdir="${ALPNCERTDIR}"
|
local alpncertdir="${ALPNCERTDIR}"
|
||||||
if [[ ! -e "${alpncertdir}" ]]; then
|
if [[ ! -e "${alpncertdir}" ]]; then
|
||||||
@@ -1277,10 +1404,17 @@ generate_alpn_certificate() {
|
|||||||
echo " + Generating ALPN certificate and key for ${1}..."
|
echo " + Generating ALPN certificate and key for ${1}..."
|
||||||
tmp_openssl_cnf="$(_mktemp)"
|
tmp_openssl_cnf="$(_mktemp)"
|
||||||
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
|
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
|
||||||
printf "[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}"
|
if [[ "${identifier_type}" = "ip" ]]; then
|
||||||
printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:${acmevalidation}\n" >> "${tmp_openssl_cnf}"
|
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}/"
|
SUBJ="/CN=${altname}/"
|
||||||
[[ "${OSTYPE:0:5}" = "MINGW" ]] && SUBJ="/${SUBJ}"
|
[[ "${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}"
|
_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"
|
chmod g+r "${alpncertdir}/${altname}.key.pem" "${alpncertdir}/${altname}.crt.pem"
|
||||||
rm -f "${tmp_openssl_cnf}"
|
rm -f "${tmp_openssl_cnf}"
|
||||||
@@ -1312,10 +1446,11 @@ sign_domain() {
|
|||||||
if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
|
if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
|
||||||
echo " + Generating private key..."
|
echo " + Generating private key..."
|
||||||
privkey="privkey-${timestamp}.pem"
|
privkey="privkey-${timestamp}.pem"
|
||||||
local tmp_privkey="$(_mktemp)"
|
local tmp_privkey
|
||||||
|
tmp_privkey="$(_mktemp)"
|
||||||
case "${KEY_ALGO}" in
|
case "${KEY_ALGO}" in
|
||||||
rsa) _openssl genrsa -out "${tmp_privkey}" "${KEYSIZE}";;
|
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
|
esac
|
||||||
cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem"
|
cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem"
|
||||||
rm "${tmp_privkey}"
|
rm "${tmp_privkey}"
|
||||||
@@ -1332,7 +1467,7 @@ sign_domain() {
|
|||||||
echo " + Generating private rollover key..."
|
echo " + Generating private rollover key..."
|
||||||
case "${KEY_ALGO}" in
|
case "${KEY_ALGO}" in
|
||||||
rsa) _openssl genrsa -out "${certdir}/privkey.roll.pem" "${KEYSIZE}";;
|
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
|
esac
|
||||||
fi
|
fi
|
||||||
# delete rolloverkeys if disabled
|
# delete rolloverkeys if disabled
|
||||||
@@ -1345,17 +1480,25 @@ sign_domain() {
|
|||||||
echo " + Generating signing request..."
|
echo " + Generating signing request..."
|
||||||
SAN=""
|
SAN=""
|
||||||
for altname in ${altnames}; do
|
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
|
done
|
||||||
|
if [[ "${domain}" =~ ^ip: ]]; then
|
||||||
|
SUBJ="/CN=${domain:3}/"
|
||||||
|
else
|
||||||
|
SUBJ="/CN=${domain}/"
|
||||||
|
fi
|
||||||
SAN="${SAN%%, }"
|
SAN="${SAN%%, }"
|
||||||
local tmp_openssl_cnf
|
local tmp_openssl_cnf
|
||||||
tmp_openssl_cnf="$(_mktemp)"
|
tmp_openssl_cnf="$(_mktemp)"
|
||||||
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
|
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
|
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}"
|
printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}"
|
||||||
fi
|
fi
|
||||||
SUBJ="/CN=${domain}/"
|
|
||||||
if [[ "${OSTYPE:0:5}" = "MINGW" ]]; then
|
if [[ "${OSTYPE:0:5}" = "MINGW" ]]; then
|
||||||
# The subject starts with a /, so MSYS will assume it's a path and convert
|
# The subject starts with a /, so MSYS will assume it's a path and convert
|
||||||
# it unless we escape it with another one:
|
# it unless we escape it with another one:
|
||||||
@@ -1426,20 +1569,21 @@ command_version() {
|
|||||||
revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")"
|
revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")"
|
||||||
echo "GIT-Revision: ${revision}"
|
echo "GIT-Revision: ${revision}"
|
||||||
echo ""
|
echo ""
|
||||||
if [[ "${OSTYPE}" =~ "BSD" ]]; then
|
# shellcheck disable=SC1091
|
||||||
|
if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then
|
||||||
echo "OS: $(uname -sr)"
|
echo "OS: $(uname -sr)"
|
||||||
elif [[ -e /etc/os-release ]]; then
|
elif [[ -e /etc/os-release ]]; then
|
||||||
( . /etc/os-release && echo "OS: $PRETTY_NAME" )
|
( . /etc/os-release && echo "OS: $PRETTY_NAME" )
|
||||||
elif [[ -e /usr/lib/os-release ]]; then
|
elif [[ -e /usr/lib/os-release ]]; then
|
||||||
( . /usr/lib/os-release && echo "OS: $PRETTY_NAME" )
|
( . /usr/lib/os-release && echo "OS: $PRETTY_NAME" )
|
||||||
else
|
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
|
fi
|
||||||
echo "Used software:"
|
echo "Used software:"
|
||||||
[[ -n "${BASH_VERSION:-}" ]] && echo " bash: ${BASH_VERSION}"
|
[[ -n "${BASH_VERSION:-}" ]] && echo " bash: ${BASH_VERSION}"
|
||||||
[[ -n "${ZSH_VERSION:-}" ]] && echo " zsh: ${ZSH_VERSION}"
|
[[ -n "${ZSH_VERSION:-}" ]] && echo " zsh: ${ZSH_VERSION}"
|
||||||
echo " curl: ${CURL_VERSION}"
|
echo " curl: ${CURL_VERSION}"
|
||||||
if [[ "${OSTYPE}" =~ "BSD" ]]; then
|
if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then
|
||||||
echo " awk, sed, mktemp, grep, diff: BSD base system versions"
|
echo " awk, sed, mktemp, grep, diff: BSD base system versions"
|
||||||
else
|
else
|
||||||
echo " awk: $(awk -W version 2>&1 | head -n1)"
|
echo " awk: $(awk -W version 2>&1 | head -n1)"
|
||||||
@@ -1518,6 +1662,20 @@ command_account() {
|
|||||||
exit 0
|
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)
|
# Usage: --cron (-c)
|
||||||
# Description: Sign/renew non-existent/changed/expiring certificates.
|
# Description: Sign/renew non-existent/changed/expiring certificates.
|
||||||
command_sign_domains() {
|
command_sign_domains() {
|
||||||
@@ -1535,9 +1693,9 @@ command_sign_domains() {
|
|||||||
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
|
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
|
||||||
DOMAINS_TXT="$(_mktemp)"
|
DOMAINS_TXT="$(_mktemp)"
|
||||||
if [[ -n "${PARAM_ALIAS:-}" ]]; then
|
if [[ -n "${PARAM_ALIAS:-}" ]]; then
|
||||||
printf -- "${PARAM_DOMAIN} > ${PARAM_ALIAS}" > "${DOMAINS_TXT}"
|
printf "%s > %s" "${PARAM_DOMAIN}" "${PARAM_ALIAS}" > "${DOMAINS_TXT}"
|
||||||
else
|
else
|
||||||
printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
|
printf "%s" "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
|
||||||
fi
|
fi
|
||||||
elif [[ -e "${DOMAINS_TXT}" ]]; then
|
elif [[ -e "${DOMAINS_TXT}" ]]; then
|
||||||
if [[ ! -r "${DOMAINS_TXT}" ]]; then
|
if [[ ! -r "${DOMAINS_TXT}" ]]; then
|
||||||
@@ -1550,17 +1708,17 @@ command_sign_domains() {
|
|||||||
# Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
|
# Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
|
||||||
ORIGIFS="${IFS}"
|
ORIGIFS="${IFS}"
|
||||||
IFS=$'\n'
|
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
|
reset_configvars
|
||||||
IFS="${ORIGIFS}"
|
IFS="${ORIGIFS}"
|
||||||
alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)"
|
alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)"
|
||||||
line="$(_sed -e 's/>[^ ]+[ ]*//g' <<< "${line}")"
|
line="$(_sed -e 's/>[^ ]+[ ]*//g' <<< "${line}")"
|
||||||
aliascount="$(grep -Eo '>' <<< "${alias}" | awk 'END {print NR}' || true )"
|
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)"
|
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
|
||||||
morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
|
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
|
export alias
|
||||||
|
|
||||||
if [[ -z "${morenames}" ]];then
|
if [[ -z "${morenames}" ]];then
|
||||||
@@ -1614,6 +1772,8 @@ command_sign_domains() {
|
|||||||
); do
|
); do
|
||||||
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
|
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
|
||||||
config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")"
|
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
|
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)
|
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}"
|
echo " + ${config_var} = ${config_value}"
|
||||||
@@ -1646,12 +1806,12 @@ command_sign_domains() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Check domain names of existing certificate
|
# 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..."
|
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/ $//')"
|
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/ $//' | _sed 's/^ //')"
|
givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')"
|
||||||
|
|
||||||
if [[ "${certnames}" = "${givennames}" ]]; then
|
if [[ "${certnames}" = "${givennames}" ]]; then
|
||||||
echo " unchanged."
|
echo " unchanged."
|
||||||
else
|
else
|
||||||
@@ -1692,13 +1852,14 @@ command_sign_domains() {
|
|||||||
if [[ ! "${skip}" = "yes" ]]; then
|
if [[ ! "${skip}" = "yes" ]]; then
|
||||||
update_ocsp="yes"
|
update_ocsp="yes"
|
||||||
[[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
|
[[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
|
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
|
||||||
skip_exit_hook=yes
|
skip_exit_hook=yes
|
||||||
sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} &
|
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} &
|
||||||
wait $! || exit_with_errorcode=1
|
wait $! || exit_with_errorcode=1
|
||||||
skip_exit_hook=no
|
skip_exit_hook=no
|
||||||
else
|
else
|
||||||
sign_domain "${certdir}" ${timestamp} ${domain} ${morenames}
|
sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1744,12 +1905,12 @@ command_sign_domains() {
|
|||||||
# Usage: --signcsr (-s) path/to/csr.pem
|
# Usage: --signcsr (-s) path/to/csr.pem
|
||||||
# Description: Sign a given CSR, output CRT on stdout (advanced usage)
|
# Description: Sign a given CSR, output CRT on stdout (advanced usage)
|
||||||
command_sign_csr() {
|
command_sign_csr() {
|
||||||
|
init_system
|
||||||
|
|
||||||
# redirect stdout to stderr
|
# redirect stdout to stderr
|
||||||
# leave stdout over at fd 3 to output the cert
|
# leave stdout over at fd 3 to output the cert
|
||||||
exec 3>&1 1>&2
|
exec 3>&1 1>&2
|
||||||
|
|
||||||
init_system
|
|
||||||
|
|
||||||
# load csr
|
# load csr
|
||||||
csrfile="${1}"
|
csrfile="${1}"
|
||||||
if [ ! -r "${csrfile}" ]; then
|
if [ ! -r "${csrfile}" ]; then
|
||||||
@@ -1762,6 +1923,7 @@ command_sign_csr() {
|
|||||||
|
|
||||||
# gen cert
|
# gen cert
|
||||||
certfile="$(_mktemp)"
|
certfile="$(_mktemp)"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
sign_csr "${csr}" ${altnames} 3> "${certfile}"
|
sign_csr "${csr}" ${altnames} 3> "${certfile}"
|
||||||
|
|
||||||
# print cert
|
# print cert
|
||||||
@@ -1866,7 +2028,7 @@ command_cleanup() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Allow globbing
|
# Allow globbing
|
||||||
[[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f
|
noglob_set
|
||||||
|
|
||||||
# Loop over all certificate directories
|
# Loop over all certificate directories
|
||||||
for certdir in "${CERTDIR}/"*; do
|
for certdir in "${CERTDIR}/"*; do
|
||||||
@@ -1907,7 +2069,6 @@ command_cleanup() {
|
|||||||
# Check if current file is in use, if unused move to archive directory
|
# Check if current file is in use, if unused move to archive directory
|
||||||
filename="$(basename "${file}")"
|
filename="$(basename "${file}")"
|
||||||
if [[ ! "${filename}" = "${current}" ]] && [[ -f "${certdir}/${filename}" ]]; then
|
if [[ ! "${filename}" = "${current}" ]] && [[ -f "${certdir}/${filename}" ]]; then
|
||||||
echo "${filename}"
|
|
||||||
if [[ "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then
|
if [[ "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then
|
||||||
echo "Deleting unused file: ${certname}/${filename}"
|
echo "Deleting unused file: ${certname}/${filename}"
|
||||||
rm "${certdir}/${filename}"
|
rm "${certdir}/${filename}"
|
||||||
@@ -1980,8 +2141,7 @@ main() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC2199
|
[[ -z "${*}" ]] && eval set -- "--help"
|
||||||
[[ -z "${@}" ]] && eval set -- "--help"
|
|
||||||
|
|
||||||
while (( ${#} )); do
|
while (( ${#} )); do
|
||||||
case "${1}" in
|
case "${1}" in
|
||||||
@@ -2107,6 +2267,12 @@ main() {
|
|||||||
PARAM_FORCE="yes"
|
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_Usage: --no-lock (-n)
|
||||||
# PARAM_Description: Don't use lockfile (potentially dangerous!)
|
# PARAM_Description: Don't use lockfile (potentially dangerous!)
|
||||||
--no-lock|-n)
|
--no-lock|-n)
|
||||||
@@ -2183,8 +2349,8 @@ main() {
|
|||||||
PARAM_ALPNCERTDIR="${1}"
|
PARAM_ALPNCERTDIR="${1}"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
# PARAM_Usage: --challenge (-t) http-01|dns-01
|
# PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01
|
||||||
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
|
# PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported
|
||||||
--challenge|-t)
|
--challenge|-t)
|
||||||
shift 1
|
shift 1
|
||||||
check_parameters "${1:-}"
|
check_parameters "${1:-}"
|
||||||
|
|||||||
@@ -34,6 +34,30 @@ under your `CERTDIR`.
|
|||||||
example.net www.example.net wiki.example.net > certalias
|
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
|
### Wildcards
|
||||||
|
|
||||||
Support for wildcards was added by the ACME v2 protocol.
|
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
|
**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
|
`*.service.example.com` which can be a useful way to create wildcard
|
||||||
certificates.
|
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'
|
# NOTE: It is a certificate for 'service.example.org'
|
||||||
*.service.example.org service.example.org > star_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
|
# 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
|
# '*.service.example.net' (which is a wildcard domain) and store it in the
|
||||||
# directory ${CERTDIR}/service.example.net
|
# directory ${CERTDIR}/service.example.net
|
||||||
|
|||||||
@@ -1,199 +1,199 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
deploy_challenge() {
|
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
|
# This hook is called once for every domain that needs to be
|
||||||
# validated, including any alternative names you may have listed.
|
# validated, including any alternative names you may have listed.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - DOMAIN
|
# - DOMAIN
|
||||||
# The domain name (CN or subject alternative name) being
|
# The domain name (CN or subject alternative name) being
|
||||||
# validated.
|
# validated.
|
||||||
# - TOKEN_FILENAME
|
# - TOKEN_FILENAME
|
||||||
# The name of the file containing the token to be served for HTTP
|
# The name of the file containing the token to be served for HTTP
|
||||||
# validation. Should be served by your web server as
|
# validation. Should be served by your web server as
|
||||||
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
|
# /.well-known/acme-challenge/${TOKEN_FILENAME}.
|
||||||
# - TOKEN_VALUE
|
# - TOKEN_VALUE
|
||||||
# The token value that needs to be served for validation. For DNS
|
# The token value that needs to be served for validation. For DNS
|
||||||
# validation, this is what you want to put in the _acme-challenge
|
# validation, this is what you want to put in the _acme-challenge
|
||||||
# TXT record. For HTTP validation it is the value that is expected
|
# TXT record. For HTTP validation it is the value that is expected
|
||||||
# be found in the $TOKEN_FILENAME file.
|
# be found in the $TOKEN_FILENAME file.
|
||||||
|
|
||||||
# Simple example: Use nsupdate with local named
|
# 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
|
# 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() {
|
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,
|
# This hook is called after attempting to validate each domain,
|
||||||
# whether or not validation was successful. Here you can delete
|
# whether or not validation was successful. Here you can delete
|
||||||
# files or DNS records that are no longer needed.
|
# files or DNS records that are no longer needed.
|
||||||
#
|
#
|
||||||
# The parameters are the same as for deploy_challenge.
|
# The parameters are the same as for deploy_challenge.
|
||||||
|
|
||||||
# Simple example: Use nsupdate with local named
|
# 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
|
# 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() {
|
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
|
# 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
|
# they are symlinked. This allows you to sync the files to disk to prevent
|
||||||
# creating a symlink to empty files on unexpected system crashes.
|
# creating a symlink to empty files on unexpected system crashes.
|
||||||
#
|
#
|
||||||
# This hook is not intended to be used for further processing of certificate
|
# This hook is not intended to be used for further processing of certificate
|
||||||
# files, see deploy_cert for that.
|
# files, see deploy_cert for that.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - KEYFILE
|
# - KEYFILE
|
||||||
# The path of the file containing the private key.
|
# The path of the file containing the private key.
|
||||||
# - CERTFILE
|
# - CERTFILE
|
||||||
# The path of the file containing the signed certificate.
|
# The path of the file containing the signed certificate.
|
||||||
# - FULLCHAINFILE
|
# - FULLCHAINFILE
|
||||||
# The path of the file containing the full certificate chain.
|
# The path of the file containing the full certificate chain.
|
||||||
# - CHAINFILE
|
# - CHAINFILE
|
||||||
# The path of the file containing the intermediate certificate(s).
|
# The path of the file containing the intermediate certificate(s).
|
||||||
# - REQUESTFILE
|
# - REQUESTFILE
|
||||||
# The path of the file containing the certificate signing request.
|
# The path of the file containing the certificate signing request.
|
||||||
|
|
||||||
# Simple example: sync the files before symlinking them
|
# Simple example: sync the files before symlinking them
|
||||||
# sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
|
# sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}"
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_cert() {
|
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
|
# This hook is called once for each certificate that has been
|
||||||
# produced. Here you might, for instance, copy your new certificates
|
# produced. Here you might, for instance, copy your new certificates
|
||||||
# to service-specific locations and reload the service.
|
# to service-specific locations and reload the service.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - DOMAIN
|
# - DOMAIN
|
||||||
# The primary domain name, i.e. the certificate common
|
# The primary domain name, i.e. the certificate common
|
||||||
# name (CN).
|
# name (CN).
|
||||||
# - KEYFILE
|
# - KEYFILE
|
||||||
# The path of the file containing the private key.
|
# The path of the file containing the private key.
|
||||||
# - CERTFILE
|
# - CERTFILE
|
||||||
# The path of the file containing the signed certificate.
|
# The path of the file containing the signed certificate.
|
||||||
# - FULLCHAINFILE
|
# - FULLCHAINFILE
|
||||||
# The path of the file containing the full certificate chain.
|
# The path of the file containing the full certificate chain.
|
||||||
# - CHAINFILE
|
# - CHAINFILE
|
||||||
# The path of the file containing the intermediate certificate(s).
|
# The path of the file containing the intermediate certificate(s).
|
||||||
# - TIMESTAMP
|
# - TIMESTAMP
|
||||||
# Timestamp when the specified certificate was created.
|
# Timestamp when the specified certificate was created.
|
||||||
|
|
||||||
# Simple example: Copy file to nginx config
|
# Simple example: Copy file to nginx config
|
||||||
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
# cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
||||||
# systemctl reload nginx
|
# systemctl reload nginx
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_ocsp() {
|
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
|
# 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
|
# been produced. Here you might, for instance, copy your new ocsp stapling
|
||||||
# files to service-specific locations and reload the service.
|
# files to service-specific locations and reload the service.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - DOMAIN
|
# - DOMAIN
|
||||||
# The primary domain name, i.e. the certificate common
|
# The primary domain name, i.e. the certificate common
|
||||||
# name (CN).
|
# name (CN).
|
||||||
# - OCSPFILE
|
# - OCSPFILE
|
||||||
# The path of the ocsp stapling file
|
# The path of the ocsp stapling file
|
||||||
# - TIMESTAMP
|
# - TIMESTAMP
|
||||||
# Timestamp when the specified ocsp stapling file was created.
|
# Timestamp when the specified ocsp stapling file was created.
|
||||||
|
|
||||||
# Simple example: Copy file to nginx config
|
# Simple example: Copy file to nginx config
|
||||||
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
# cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl
|
||||||
# systemctl reload nginx
|
# systemctl reload nginx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
unchanged_cert() {
|
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
|
# This hook is called once for each certificate that is still
|
||||||
# valid and therefore wasn't reissued.
|
# valid and therefore wasn't reissued.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - DOMAIN
|
# - DOMAIN
|
||||||
# The primary domain name, i.e. the certificate common
|
# The primary domain name, i.e. the certificate common
|
||||||
# name (CN).
|
# name (CN).
|
||||||
# - KEYFILE
|
# - KEYFILE
|
||||||
# The path of the file containing the private key.
|
# The path of the file containing the private key.
|
||||||
# - CERTFILE
|
# - CERTFILE
|
||||||
# The path of the file containing the signed certificate.
|
# The path of the file containing the signed certificate.
|
||||||
# - FULLCHAINFILE
|
# - FULLCHAINFILE
|
||||||
# The path of the file containing the full certificate chain.
|
# The path of the file containing the full certificate chain.
|
||||||
# - CHAINFILE
|
# - CHAINFILE
|
||||||
# The path of the file containing the intermediate certificate(s).
|
# The path of the file containing the intermediate certificate(s).
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid_challenge() {
|
invalid_challenge() {
|
||||||
local DOMAIN="${1}" RESPONSE="${2}"
|
local DOMAIN="${1}" RESPONSE="${2}"
|
||||||
|
|
||||||
# This hook is called if the challenge response has failed, so domain
|
# This hook is called if the challenge response has failed, so domain
|
||||||
# owners can be aware and act accordingly.
|
# owners can be aware and act accordingly.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - DOMAIN
|
# - DOMAIN
|
||||||
# The primary domain name, i.e. the certificate common
|
# The primary domain name, i.e. the certificate common
|
||||||
# name (CN).
|
# name (CN).
|
||||||
# - RESPONSE
|
# - RESPONSE
|
||||||
# The response that the verification server returned
|
# The response that the verification server returned
|
||||||
|
|
||||||
# Simple example: Send mail to root
|
# Simple example: Send mail to root
|
||||||
# printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root
|
# printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root
|
||||||
}
|
}
|
||||||
|
|
||||||
request_failure() {
|
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
|
# 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
|
# 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
|
# response code that does not start with '2'. Useful to alert admins
|
||||||
# about problems with requests.
|
# about problems with requests.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - STATUSCODE
|
# - STATUSCODE
|
||||||
# The HTML status code that originated the error.
|
# The HTML status code that originated the error.
|
||||||
# - REASON
|
# - REASON
|
||||||
# The specified reason for the error.
|
# The specified reason for the error.
|
||||||
# - REQTYPE
|
# - REQTYPE
|
||||||
# The kind of request that was made (GET, POST...)
|
# The kind of request that was made (GET, POST...)
|
||||||
# - HEADERS
|
# - HEADERS
|
||||||
# HTTP headers returned by the CA
|
# HTTP headers returned by the CA
|
||||||
|
|
||||||
# Simple example: Send mail to root
|
# Simple example: Send mail to root
|
||||||
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
|
# printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_csr() {
|
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.
|
# 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
|
# It can be used to generate or fetch a certificate signing request with external
|
||||||
# tools.
|
# tools.
|
||||||
# The output should be just the certificate signing request formatted as PEM.
|
# The output should be just the certificate signing request formatted as PEM.
|
||||||
#
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
# - DOMAIN
|
# - DOMAIN
|
||||||
# The primary domain as specified in domains.txt. This does not need to
|
# 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.
|
# match with the domains in the CSR, it's basically just the directory name.
|
||||||
# - CERTDIR
|
# - CERTDIR
|
||||||
# Certificate output directory for this particular certificate. Can be used
|
# Certificate output directory for this particular certificate. Can be used
|
||||||
# for storing additional files.
|
# for storing additional files.
|
||||||
# - ALTNAMES
|
# - ALTNAMES
|
||||||
# All domain names for the current certificate as specified in domains.txt.
|
# 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.
|
# Again, this doesn't need to match with the CSR, it's just there for convenience.
|
||||||
|
|
||||||
# Simple example: Look for pre-generated CSRs
|
# Simple example: Look for pre-generated CSRs
|
||||||
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
|
# if [ -e "${CERTDIR}/pre-generated.csr" ]; then
|
||||||
# cat "${CERTDIR}/pre-generated.csr"
|
# cat "${CERTDIR}/pre-generated.csr"
|
||||||
# fi
|
# fi
|
||||||
}
|
}
|
||||||
|
|
||||||
startup_hook() {
|
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
|
- KEY_ALGO
|
||||||
- KEYSIZE
|
- KEYSIZE
|
||||||
- OCSP_MUST_STAPLE
|
- OCSP_MUST_STAPLE
|
||||||
|
- OCSP_FETCH
|
||||||
|
- OCSP_DAYS
|
||||||
- CHALLENGETYPE
|
- CHALLENGETYPE
|
||||||
- HOOK
|
- HOOK
|
||||||
- HOOK_CHAIN
|
- 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:
|
To avoid this, please set the CA property to the Let’s Encrypt staging server URL in your config file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CA="https://acme-staging.api.letsencrypt.org/directory"
|
CA="https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
```
|
```
|
||||||
|
|
||||||
# ACMEv2 staging
|
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).
|
||||||
|
|
||||||
You can use `CA="https://acme-staging-v02.api.letsencrypt.org/directory"` to test dehydrated with
|
|
||||||
the ACMEv2 staging endpoint.
|
|
||||||
|
|||||||
@@ -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.
|
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
|
### Example nginx config
|
||||||
|
|
||||||
On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls
|
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