|
|
|
|
@@ -291,6 +291,8 @@ store_configvars() {
|
|
|
|
|
__OPENSSL_CNF="${OPENSSL_CNF}"
|
|
|
|
|
__RENEW_DAYS="${RENEW_DAYS}"
|
|
|
|
|
__IP_VERSION="${IP_VERSION}"
|
|
|
|
|
__ACME_PROFILE="${ACME_PROFILE}"
|
|
|
|
|
__ORDER_TIMEOUT=${ORDER_TIMEOUT}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reset_configvars() {
|
|
|
|
|
@@ -309,6 +311,8 @@ reset_configvars() {
|
|
|
|
|
OPENSSL_CNF="${__OPENSSL_CNF}"
|
|
|
|
|
RENEW_DAYS="${__RENEW_DAYS}"
|
|
|
|
|
IP_VERSION="${__IP_VERSION}"
|
|
|
|
|
ACME_PROFILE="${__ACME_PROFILE}"
|
|
|
|
|
ORDER_TIMEOUT=${__ORDER_TIMEOUT}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hookscript_bricker_hook() {
|
|
|
|
|
@@ -334,6 +338,7 @@ verify_config() {
|
|
|
|
|
fi
|
|
|
|
|
[[ "${API}" == "auto" || "${API}" == "1" || "${API}" == "2" ]] || _exiterr "Unsupported API version defined in config: ${API}"
|
|
|
|
|
[[ "${OCSP_DAYS}" =~ ^[0-9]+$ ]] || _exiterr "OCSP_DAYS must be a number"
|
|
|
|
|
[[ "${ORDER_TIMEOUT}" =~ ^[0-9]+$ ]] || _exiterr "ORDER_TIMEOUT must be a number"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Setup default config values, search for and load configuration files
|
|
|
|
|
@@ -355,6 +360,8 @@ load_config() {
|
|
|
|
|
CA_LETSENCRYPT_TEST="https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
|
|
|
CA_BUYPASS="https://api.buypass.com/acme/directory"
|
|
|
|
|
CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory"
|
|
|
|
|
CA_GOOGLE="https://dv.acme-v02.api.pki.goog/directory"
|
|
|
|
|
CA_GOOGLE_TEST="https://dv.acme-v02.test-api.pki.goog/directory"
|
|
|
|
|
|
|
|
|
|
# Default values
|
|
|
|
|
CA="letsencrypt"
|
|
|
|
|
@@ -372,7 +379,7 @@ load_config() {
|
|
|
|
|
HOOK=
|
|
|
|
|
PREFERRED_CHAIN=
|
|
|
|
|
HOOK_CHAIN="no"
|
|
|
|
|
RENEW_DAYS="30"
|
|
|
|
|
RENEW_DAYS="32"
|
|
|
|
|
KEYSIZE="4096"
|
|
|
|
|
WELLKNOWN=
|
|
|
|
|
PRIVATE_KEY_RENEW="yes"
|
|
|
|
|
@@ -388,9 +395,12 @@ load_config() {
|
|
|
|
|
IP_VERSION=
|
|
|
|
|
CHAINCACHE=
|
|
|
|
|
AUTO_CLEANUP="no"
|
|
|
|
|
AUTO_CLEANUP_DELETE="no"
|
|
|
|
|
DEHYDRATED_USER=
|
|
|
|
|
DEHYDRATED_GROUP=
|
|
|
|
|
API="auto"
|
|
|
|
|
ACME_PROFILE=""
|
|
|
|
|
ORDER_TIMEOUT=0
|
|
|
|
|
|
|
|
|
|
if [[ -z "${CONFIG:-}" ]]; then
|
|
|
|
|
echo "#" >&2
|
|
|
|
|
@@ -481,6 +491,10 @@ load_config() {
|
|
|
|
|
CA="${CA_BUYPASS}"
|
|
|
|
|
elif [ "${CA}" = "buypass-test" ]; then
|
|
|
|
|
CA="${CA_BUYPASS_TEST}"
|
|
|
|
|
elif [ "${CA}" = "google" ]; then
|
|
|
|
|
CA="${CA_GOOGLE}"
|
|
|
|
|
elif [ "${CA}" = "google-test" ]; then
|
|
|
|
|
CA="${CA_GOOGLE_TEST}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then
|
|
|
|
|
@@ -544,6 +558,8 @@ load_config() {
|
|
|
|
|
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
|
|
|
|
|
[[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
|
|
|
|
|
[[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
|
|
|
|
|
[[ -n "${PARAM_ACME_PROFILE:-}" ]] && ACME_PROFILE="${PARAM_ACME_PROFILE}"
|
|
|
|
|
[[ -n "${PARAM_ORDER_TIMEOUT:-}" ]] && ORDER_TIMEOUT="${PARAM_ORDER_TIMEOUT}"
|
|
|
|
|
|
|
|
|
|
if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ] && [ "${PARAM_FORCE:-no}" = "no" ]; then
|
|
|
|
|
_exiterr "Argument --force-validation can only be used in combination with --force (-x)"
|
|
|
|
|
@@ -587,6 +603,10 @@ init_system() {
|
|
|
|
|
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
|
|
|
|
|
# Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part
|
|
|
|
|
CA_REG=${CA_NEW_REG/new-reg/reg}
|
|
|
|
|
|
|
|
|
|
if [[ -n "${ACME_PROFILE}" ]]; then
|
|
|
|
|
_exiterr "ACME profiles are not supported in ACME v1."
|
|
|
|
|
fi
|
|
|
|
|
else
|
|
|
|
|
CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" &&
|
|
|
|
|
CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" &&
|
|
|
|
|
@@ -595,6 +615,35 @@ init_system() {
|
|
|
|
|
CA_REQUIRES_EAB="$(printf "%s" "${CA_DIRECTORY}" | get_json_bool_value -p '"meta","externalAccountRequired"' || echo false)" &&
|
|
|
|
|
CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revokeCert)" ||
|
|
|
|
|
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
|
|
|
|
|
|
|
|
|
|
# Checking ACME profile
|
|
|
|
|
if [[ -n "${ACME_PROFILE}" ]]; then
|
|
|
|
|
# Extract available profiles from CA directory
|
|
|
|
|
declare -A available_profiles=()
|
|
|
|
|
while IFS=$'\t' read -r path value; do
|
|
|
|
|
if [[ "${value}" =~ ^\"([^\"]+)\"$ ]]; then
|
|
|
|
|
value=${BASH_REMATCH[1]}
|
|
|
|
|
fi
|
|
|
|
|
if [[ "${path}" =~ ^\[\"([^\"]+)\"\]$ ]]; then
|
|
|
|
|
available_profiles[${BASH_REMATCH[1]}]=$value
|
|
|
|
|
fi
|
|
|
|
|
done <<< "$(printf "%s" "${CA_DIRECTORY}" | get_json_dict_value -p '"meta","profiles"' 2>/dev/null)"
|
|
|
|
|
if [[ ${#available_profiles[@]} -eq 0 ]]; then
|
|
|
|
|
_exiterr "ACME profile not supported by this CA"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check if the requested profile is available
|
|
|
|
|
found_profile="no"
|
|
|
|
|
for profile in "${!available_profiles[@]}"; do
|
|
|
|
|
if [[ "${profile}" == "${ACME_PROFILE}" ]]; then
|
|
|
|
|
found_profile="yes"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
if [[ "${found_profile}" == "no" ]]; then
|
|
|
|
|
_exiterr "ACME profile '${ACME_PROFILE}' not found, available profiles:$(for key in "${!available_profiles[@]}"; do printf "\n %s: %s" "${key}" "${available_profiles[$key]}"; done)"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Export some environment variables to be used in hook script
|
|
|
|
|
@@ -703,6 +752,14 @@ init_system() {
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Google special sauce
|
|
|
|
|
if [[ "${CA}" = "${CA_GOOGLE}" ]]; then
|
|
|
|
|
if [[ -z "${CONTACT_EMAIL}" ]] || [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then
|
|
|
|
|
echo "Google requires contact email, EAB_KID and EAB_HMAC_KEY to be manually configured (see https://cloud.google.com/certificate-manager/docs/public-ca-tutorial)"
|
|
|
|
|
FAILED=true
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Check if external account is required
|
|
|
|
|
if [[ "${FAILED}" = "false" ]]; then
|
|
|
|
|
if [[ "${CA_REQUIRES_EAB}" = "true" ]]; then
|
|
|
|
|
@@ -1009,13 +1066,13 @@ signed_request() {
|
|
|
|
|
# Extracts all subject names from a CSR
|
|
|
|
|
# Outputs either the CN, or the SANs, one per line
|
|
|
|
|
extract_altnames() {
|
|
|
|
|
csr="${1}" # the CSR itself (not a file)
|
|
|
|
|
csrfile="${1}" # path to CSR file
|
|
|
|
|
|
|
|
|
|
if ! <<<"${csr}" "${OPENSSL}" req -verify -noout >/dev/null 2>&1; then
|
|
|
|
|
if ! "${OPENSSL}" req -in "${csrfile}" -verify -noout >/dev/null; then
|
|
|
|
|
_exiterr "Certificate signing request isn't valid"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
reqtext="$( <<<"${csr}" "${OPENSSL}" req -noout -text )"
|
|
|
|
|
reqtext="$("${OPENSSL}" req -in "${csrfile}" -noout -text)"
|
|
|
|
|
if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then
|
|
|
|
|
# SANs used, extract these
|
|
|
|
|
altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )"
|
|
|
|
|
@@ -1043,7 +1100,7 @@ get_last_cn() {
|
|
|
|
|
|
|
|
|
|
# Create certificate for domain(s) and outputs it FD 3
|
|
|
|
|
sign_csr() {
|
|
|
|
|
csr="${1}" # the CSR itself (not a file)
|
|
|
|
|
csrfile="${1}" # path to CSR file
|
|
|
|
|
|
|
|
|
|
if { true >&3; } 2>/dev/null; then
|
|
|
|
|
: # fd 3 looks OK
|
|
|
|
|
@@ -1082,7 +1139,12 @@ sign_csr() {
|
|
|
|
|
challenge_identifiers="[${challenge_identifiers%, }]"
|
|
|
|
|
|
|
|
|
|
echo " + Requesting new certificate order from CA..."
|
|
|
|
|
order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
|
|
|
|
|
local order_payload='{"identifiers": '"${challenge_identifiers}"
|
|
|
|
|
if [[ -n "${ACME_PROFILE}" ]]; then
|
|
|
|
|
order_payload="${order_payload}"',"profile":"'"${ACME_PROFILE}"'"'
|
|
|
|
|
fi
|
|
|
|
|
order_payload="${order_payload}"'}'
|
|
|
|
|
order_location="$(signed_request "${CA_NEW_ORDER}" "${order_payload}" 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')"
|
|
|
|
|
result="$(signed_request "${order_location}" "" | jsonsh)"
|
|
|
|
|
|
|
|
|
|
order_authorizations="$(echo "${result}" | get_json_array_values authorizations)"
|
|
|
|
|
@@ -1268,25 +1330,30 @@ sign_csr() {
|
|
|
|
|
|
|
|
|
|
# Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem
|
|
|
|
|
echo " + Requesting certificate..."
|
|
|
|
|
csr64="$( <<<"${csr}" "${OPENSSL}" req -config "${OPENSSL_CNF}" -outform DER | urlbase64)"
|
|
|
|
|
csr64="$("${OPENSSL}" req -in "${csrfile}" -config "${OPENSSL_CNF}" -outform DER | urlbase64)"
|
|
|
|
|
if [[ ${API} -eq 1 ]]; then
|
|
|
|
|
crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)"
|
|
|
|
|
crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
|
|
|
|
|
else
|
|
|
|
|
result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | jsonsh)"
|
|
|
|
|
waited=0
|
|
|
|
|
while :; do
|
|
|
|
|
orderstatus="$(echo "${result}" | get_json_string_value status)"
|
|
|
|
|
case "${orderstatus}"
|
|
|
|
|
in
|
|
|
|
|
"processing" | "pending")
|
|
|
|
|
if [ ${ORDER_TIMEOUT} -gt 0 ] && [ ${waited} -gt ${ORDER_TIMEOUT} ]; then
|
|
|
|
|
_exiterr "Timed out waiting for processing of order (still ${orderstatus})"
|
|
|
|
|
fi
|
|
|
|
|
echo " + Order is ${orderstatus}..."
|
|
|
|
|
sleep 2;
|
|
|
|
|
waited=$((waited+2))
|
|
|
|
|
;;
|
|
|
|
|
"valid")
|
|
|
|
|
break;
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
_exiterr "Order in status ${orderstatus}"
|
|
|
|
|
_exiterr "Order has invalid/unknown status: ${orderstatus}"
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
result="$(signed_request "${order_location}" "" | jsonsh)"
|
|
|
|
|
@@ -1510,7 +1577,7 @@ sign_domain() {
|
|
|
|
|
|
|
|
|
|
crt_path="${certdir}/cert-${timestamp}.pem"
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
sign_csr "$(< "${certdir}/cert-${timestamp}.csr")" ${altnames} 3>"${crt_path}"
|
|
|
|
|
sign_csr "${certdir}/cert-${timestamp}.csr" ${altnames} 3>"${crt_path}"
|
|
|
|
|
|
|
|
|
|
# Create fullchain.pem
|
|
|
|
|
echo " + Creating fullchain.pem..."
|
|
|
|
|
@@ -1775,7 +1842,7 @@ command_sign_domains() {
|
|
|
|
|
# All settings that are allowed here should also be stored and
|
|
|
|
|
# restored in store_configvars() and reset_configvars()
|
|
|
|
|
case "${config_var}" in
|
|
|
|
|
KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
|
|
|
|
|
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|ACME_PROFILE|ORDER_TIMEOUT)
|
|
|
|
|
echo " + ${config_var} = ${config_value}"
|
|
|
|
|
declare -- "${config_var}=${config_value}"
|
|
|
|
|
;;
|
|
|
|
|
@@ -1792,16 +1859,18 @@ command_sign_domains() {
|
|
|
|
|
skip="no"
|
|
|
|
|
|
|
|
|
|
# Allow for external CSR generation
|
|
|
|
|
local csr=""
|
|
|
|
|
local csrfile=""
|
|
|
|
|
if [[ -n "${HOOK}" ]]; then
|
|
|
|
|
csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")" || _exiterr 'generate_csr hook returned with non-zero exit code'
|
|
|
|
|
if grep -qE "\-----BEGIN (NEW )?CERTIFICATE REQUEST-----" <<< "${csr}"; then
|
|
|
|
|
altnames="$(extract_altnames "${csr}")"
|
|
|
|
|
csrfile="$(_mktemp)"
|
|
|
|
|
cat > "${csrfile}" <<< "${csr}"
|
|
|
|
|
altnames="$(extract_altnames "${csrfile}")"
|
|
|
|
|
domain="$(cut -d' ' -f1 <<< "${altnames}")"
|
|
|
|
|
morenames="$(cut -s -d' ' -f2- <<< "${altnames}")"
|
|
|
|
|
echo " + Using CSR from hook script (real names: ${altnames})"
|
|
|
|
|
else
|
|
|
|
|
csr=""
|
|
|
|
|
csrfile=""
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
@@ -1851,7 +1920,10 @@ command_sign_domains() {
|
|
|
|
|
# Sign certificate for this domain
|
|
|
|
|
if [[ ! "${skip}" = "yes" ]]; then
|
|
|
|
|
update_ocsp="yes"
|
|
|
|
|
[[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr"
|
|
|
|
|
if [[ -n "${csrfile}" ]]; then
|
|
|
|
|
cat "${csrfile}" > "${certdir}/cert-${timestamp}.csr"
|
|
|
|
|
rm "${csrfile}"
|
|
|
|
|
fi
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
|
|
|
|
|
skip_exit_hook=yes
|
|
|
|
|
@@ -1895,8 +1967,8 @@ command_sign_domains() {
|
|
|
|
|
|
|
|
|
|
[[ -n "${HOOK}" ]] && ("${HOOK}" "exit_hook" || echo 'exit_hook returned with non-zero exit code!' >&2)
|
|
|
|
|
if [[ "${AUTO_CLEANUP}" == "yes" ]]; then
|
|
|
|
|
echo "+ Running automatic cleanup"
|
|
|
|
|
command_cleanup noinit
|
|
|
|
|
echo " + Running automatic cleanup"
|
|
|
|
|
PARAM_CLEANUPDELETE="${AUTO_CLEANUP_DELETE:-no}" command_cleanup noinit | _sed 's/^/ + /g'
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
exit "${exit_with_errorcode}"
|
|
|
|
|
@@ -1912,19 +1984,18 @@ command_sign_csr() {
|
|
|
|
|
exec 3>&1 1>&2
|
|
|
|
|
|
|
|
|
|
# load csr
|
|
|
|
|
csrfile="${1}"
|
|
|
|
|
local csrfile="${1}"
|
|
|
|
|
if [ ! -r "${csrfile}" ]; then
|
|
|
|
|
_exiterr "Could not read certificate signing request ${csrfile}"
|
|
|
|
|
fi
|
|
|
|
|
csr="$(cat "${csrfile}")"
|
|
|
|
|
|
|
|
|
|
# extract names
|
|
|
|
|
altnames="$(extract_altnames "${csr}")"
|
|
|
|
|
altnames="$(extract_altnames "${csrfile}")"
|
|
|
|
|
|
|
|
|
|
# gen cert
|
|
|
|
|
certfile="$(_mktemp)"
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
sign_csr "${csr}" ${altnames} 3> "${certfile}"
|
|
|
|
|
sign_csr "${csrfile}" ${altnames} 3> "${certfile}"
|
|
|
|
|
|
|
|
|
|
# print cert
|
|
|
|
|
echo "# CERT #" >&3
|
|
|
|
|
@@ -2364,6 +2435,23 @@ main() {
|
|
|
|
|
check_parameters "${1:-}"
|
|
|
|
|
PARAM_KEY_ALGO="${1}"
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
|
|
# PARAM_Usage: --acme-profile profile_name
|
|
|
|
|
# PARAM_Description: Use specified ACME profile
|
|
|
|
|
--acme-profile)
|
|
|
|
|
shift 1
|
|
|
|
|
check_parameters "${1:-}"
|
|
|
|
|
PARAM_ACME_PROFILE="${1}"
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
|
|
# PARAM_Usage: --order-timeout seconds
|
|
|
|
|
# PARAM_Description: Amount of seconds to wait for processing of order until erroring out
|
|
|
|
|
--order-timeout)
|
|
|
|
|
shift 1
|
|
|
|
|
check_parameters "${1:-}"
|
|
|
|
|
PARAM_ORDER_TIMEOUT=${1}
|
|
|
|
|
;;
|
|
|
|
|
|
|
|
|
|
*)
|
|
|
|
|
echo "Unknown parameter detected: ${1}" >&2
|
|
|
|
|
echo >&2
|
|
|
|
|
|