Skip to content

Commit

Permalink
feat: use EAB if available no matter the ACME CI (#981)
Browse files Browse the repository at this point in the history
* ci: setup Pebble with docker-compose + .env file

* refactor: move acme.sh hooks further down the file

* feat: user EAB with other CAs than Zero SSL

* tests: ACME External Account Binding (EAB)

* ci: add local Pebble EAB testing
  • Loading branch information
buchdag committed Dec 6, 2022
1 parent 68005e6 commit 87c27d2
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 107 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/test.yml
Expand Up @@ -97,7 +97,16 @@ jobs:
]
setup: [2containers, 3containers]
acme-ca: [pebble]
pebble-config: [pebble-config.json]
include:
- test-name: acme_eab
setup: 2containers
acme-ca: pebble
pebble-config: pebble-config-eab.json
- test-name: acme_eab
setup: 3containers
acme-ca: pebble
pebble-config: pebble-config-eab.json
- test-name: ocsp_must_staple
setup: 2containers
acme-ca: boulder
Expand All @@ -122,7 +131,9 @@ jobs:
- name: Setup Pebble
if: ${{ matrix.acme-ca == 'pebble' }}
run: test/setup/setup-pebble.sh
env:
PEBBLE_CONFIG: ${{ matrix.pebble-config }}
run: test/setup/pebble/setup-pebble.sh

- name: Setup Boulder
if: ${{ matrix.acme-ca == 'boulder' }}
Expand All @@ -148,6 +159,7 @@ jobs:
env:
SETUP: ${{ matrix.setup }}
ACME_CA: ${{ matrix.acme-ca }}
PEBBLE_CONFIG: ${{ matrix.pebble-config }}
run: test/run.sh -t ${{ matrix.test-name }} "$IMAGE"

- name: Display containers logs
Expand Down
9 changes: 9 additions & 0 deletions app/functions.sh
Expand Up @@ -24,6 +24,15 @@ function parse_true() {
esac
}

function in_array() {
local needle="$1" item
local -n arrref="$2"
for item in "${arrref[@]}"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}

[[ -z "${VHOST_DIR:-}" ]] && \
declare -r VHOST_DIR=/etc/nginx/vhost.d
[[ -z "${START_HEADER:-}" ]] && \
Expand Down
92 changes: 57 additions & 35 deletions app/letsencrypt_service
Expand Up @@ -220,37 +220,50 @@ function update_cert {
--fullchain-file "${certificate_dir}/fullchain.pem" \
)

# acme.sh pre and post hooks
local -n acme_pre_hook="ACME_${cid}_PRE_HOOK"
acme_pre_hook=${acme_pre_hook:-$ACME_PRE_HOOK}
if [[ -n "${acme_pre_hook// }" ]]; then
params_issue_arr+=(--pre-hook "$acme_pre_hook")
fi
local -n acme_post_hook="ACME_${cid}_POST_HOOK"
acme_post_hook=${acme_post_hook:-$ACME_POST_HOOK}
if [[ -n "${acme_post_hook// }" ]]; then
params_issue_arr+=(--post-hook "$acme_post_hook")
fi

[[ ! -d "$config_home" ]] && mkdir -p "$config_home"
params_base_arr+=(--config-home "$config_home")
local account_file="${config_home}/ca/${ca_dir}/account.json"

# Zero SSL External Account Binding (EAB)
# External Account Binding (EAB)
local -n eab_kid="ACME_${cid}_EAB_KID"
local -n eab_hmac_key="ACME_${cid}_EAB_HMAC_KEY"
if [[ ! -f "$account_file" ]]; then
if [[ -n "${eab_kid}" && -n "${eab_hmac_key}" ]]; then
# Register the ACME account with the per container EAB credentials.
params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
elif [[ -n "${ACME_EAB_KID// }" && -n "${ACME_EAB_HMAC_KEY// }" ]]; then
# We don't have per-container EAB kid and hmac key or Zero SSL API key.
# Register the ACME account with the default EAB credentials.
params_register_arr+=(--eab-kid "$ACME_EAB_KID" --eab-hmac-key "$ACME_EAB_HMAC_KEY")
elif [[ -n "${accountemail// }" ]]; then
# We don't have per container nor default EAB credentials, register a new account.
params_register_arr+=(--accountemail "$accountemail")
fi
fi

# Zero SSL
if [[ "$acme_ca_uri" == "https://acme.zerossl.com/v2/DV90" ]]; then
local -n eab_kid="ACME_${cid}_EAB_KID"
local -n eab_hmac_key="ACME_${cid}_EAB_HMAC_KEY"
local -n zerossl_api_key="ZEROSSL_${cid}_API_KEY"
if [[ -z "$zerossl_api_key" ]]; then
# Try using the default API key
zerossl_api_key="$ZEROSSL_API_KEY"
# Test if we already have:
# - an account file
# - the --accountemail account registration parameter
# - the --eab-kid and --eab-hmac-key account registration parameters
local account_ok='false'
if [[ -f "$account_file" ]]; then
account_ok='true'
elif in_array '--accountemail' 'params_register_arr'; then
account_ok='true'
elif in_array '--eab-kid' 'params_register_arr' && in_array '--eab-hmac-key' 'params_register_arr'; then
account_ok='true'
fi
if [[ ! -f "$account_file" ]]; then
if [[ -n "${eab_kid}" && -n "${eab_hmac_key}" ]]; then
# Register the ACME account with the per container EAB credentials.
params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
elif [[ -n "${zerossl_api_key}" ]]; then
# We have a Zero SSL API key but no per-container EAB kid and hmac key.

if [[ $account_ok == 'false' ]]; then
local -n zerossl_api_key="ZEROSSL_${cid}_API_KEY"
if [[ -z "$zerossl_api_key" ]]; then
# Try using the default API key
zerossl_api_key="${ZEROSSL_API_KEY:-}"
fi

if [[ -n "${zerossl_api_key// }" ]]; then
# Generate a set of ACME EAB credentials using the ZeroSSL API.
local zerossl_api_response
if zerossl_api_response="$(curl -s -X POST "https://api.zerossl.com/acme/eab-credentials?access_key=${zerossl_api_key}")"; then
Expand All @@ -267,23 +280,13 @@ function update_cert {
# curl failed.
echo "Warning: curl failed to make an HTTP POST request to https://api.zerossl.com/acme/eab-credentials."
fi
elif [[ -n "${ACME_EAB_KID// }" && -n "${ACME_EAB_HMAC_KEY// }" ]]; then
# We don't have per-container EAB kid and hmac key or Zero SSL API key.
# Register the ACME account with the default EAB credentials.
params_register_arr+=(--eab-kid "$ACME_EAB_KID" --eab-hmac-key "$ACME_EAB_HMAC_KEY")
elif [[ -n "${accountemail// }" ]]; then
# We don't have per container nor default EAB credentials, register a new account with ZeroSSL.
params_register_arr+=(--accountemail "$accountemail")
else
# We don't have a Zero SSL ACME account, EAB credentials, a ZeroSSL API key or an account email :
# skip certificate account registration and certificate issuance.
echo "Error: usage of ZeroSSL require an email bound account. No EAB credentials, ZeroSSL API key or email were provided for this certificate, creation aborted."
return 1
fi
fi
elif [[ -n "${accountemail// }" ]]; then
# We're not using Zero SSL, register the ACME account using the provided email.
params_register_arr+=(--accountemail "$accountemail")
fi

# Account registration and update if required
Expand All @@ -304,6 +307,25 @@ function update_cert {
return 1
fi

# acme.sh pre and post hooks
local -n acme_pre_hook="ACME_${cid}_PRE_HOOK"
if [[ -n "${acme_pre_hook}" ]]; then
# Use per-container pre hook
params_issue_arr+=(--pre-hook "$acme_pre_hook")
elif [[ -n ${ACME_PRE_HOOK// } ]]; then
# Use default pre hook
params_issue_arr+=(--pre-hook "$ACME_PRE_HOOK")
fi

local -n acme_post_hook="ACME_${cid}_POST_HOOK"
if [[ -n "${acme_post_hook}" ]]; then
# Use per-container post hook
params_issue_arr+=(--post-hook "$acme_post_hook")
elif [[ -n ${ACME_POST_HOOK// } ]]; then
# Use default post hook
params_issue_arr+=(--post-hook "$ACME_POST_HOOK")
fi

local -n acme_preferred_chain="ACME_${cid}_PREFERRED_CHAIN"
if [[ -n "${acme_preferred_chain}" ]]; then
# Using amce.sh --preferred-chain to select alternate chain.
Expand Down
7 changes: 7 additions & 0 deletions test/config.sh
Expand Up @@ -19,6 +19,13 @@ globalTests+=(
acme_hooks
)

# The acme_eab test requires Pebble with a specific configuration
if [[ "$ACME_CA" == 'pebble' && "$PEBBLE_CONFIG" == 'pebble-config-eab.json' ]]; then
globalTests+=(
acme_eab
)
fi

# The ocsp_must_staple test does not work with Pebble
if [[ "$ACME_CA" == 'boulder' ]]; then
globalTests+=(
Expand Down
12 changes: 0 additions & 12 deletions test/setup/pebble-config.json

This file was deleted.

2 changes: 2 additions & 0 deletions test/setup/pebble/.env
@@ -0,0 +1,2 @@
PEBBLE_VERSION='v2.3.1'
PEBBLE_CONFIG='pebble-config.json'
36 changes: 36 additions & 0 deletions test/setup/pebble/docker-compose.yml
@@ -0,0 +1,36 @@
version: '3'

services:
pebble:
image: "letsencrypt/pebble:${PEBBLE_VERSION}"
container_name: pebble
volumes:
- "./${PEBBLE_CONFIG}:/test/config/pebble-config.json"
environment:
- PEBBLE_VA_NOSLEEP=1
command: pebble -config /test/config/pebble-config.json -dnsserver 10.30.50.3:8053
ports:
- 14000:14000 # HTTPS ACME API
- 15000:15000 # HTTPS Management API
networks:
acme_net:
ipv4_address: 10.30.50.2

challtestsrv:
image: "letsencrypt/pebble-challtestsrv:${PEBBLE_VERSION}"
container_name: challtestserv
command: pebble-challtestsrv -tlsalpn01 ""
ports:
- 8055:8055 # HTTP Management API
networks:
acme_net:
ipv4_address: 10.30.50.3

networks:
acme_net:
name: acme_net
driver: bridge
ipam:
driver: default
config:
- subnet: 10.30.50.0/24
16 changes: 16 additions & 0 deletions test/setup/pebble/pebble-config-eab.json
@@ -0,0 +1,16 @@
{
"pebble": {
"listenAddress": "0.0.0.0:14000",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "test/certs/localhost/cert.pem",
"privateKey": "test/certs/localhost/key.pem",
"httpPort": 80,
"tlsPort": 443,
"ocspResponderURL": "",
"externalAccountBindingRequired": true,
"externalAccountMACKeys": {
"kid-1": "zWNDZM6eQGHWpSRTPal5eIUYFTu7EajVIoguysqZ9wG44nMEtx3MUAsUDkMTQ12W",
"kid-2": "b10lLJs8l1GPIzsLP0s6pMt8O0XVGnfTaCeROxQM0BIt2XrJMDHJZBM5NuQmQJQH"
}
}
}
12 changes: 12 additions & 0 deletions test/setup/pebble/pebble-config.json
@@ -0,0 +1,12 @@
{
"pebble": {
"listenAddress": "0.0.0.0:14000",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "test/certs/localhost/cert.pem",
"privateKey": "test/certs/localhost/key.pem",
"httpPort": 80,
"tlsPort": 443,
"ocspResponderURL": "",
"externalAccountBindingRequired": false
}
}
33 changes: 33 additions & 0 deletions test/setup/pebble/setup-pebble.sh
@@ -0,0 +1,33 @@
#!/bin/bash

set -e

setup_pebble() {
curl --silent --show-error https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem > "${GITHUB_WORKSPACE}/pebble.minica.pem"
cat "${GITHUB_WORKSPACE}/pebble.minica.pem"
docker-compose --file "${GITHUB_WORKSPACE}/test/setup/pebble/docker-compose.yml" up --detach
}

wait_for_pebble() {
for endpoint in 'https://pebble:14000/dir' 'http://pebble-challtestsrv:8055'; do
while ! curl --cacert "${GITHUB_WORKSPACE}/pebble.minica.pem" "$endpoint" >/dev/null 2>&1; do
if [ $((i * 5)) -gt $((5 * 60)) ]; then
echo "$endpoint was not available under 5 minutes, timing out."
exit 1
fi
i=$((i + 1))
sleep 5
done
done
}

setup_pebble_challtestserv() {
curl --silent --show-error --data '{"ip":"10.30.50.1"}' http://pebble-challtestsrv:8055/set-default-ipv4
curl --silent --show-error --data '{"ip":""}' http://pebble-challtestsrv:8055/set-default-ipv6
curl --silent --show-error --data '{"host":"lim.it", "addresses":["10.0.0.0"]}' http://pebble-challtestsrv:8055/add-a
}

setup_pebble
wait_for_pebble
setup_pebble_challtestserv
docker-compose --file "${GITHUB_WORKSPACE}/test/setup/pebble/docker-compose.yml" logs
26 changes: 18 additions & 8 deletions test/setup/setup-local.sh
Expand Up @@ -44,7 +44,7 @@ function get_environment {
break
;;
*)
:
exit 1
;;
esac
done
Expand All @@ -55,25 +55,33 @@ function get_environment {
while true; do
echo "Which ACME CA do you want to use or remove ?"
echo ""
echo " 1) Boulder https://github.com/letsencrypt/boulder"
echo " 2) Pebble https://github.com/letsencrypt/pebble"
read -re -p "Select an option [1-2]: " option
echo " 1) Boulder https://github.com/letsencrypt/boulder"
echo " 2) Pebble with base configuration https://github.com/letsencrypt/pebble"
echo " 3) Pebble with EAB configuration https://github.com/letsencrypt/pebble"
read -re -p "Select an option [1-3]: " option
case $option in
1)
acme_ca="boulder"
break
;;
2)
acme_ca="pebble"
pebble_config="pebble-config.json"
break
;;
3)
acme_ca="pebble"
pebble_config="pebble-config-eab.json"
break
;;
*)
:
exit 1
;;
esac
done
fi
export ACME_CA="${ACME_CA:-$acme_ca}"
export PEBBLE_CONFIG="${PEBBLE_CONFIG:-$pebble_config}"
}

case $1 in
Expand All @@ -88,6 +96,7 @@ export DOCKER_GEN_CONTAINER_NAME="$DOCKER_GEN_CONTAINER_NAME"
export TEST_DOMAINS="$TEST_DOMAINS"
export SETUP="$SETUP"
export ACME_CA="$ACME_CA"
export PEBBLE_CONFIG="$PEBBLE_CONFIG"
EOF

# Add the required custom entries to /etc/hosts
Expand All @@ -109,7 +118,7 @@ EOF
if [[ "$ACME_CA" == 'boulder' ]]; then
"${GITHUB_WORKSPACE}/test/setup/setup-boulder.sh"
elif [[ "$ACME_CA" == 'pebble' ]]; then
"${GITHUB_WORKSPACE}/test/setup/setup-pebble.sh"
"${GITHUB_WORKSPACE}/test/setup/pebble/setup-pebble.sh"
else
echo "ACME_CA is not set, aborting."
exit 1
Expand All @@ -127,14 +136,15 @@ EOF
done

if [[ "$ACME_CA" == 'boulder' ]]; then
# Stop and remove boulder
# Stop and remove Boulder
docker stop boulder
docker-compose --project-name 'boulder' \
--file "${GITHUB_WORKSPACE}/go/src/github.com/letsencrypt/boulder/docker-compose.yml" \
down --volumes
docker rm boulder
elif [[ "$ACME_CA" == 'pebble' ]]; then
docker network rm acme_net
# Stop and remove Pebble
docker-compose --file "${GITHUB_WORKSPACE}/test/setup/pebble/docker-compose.yml" down
[[ -f "${GITHUB_WORKSPACE}/pebble.minica.pem" ]] && rm "${GITHUB_WORKSPACE}/pebble.minica.pem"
fi

Expand Down

0 comments on commit 87c27d2

Please sign in to comment.