aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@debian.org>2021-02-22 03:30:32 +0100
committerGuilhem Moulin <guilhem@debian.org>2021-02-22 03:30:32 +0100
commitd1be19ea9484f4c48af2de54266465d49bb1281d (patch)
tree768da9388a9ea6ed42d8d818a6433a4871a1172e /tests
parent847ae99fb1ed73fd77c6ffd30f2c554ab5892fde (diff)
parent3eba02ef820a393bd5781be9f8fcda1611ae7c3d (diff)
Merge tag 'v0.8.0' into debian/latest
Release version 0.8.0
Diffstat (limited to 'tests')
-rw-r--r--tests/account-encrypted-gpg15
-rw-r--r--tests/account-encrypted-openssl10
-rw-r--r--tests/accountd87
-rw-r--r--tests/accountd-kid59
-rw-r--r--tests/accountd-remote55
-rw-r--r--tests/apache2-proxy33
-rw-r--r--tests/apache2-static47
-rw-r--r--tests/cert-extensions91
-rw-r--r--tests/cert-install176
-rw-r--r--tests/cert-renew21
-rw-r--r--tests/cert-revoke32
-rw-r--r--tests/cert-verify43
-rw-r--r--tests/drop-privileges166
-rw-r--r--tests/nginx-proxy35
-rw-r--r--tests/nginx-static48
-rw-r--r--tests/old-accountd30
-rw-r--r--tests/old-lacme36
-rw-r--r--tests/register8
-rw-r--r--tests/spec-expansion130
-rw-r--r--tests/webservers16
20 files changed, 1138 insertions, 0 deletions
diff --git a/tests/account-encrypted-gpg b/tests/account-encrypted-gpg
new file mode 100644
index 0000000..fd1e4ac
--- /dev/null
+++ b/tests/account-encrypted-gpg
@@ -0,0 +1,15 @@
+# GnuPG-encrypted account key (WARN: this test is not interactive)
+
+PASSPHRASE="test"
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends gpg gpg-agent
+
+gpg --batch --passphrase "$PASSPHRASE" --quick-generate-key "nobody <noreply@example.net>"
+keyid="$(gpg --list-secret-key --with-colons | grep -m1 ^fpr: | cut -sd: -f10)"
+gpg --encrypt -r "$keyid" /etc/lacme/account.key
+sed -ri '0,\|^#?privkey\s*=.*| {s||privkey = gpg:/etc/lacme/account.key.gpg|}' /etc/lacme/lacme-accountd.conf
+
+export GPG_TTY="$(tty)"
+lacme account
+
+# vim: set filetype=sh :
diff --git a/tests/account-encrypted-openssl b/tests/account-encrypted-openssl
new file mode 100644
index 0000000..e79a528
--- /dev/null
+++ b/tests/account-encrypted-openssl
@@ -0,0 +1,10 @@
+# OpenSSL-encrypted account key (WARN: this test is not interactive)
+
+PASSPHRASE="test"
+
+openssl rsa -aes128 -passout pass:"$PASSPHRASE" </etc/lacme/account.key >/etc/lacme/account.enc.key
+sed -ri '0,\|^#?privkey\s*=.*| {s||privkey = file:/etc/lacme/account.enc.key|}' /etc/lacme/lacme-accountd.conf
+
+lacme account
+
+# vim: set filetype=sh :
diff --git a/tests/accountd b/tests/accountd
new file mode 100644
index 0000000..a603c16
--- /dev/null
+++ b/tests/accountd
@@ -0,0 +1,87 @@
+# Use a separate accountd server process
+
+adduser --disabled-password \
+ --home /home/lacme-account \
+ --gecos "lacme account user" \
+ --quiet lacme-account
+
+# non-existent parent directory
+! lacme --socket="/nonexistent/S.lacme" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: stat(/nonexistent): No such file or directory"
+
+# word-writable parent directory
+! lacme --socket="/tmp/S.lacme" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Insecure permissions on /tmp"
+
+# missing socket
+SOCKET=~lacme-account/S.lacme
+! lacme --socket="$SOCKET" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't stat $SOCKET: No such file or directory (Is lacme-accountd running?)"
+
+#######################################################################
+
+# missing configuration at default location
+! runuser -u lacme-account -- lacme-accountd --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Ignoring missing configuration file at default location /home/lacme-account/.config/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Error: 'privkey' is not specified"
+
+install -olacme-account -glacme-account -Ddm0700 -- \
+ ~lacme-account/.config/lacme ~lacme-account/.local/share/lacme
+mv -t ~lacme-account/.config/lacme /etc/lacme/account.key
+chown lacme-account: ~lacme-account/.config/lacme/account.key
+
+cat >~lacme-account/.config/lacme/lacme-accountd.conf <<-EOF
+ privkey = file:%E/lacme/account.key
+ logfile = %h/.local/share/lacme/accountd.log
+EOF
+
+# non-existent parent directory
+! runuser -u lacme-account -- lacme-accountd --socket="/nonexistent/S.lacme" 2>"$STDERR" || fail
+grepstderr -Fxq "Error: stat(/nonexistent): No such file or directory"
+
+# word-writable parent directory
+! runuser -u lacme-account -- lacme-accountd --socket="%T/S.lacme" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Insecure permissions on /tmp"
+
+# unset XDG_RUNTIME_DIR
+! runuser -u lacme-account -- lacme-accountd 2>"$STDERR" || fail
+grepstderr "Error: Undefined expansion %t in \"%t/S.lacme\""
+
+# non-existent $XDG_RUNTIME_DIR
+! runuser -u lacme-account -- env XDG_RUNTIME_DIR="/nonexistent" lacme-accountd 2>"$STDERR" || fail
+grepstderr -Fxq "Error: stat(/nonexistent): No such file or directory"
+
+# test running accountd
+runuser -u lacme-account -- env XDG_RUNTIME_DIR=/home/lacme-account lacme-accountd --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /home/lacme-account/.config/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /home/lacme-account/S.lacme"
+
+# make sure errors are logged too
+grep -F "Error: " ~lacme-account/.local/share/lacme/accountd.log
+
+# rotate the log and start accountd
+rm -f ~lacme-account/.local/share/lacme/accountd.log
+runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" --quiet & PID=$!
+
+# run lacme(8) multiple times using that single lacme-accountd(1) instance
+lacme --socket="$SOCKET" --debug account 2>"$STDERR" || fail
+grepstderr -F "Received extra greeting data from accountd:"
+lacme --socket="$SOCKET" newOrder 2>"$STDERR" || fail
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+# terminate accountd and check that it removes the socket
+kill $PID
+wait
+! test -e "$SOCKET"
+
+# ensure signature requests are logged
+grep -Fq "Starting lacme Account Key Manager at /home/lacme-account/S.lacme" ~lacme-account/.local/share/lacme/accountd.log
+grep -Fq "[0] >>> Accepted new connection" ~lacme-account/.local/share/lacme/accountd.log
+grep -Fq "[1] >>> Accepted new connection" ~lacme-account/.local/share/lacme/accountd.log
+grep -Fq "Shutting down and closing lacme Account Key Manager" ~lacme-account/.local/share/lacme/accountd.log
+grep -F ">>> OK signing request:" ~lacme-account/.local/share/lacme/accountd.log
+
+# vim: set filetype=sh :
diff --git a/tests/accountd-kid b/tests/accountd-kid
new file mode 100644
index 0000000..e1bd63d
--- /dev/null
+++ b/tests/accountd-kid
@@ -0,0 +1,59 @@
+# Hide JWK from ACME client and pass KID instead
+
+# get the key ID
+lacme account 2>"$STDERR" || fail
+keyid="$(sed -n "/^Key ID: / {s///p;q}" <"$STDERR")"
+
+# prepare accountd
+adduser --disabled-password \
+ --home /home/lacme-account \
+ --gecos "lacme account user" \
+ --quiet lacme-account
+
+install -olacme-account -glacme-account -Ddm0700 -- \
+ ~lacme-account/.config/lacme ~lacme-account/.local/share/lacme
+mv -t ~lacme-account/.config/lacme /etc/lacme/account.key
+chown lacme-account: ~lacme-account/.config/lacme/account.key
+
+cat >~lacme-account/.config/lacme/lacme-accountd.conf <<-EOF
+ privkey = file:%E/lacme/account.key
+ logfile = %h/.local/share/lacme/accountd.log
+ keyid = $keyid
+EOF
+
+SOCKET=~lacme-account/S.lacme
+runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" --quiet & PID=$!
+
+# newAccount resource fails as per RFC 8555 sec. 6.2 it requires a JWK
+! lacme --socket="$SOCKET" account 2>"$STDERR" || fail
+grepstderr -Fxq "WARNING: lacme-accountd supplied an empty JWK; try removing 'keyid' setting from lacme-accountd.conf if the ACME resource request fails."
+grepstderr -Fxq "400 Bad Request (Parse error reading JWS)"
+! grep -F ">>> OK signing request: header=" ~lacme-account/.local/share/lacme/accountd.log | \
+ grep -vF ">>> OK signing request: header=base64url({\"alg\":\"RS256\",\"jwk\":{}," || exit 1
+
+# rotate log and restart accountd
+kill $PID
+wait
+
+rm ~lacme-account/.local/share/lacme/accountd.log
+runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" --quiet & PID=$!
+
+# newOrder works fine without JWK
+lacme --socket="$SOCKET" newOrder
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+# and so does revokeCert (for requests authenticated with the account key)
+lacme --socket="$SOCKET" revokeCert /etc/lacme/simpletest.rsa.crt
+! lacme --socket="$SOCKET" revokeCert /etc/lacme/simpletest.rsa.crt 2>"$STDERR" || fail
+grepstderr -Fxq "Revoking /etc/lacme/simpletest.rsa.crt"
+grepstderr -Fxq "400 Bad Request (Certificate already revoked)"
+grepstderr -Fxq "Warning: Couldn't revoke /etc/lacme/simpletest.rsa.crt"
+
+kill $PID
+wait
+
+# make sure all signing requests have a KID
+! grep -F ">>> OK signing request: header=" ~lacme-account/.local/share/lacme/accountd.log | \
+ grep -vF ">>> OK signing request: header=base64url({\"alg\":\"RS256\",\"kid\":\"$keyid\"," || exit 1
+
+# vim: set filetype=sh :
diff --git a/tests/accountd-remote b/tests/accountd-remote
new file mode 100644
index 0000000..9e7f812
--- /dev/null
+++ b/tests/accountd-remote
@@ -0,0 +1,55 @@
+# Remote accountd server process
+
+adduser --disabled-password --shell /bin/sh \
+ --home /home/lacme-account \
+ --gecos "lacme account user" \
+ --quiet lacme-account
+
+chown lacme-account: /etc/lacme/account.key
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends \
+ openssh-client openssh-server
+ssh-keygen -N "" -f ~root/.ssh/id_rsa
+
+install -olacme-account -glacme-account -dm0700 ~lacme-account/.ssh
+install -olacme-account -glacme-account -m0644 ~root/.ssh/id_rsa.pub ~lacme-account/.ssh/authorized_keys
+{ echo -n "[127.0.0.1]:2222 "; cat /etc/ssh/ssh_host_rsa_key.pub; } >~root/.ssh/known_hosts
+
+cat >/etc/ssh/sshd_config <<-EOF
+ Port 2222
+ ListenAddress 127.0.0.1
+ ChallengeResponseAuthentication no
+ PasswordAuthentication no
+ UsePAM yes
+EOF
+install -oroot -groot -dm0755 /run/sshd
+/usr/sbin/sshd
+
+sed -ri 's|^#?command\s*=.*/lacme-accountd$|command = ssh -Tp2222 -llacme-account 127.0.0.1 lacme-accountd|' /etc/lacme/lacme.conf
+sed -ri 's|^#?config\s*=.*|config = /etc/lacme/lacme-accountd.conf|' /etc/lacme/lacme.conf
+lacme newOrder --debug 2>"$STDERR" || fail # intentionally use --debug, ssh should tunnel stdin + stdout + stderr
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+# and now with an authorized_keys(5) restriction
+sed -ri "s|^[^#]|restrict,from=\"127.0.0.1\",command=\"/usr/bin/lacme-accountd --quiet --stdio\" &|" ~lacme-account/.ssh/authorized_keys
+rm -vf /etc/lacme/simpletest.rsa.crt
+
+! lacme newOrder 2>"$STDERR" || fail # --config= (and --debug) should be ignored
+grepstderr -Fxq "Error: 'privkey' is not specified"
+grepstderr -Fxq "[simpletest-rsa] Error: Couldn't issue X.509 certificate!"
+
+install -olacme-account -glacme-account -Ddm0700 -- \
+ ~lacme-account/.config/lacme ~lacme-account/.local/share/lacme
+mv -t ~lacme-account/.config/lacme /etc/lacme/account.key
+cat >~lacme-account/.config/lacme/lacme-accountd.conf <<-EOF
+ privkey = file:%E/lacme/account.key
+ logfile = %h/.local/share/lacme/accountd.log
+EOF
+
+lacme newOrder
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+# ensure signature requests are logged
+grep -F ">>> OK signing request:" ~lacme-account/.local/share/lacme/accountd.log
+
+# vim: set filetype=sh :
diff --git a/tests/apache2-proxy b/tests/apache2-proxy
new file mode 100644
index 0000000..016b426
--- /dev/null
+++ b/tests/apache2-proxy
@@ -0,0 +1,33 @@
+# Use Apache2 as reverse proxy for lacme's internal webserver using the
+# provided snippet
+
+# bind the webserver to the default listening address
+sed -i 's|^listen\s*=|#&|' /etc/lacme/lacme.conf
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends apache2 curl
+
+a2enmod proxy_http
+a2enconf lacme
+
+mkdir /run/apache2
+( set +eux && . /etc/apache2/envvars && apache2 )
+
+# ensure that requests to the root URI and challenge URIs yield 502 Bad Gateway before starting the webserver
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/)"; [ $rv -eq 503 ]
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/foo)"; [ $rv -eq 503 ]
+
+lacme --debug newOrder 2>"$STDERR" || fail
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+grepstderr -Fq "Forking ACME webserver bound to /run/lacme-www.socket, child PID "
+grepstderr -Fq "Forking lacme-accountd, child PID "
+grepstderr -Fq "Forking /usr/libexec/lacme/client, child PID "
+grepstderr -Fq "Shutting down lacme-accountd"
+grepstderr -Fq "Shutting down ACME webserver bound to /run/lacme-www.socket"
+grepstderr -Eq "Incoming connection: GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+$"
+
+# ensure apache2 was indeed used to serve challenge responses (Let's Encrypt caches validation results)
+grep -E "\"GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+\" 200 .* \(([^)]+; )*Let's Encrypt validation server(; [^)]+)*\)\"$" \
+ /var/log/apache2/access.log
+
+# vim: set filetype=sh :
diff --git a/tests/apache2-static b/tests/apache2-static
new file mode 100644
index 0000000..f697cd7
--- /dev/null
+++ b/tests/apache2-static
@@ -0,0 +1,47 @@
+# Use Nginx to directly serve ACME challenge responses using the
+# provided snippet
+
+# bind the webserver to the default listening address
+sed -i 's|^listen\s*=|#&|' /etc/lacme/lacme.conf
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends apache2 curl
+
+ln -fs /etc/lacme/apache2-static.conf /etc/apache2/conf-available/lacme.conf
+a2enmod proxy_http
+a2enconf lacme
+
+mkdir /run/apache2
+( set +eux && . /etc/apache2/envvars && apache2 )
+
+# 'challenge-directory' set to a non-existent directory
+sed -ri 's|^#?challenge-directory\s*=.*|challenge-directory = /var/www/acme-challenge|' /etc/lacme/lacme.conf
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fqx "opendir(/var/www/acme-challenge): No such file or directory"
+
+# ensure that requests to the root URI and challenge URIs respectively yield 403 Forbidden (no index) and 404 Not Found
+install -o_lacme-client -gwww-data -m0750 -d /var/www/acme-challenge
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/)"; [ $rv -eq 403 ]
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/foo)"; [ $rv -eq 404 ]
+
+# 'challenge-directory' set to a non-empty directory
+touch /var/www/acme-challenge/.stamp
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fqx "Error: Refusing to use non-empty challenge directory /var/www/acme-challenge"
+
+rm -f /var/www/acme-challenge/.stamp
+lacme --debug newOrder 2>"$STDERR" || fail
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+ngrepstderr -Fq "Forking ACME webserver"
+grepstderr -Fq "Using existing webserver on /var/www/acme-challenge"
+grepstderr -Fq "Forking lacme-accountd, child PID "
+grepstderr -Fq "Forking /usr/libexec/lacme/client, child PID "
+grepstderr -Fq "Shutting down lacme-accountd"
+ngrepstderr -Fq "Shutting down ACME webserver"
+ngrepstderr -Eq "Incoming connection( from \S+)?: GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+$"
+
+# ensure apache2 was indeed used to serve challenge responses (Let's Encrypt caches validation results)
+grep -E "\"GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+\" 200 .* \(([^)]+; )*Let's Encrypt validation server(; [^)]+)*\)\"$" \
+ /var/log/apache2/access.log
+
+# vim: set filetype=sh :
diff --git a/tests/cert-extensions b/tests/cert-extensions
new file mode 100644
index 0000000..a397ee5
--- /dev/null
+++ b/tests/cert-extensions
@@ -0,0 +1,91 @@
+# X509v3 certificate extension, cf. x509v3_config(5ssl)
+
+x509_check() {
+ local cert="$1" ext out
+ out="$(mktemp --tmpdir)"
+ ext="basicConstraints,subjectAltName,keyUsage,extendedKeyUsage,tlsfeature"
+ openssl x509 -noout -subject -ext "$ext" -nameopt compat <"$cert" >"$out"
+ diff --unified --color=auto -b --label="a/${cert#/}" --label="b/${cert#/}" -- - "$out"
+}
+
+# default settings (the ACME server adds a subjectAltName with the Common Name)
+openssl genpkey -algorithm RSA -out /etc/lacme/test1.key
+commonName="$(head -c10 /dev/urandom | base32 -w0 | tr "[A-Z]" "[a-z]").$DOMAINNAME"
+cat >"/etc/lacme/lacme-certs.conf.d/test1.conf" <<- EOF
+ [test1]
+ certificate-key = /etc/lacme/test1.key
+ certificate-chain = /etc/lacme/test1.crt
+ subject = /CN=$commonName
+EOF
+
+lacme newOrder test1
+test /etc/lacme/test1.crt -nt /etc/lacme/test1.key
+x509_check /etc/lacme/test1.crt <<-EOF
+ subject=/CN=$commonName
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 Subject Alternative Name:
+ DNS:$commonName
+EOF
+
+# subjectAltName
+openssl genpkey -algorithm RSA -out /etc/lacme/test2.key
+commonName="$(head -c10 /dev/urandom | base32 -w0 | tr "[A-Z]" "[a-z]").$DOMAINNAME"
+subjectAltName=""
+for i in $(seq 1 8); do
+ subjectAltName="${subjectAltName:+"$subjectAltName "}$(head -c10 /dev/urandom | base32 -w0 | tr "[A-Z]" "[a-z]").$DOMAINNAME"
+done
+cat >"/etc/lacme/lacme-certs.conf.d/test2.conf" <<- EOF
+ [test2]
+ certificate-key = /etc/lacme/test2.key
+ certificate-chain = /etc/lacme/test2.crt
+ subject = /CN=$commonName
+ subjectAltName = DNS:$(echo "$subjectAltName" | sed -r "s/ /, DNS:/g")
+EOF
+
+lacme newOrder test2
+test /etc/lacme/test2.crt -nt /etc/lacme/test2.key
+x509_check /etc/lacme/test2.crt <<-EOF
+ subject=/CN=$commonName
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 Subject Alternative Name:
+ DNS:$(echo "$commonName" "$subjectAltName" | tr " " "\\n" | sort -u | paste -sd" " | sed -r "s/ /, DNS:/g")
+EOF
+
+# tlsfeature
+openssl genpkey -algorithm RSA -out /etc/lacme/test3.key
+commonName="$(head -c10 /dev/urandom | base32 -w0 | tr "[A-Z]" "[a-z]").$DOMAINNAME"
+cat >"/etc/lacme/lacme-certs.conf.d/test3.conf" <<- EOF
+ [test3]
+ certificate-key = /etc/lacme/test3.key
+ certificate-chain = /etc/lacme/test3.crt
+ subject = /CN=$commonName
+ tlsfeature = status_request
+EOF
+
+lacme newOrder test3
+test /etc/lacme/test3.crt -nt /etc/lacme/test3.key
+x509_check /etc/lacme/test3.crt <<-EOF
+ subject=/CN=$commonName
+ X509v3 Key Usage: critical
+ Digital Signature, Key Encipherment
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:FALSE
+ X509v3 Subject Alternative Name:
+ DNS:$commonName
+ TLS Feature:
+ status_request
+EOF
+
+# vim: set filetype=sh :
diff --git a/tests/cert-install b/tests/cert-install
new file mode 100644
index 0000000..f2147d2
--- /dev/null
+++ b/tests/cert-install
@@ -0,0 +1,176 @@
+# Certificate installation and post-issuance notification
+
+# at least one of 'certificate' or 'certificate-chain' is required
+cat >"/etc/lacme/lacme-certs.conf.d/bad1.conf" <<- EOF
+ [bad1]
+ certificate-key = /etc/lacme/bad1.key
+ subject = /CN=bad1.$DOMAINNAME
+EOF
+! lacme newOrder bad1 2>"$STDERR" || fail newOrder bad1
+grepstderr -Fxq "[bad1] Warning: Missing 'certificate' and 'certificate-chain', skipping"
+
+# 'subject' is required
+cat >"/etc/lacme/lacme-certs.conf.d/bad2.conf" <<- EOF
+ [bad2]
+ certificate-key = /etc/lacme/bad2.key
+ certificate = /etc/lacme/bad2.crt
+EOF
+! lacme newOrder bad2 2>"$STDERR" || fail newOrder bad2
+grepstderr -Fxq "[bad2] Warning: Couldn't generate CSR, skipping"
+
+# 'certificate-key' is required
+cat >"/etc/lacme/lacme-certs.conf.d/bad3.conf" <<- EOF
+ [bad3]
+ certificate = /etc/lacme/bad3.crt
+ subject = /CN=bad3.$DOMAINNAME
+EOF
+! lacme newOrder bad3 2>"$STDERR" || fail newOrder bad3
+grepstderr -Fxq "[bad3] Warning: Couldn't generate CSR, skipping"
+
+
+# 'certificate' installs only the leaf certificate
+openssl genpkey -algorithm RSA -out /etc/lacme/test1.key
+subject="/CN=$(head -c10 /dev/urandom | base32 -w0).$DOMAINNAME"
+cat >"/etc/lacme/lacme-certs.conf.d/test1.conf" <<- EOF
+ [test1]
+ certificate-key = /etc/lacme/test1.key
+ certificate = /etc/lacme/test1.crt
+ subject = $subject
+EOF
+
+lacme newOrder test1 2>"$STDERR" || fail newOrder test1
+test /etc/lacme/test1.crt -nt /etc/lacme/test1.key
+sed -n "0,/^-----END CERTIFICATE-----$/ p" /etc/lacme/test1.crt >/etc/lacme/test1.pem
+diff --unified /etc/lacme/test1.crt /etc/lacme/test1.pem
+
+
+check_hash() {
+ local p1="$1" p2 s1 s2
+ s1="$(openssl x509 -noout -hash <"$p1")"
+ for p2 in /usr/share/lacme/ca-certificates.pem.*; do
+ s2="$(openssl x509 -noout -hash <"$p2")"
+ if [ "$s1" = "$s2" ]; then
+ return 0
+ fi
+ done
+ return 1
+}
+csplit -f /usr/share/lacme/ca-certificates.pem. /usr/share/lacme/ca-certificates.crt \
+ "/-----BEGIN CERTIFICATE-----/" "{*}"
+rm -f /usr/share/lacme/ca-certificates.pem.00
+
+# 'certificate-chain' appends the chain of trust
+openssl genpkey -algorithm RSA -out /etc/lacme/test2.key
+cat >"/etc/lacme/lacme-certs.conf.d/test2.conf" <<- EOF
+ [test2]
+ certificate-key = /etc/lacme/test2.key
+ certificate-chain = /etc/lacme/test2.crt
+ subject = $subject
+EOF
+
+lacme newOrder test2 2>"$STDERR" || fail newOrder test2
+test /etc/lacme/test2.crt -nt /etc/lacme/test2.key
+csplit -f /etc/lacme/test2.chain.pem /etc/lacme/test2.crt \
+ "/-----BEGIN CERTIFICATE-----/" "{*}"
+test -s /etc/lacme/test2.chain.pem01 # leaf cert (00 is empty)
+rm -f /etc/lacme/test2.chain.pem0[01]
+test -s /etc/lacme/test2.chain.pem02 # depth 1
+
+# all certificates at depth >=1 must be in our CA bundle
+for p in /etc/lacme/test2.chain.pem*; do
+ check_hash "$p"
+done
+
+# 'certificate' + 'certificate-chain'
+openssl genpkey -algorithm RSA -out /etc/lacme/test3.key
+cat >"/etc/lacme/lacme-certs.conf.d/test3.conf" <<- EOF
+ [test3]
+ certificate-key = /etc/lacme/test3.key
+ certificate = /etc/lacme/test3.pem
+ certificate-chain = /etc/lacme/test3.crt
+ subject = $subject
+EOF
+
+lacme newOrder test3 2>"$STDERR" || fail newOrder test3
+test /etc/lacme/test3.pem -nt /etc/lacme/test3.key
+test /etc/lacme/test3.crt -nt /etc/lacme/test3.key
+csplit -f /etc/lacme/test3.chain.pem /etc/lacme/test3.crt \
+ "/-----BEGIN CERTIFICATE-----/" "{*}"
+sed -i "/^$/d" /etc/lacme/test3.chain.pem*
+diff -q /etc/lacme/test3.chain.pem01 /etc/lacme/test3.pem
+st="$(stat -c "%U:%G %#a" /etc/lacme/test3.pem)"
+[ "$st" = "root:root 0644" ]
+st="$(stat -c "%U:%G %#a" /etc/lacme/test3.crt)"
+[ "$st" = "root:root 0644" ]
+
+# chmod user
+openssl genpkey -algorithm RSA -out /etc/lacme/test4.key
+cat >"/etc/lacme/lacme-certs.conf.d/test4.conf" <<- EOF
+ [test4]
+ certificate-key = /etc/lacme/test4.key
+ certificate = /etc/lacme/test4.pem
+ certificate-chain = /etc/lacme/test4.crt
+ chown = nobody
+ subject = $subject
+EOF
+
+lacme newOrder test4 2>"$STDERR" || fail newOrder test4
+st="$(stat -c "%U:%G %#a" /etc/lacme/test4.pem)"
+[ "$st" = "nobody:root 0644" ]
+st="$(stat -c "%U:%G %#a" /etc/lacme/test4.crt)"
+[ "$st" = "nobody:root 0644" ]
+
+# chmod user:group
+openssl genpkey -algorithm RSA -out /etc/lacme/test5.key
+cat >"/etc/lacme/lacme-certs.conf.d/test5.conf" <<- EOF
+ [test5]
+ certificate-key = /etc/lacme/test5.key
+ certificate = /etc/lacme/test5.pem
+ certificate-chain = /etc/lacme/test5.crt
+ chown = nobody:nogroup
+ subject = $subject
+EOF
+
+lacme newOrder test5 2>"$STDERR" || fail newOrder test5
+st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)"
+[ "$st" = "nobody:nogroup 0644" ]
+st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)"
+[ "$st" = "nobody:nogroup 0644" ]
+
+# chown
+openssl genpkey -algorithm RSA -out /etc/lacme/test6.key
+cat >"/etc/lacme/lacme-certs.conf.d/test6.conf" <<- EOF
+ [test6]
+ certificate-key = /etc/lacme/test6.key
+ certificate = /etc/lacme/test6.pem
+ certificate-chain = /etc/lacme/test6.crt
+ chmod = 0400
+ subject = $subject
+EOF
+
+lacme newOrder test6 2>"$STDERR" || fail newOrder test6
+st="$(stat -c "%U:%G %#a" /etc/lacme/test6.pem)"
+[ "$st" = "root:root 0400" ]
+st="$(stat -c "%U:%G %#a" /etc/lacme/test6.crt)"
+[ "$st" = "root:root 0400" ]
+
+# post-issuance notification
+openssl genpkey -algorithm RSA -out /etc/lacme/test7.key
+cat >"/etc/lacme/lacme-certs.conf.d/test7.conf" <<- EOF
+ [test7]
+ certificate-key = /etc/lacme/test7.key
+ certificate-chain = /etc/lacme/test7.crt
+ subject = $subject
+ notify = touch /tmp/test7.notify
+EOF
+
+lacme newOrder test7 2>"$STDERR" || fail newOrder test7
+grepstderr -Fxq "Running notification command \`touch /tmp/test7.notify\`"
+test -e /tmp/test7.notify
+
+rm -f /tmp/test7.notify
+lacme newOrder test7 2>"$STDERR" || fail newOrder test7
+ngrepstderr -Fq "Running notification command"
+! test -e /tmp/test7.notify
+
+# vim: set filetype=sh :
diff --git a/tests/cert-renew b/tests/cert-renew
new file mode 100644
index 0000000..aca1b34
--- /dev/null
+++ b/tests/cert-renew
@@ -0,0 +1,21 @@
+# Skip renewal for recent enough certificates
+
+lacme newOrder 2>"$STDERR" || fail
+grepstderr -Exq "Installing X.509 certificate chain /etc/lacme/simpletest.rsa.crt"
+ino1="$(stat -c%i /etc/lacme/simpletest.rsa.crt)"
+
+lacme newOrder 2>"$STDERR" || fail
+grepstderr -Ex "^\[simpletest-rsa\] Valid until .*, skipping"
+
+lacme newOrder --force 2>"$STDERR" || fail
+grepstderr -Exq "Installing X.509 certificate chain /etc/lacme/simpletest.rsa.crt"
+ino2="$(stat -c%i /etc/lacme/simpletest.rsa.crt)"
+test "$ino1" != "$ino2" # we never truncate existing certificates
+
+sed -ri 's|^#?min-days\s*=.*|min-days = 90|' /etc/lacme/lacme-certs.conf
+lacme newOrder 2>"$STDERR" || fail
+grepstderr -Exq "Installing X.509 certificate chain /etc/lacme/simpletest.rsa.crt"
+ino3="$(stat -c%i /etc/lacme/simpletest.rsa.crt)"
+test "$ino2" != "$ino3" # we never truncate existing certificates
+
+# vim: set filetype=sh :
diff --git a/tests/cert-revoke b/tests/cert-revoke
new file mode 100644
index 0000000..f3d585e
--- /dev/null
+++ b/tests/cert-revoke
@@ -0,0 +1,32 @@
+# Certification revocation, using either the account key or the
+# certificate key
+
+# also check issuance for ECDSA keys
+openssl genpkey -algorithm EC -out /etc/lacme/simpletest.ecdsa.key \
+ -pkeyopt ec_paramgen_curve:P-256 \
+ -pkeyopt ec_param_enc:named_curve
+
+sed "s/rsa/ecdsa/" /etc/lacme/lacme-certs.conf.d/simpletest-rsa.conf > \
+ /etc/lacme/lacme-certs.conf.d/simpletest-ecdsa.conf
+
+# issue both RSA and ECDSA certificates
+lacme newOrder 2>"$STDERR" || fail newOrder
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+test /etc/lacme/simpletest.ecdsa.crt -nt /etc/lacme/simpletest.ecdsa.key
+
+# revoke the ECDSA certificate using the account key
+lacme revokeCert /etc/lacme/simpletest.ecdsa.crt
+! lacme revokeCert /etc/lacme/simpletest.ecdsa.crt 2>"$STDERR" || fail
+grepstderr -Fxq "Revoking /etc/lacme/simpletest.ecdsa.crt"
+grepstderr -Fxq "400 Bad Request (Certificate already revoked)"
+grepstderr -Fxq "Warning: Couldn't revoke /etc/lacme/simpletest.ecdsa.crt"
+
+# and the RSA certificate using the service key
+mv -vfT /etc/lacme/simpletest.rsa.key /etc/lacme/account.key
+lacme revokeCert /etc/lacme/simpletest.rsa.crt
+! lacme revokeCert /etc/lacme/simpletest.rsa.crt 2>"$STDERR" || fail
+grepstderr -Fxq "Revoking /etc/lacme/simpletest.rsa.crt"
+grepstderr -Fxq "400 Bad Request (Certificate already revoked)"
+grepstderr -Fxq "Warning: Couldn't revoke /etc/lacme/simpletest.rsa.crt"
+
+# vim: set filetype=sh :
diff --git a/tests/cert-verify b/tests/cert-verify
new file mode 100644
index 0000000..49629f2
--- /dev/null
+++ b/tests/cert-verify
@@ -0,0 +1,43 @@
+# Certificate verification
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends ssl-cert
+
+# add staging root certificates to the trust store to emulate the production environment
+for ca in /usr/share/lacme/letsencrypt-stg-root-*.pem; do
+ ln -sT "$ca" "/usr/local/share/ca-certificates/$(basename -s.pem "$ca").crt"
+done
+update-ca-certificates
+
+# test (modified) trust store for intermediate certificates
+openssl verify -no-CAfile -CApath /etc/ssl/certs -show_chain /usr/share/lacme/letsencrypt-stg-int-*.pem
+openssl verify -no-CApath -CAfile /etc/ssl/certs/ca-certificates.crt -show_chain /usr/share/lacme/letsencrypt-stg-int-*.pem
+
+mv /usr/share/lacme/ca-certificates.crt /usr/share/lacme/ca-certificates.crt.back
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /usr/share/lacme/ca-certificates.crt for reading, No such file or directory"
+grepstderr -Fxq "[simpletest-rsa] Error: Received invalid X.509 certificate from ACME server!"
+
+# verification error for unrelated CA bundle
+cat /etc/ssl/certs/ssl-cert-snakeoil.pem >/usr/share/lacme/ca-certificates.crt
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fxq "error 20 at 0 depth lookup: unable to get local issuer certificate"
+grepstderr -Fxq "[simpletest-rsa] Error: Received invalid X.509 certificate from ACME server!"
+
+# verification error when the CA bundle contains only the root certificates
+cat /usr/share/lacme/letsencrypt-stg-root-*.pem >/usr/share/lacme/ca-certificates.crt
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fxq "error 20 at 0 depth lookup: unable to get local issuer certificate"
+grepstderr -Fxq "[simpletest-rsa] Error: Received invalid X.509 certificate from ACME server!"
+
+# verification error when the CA bundle contains only the intermediate certificates
+cat /usr/share/lacme/letsencrypt-stg-int-*.pem >/usr/share/lacme/ca-certificates.crt
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fxq "error 2 at 1 depth lookup: unable to get issuer certificate"
+grepstderr -Fxq "[simpletest-rsa] Error: Received invalid X.509 certificate from ACME server!"
+
+# use saved bundle as custom CAfile
+sed -ri 's|^#?CAfile\s*=.*|CAfile = /usr/share/lacme/ca-certificates.crt.back|' /etc/lacme/lacme-certs.conf
+lacme newOrder 2>"$STDERR" || fail
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+# vim: set filetype=sh :
diff --git a/tests/drop-privileges b/tests/drop-privileges
new file mode 100644
index 0000000..0596e31
--- /dev/null
+++ b/tests/drop-privileges
@@ -0,0 +1,166 @@
+# Check privilige drop: UID/GID changes, chdir, environment, and file
+# descriptors
+
+# create wrapper to inspect processes
+STATUSDIR="/dev/shm/lacme-wrap"
+install -oroot -groot -m0755 /dev/stdin /run/lacme-wrap <<-EOF
+ #!/bin/sh
+ set -ue
+ PATH="/usr/bin:/bin"
+ export PATH
+
+ prefix="$STATUSDIR/\${1##*[/-]}"
+ cat </proc/\$\$/status >"\$prefix/status"
+ pwd >"\$prefix/cwd"
+ stat -c "%U:%G %#a" . >"\$prefix/cwd-mod"
+ sort -z </proc/\$\$/environ | tr "\\0" "\\n" >"\$prefix/environ"
+ ( find -P /proc/\$\$/fd -mindepth 1 \! -lname "\$0" -printf "%P %#m %U:%G %l\\n" >"\$prefix/fd" )
+
+ exec "\$@"
+EOF
+
+# also check privilege drop for the spawned accountd
+adduser --system --group \
+ --home /nonexistent --no-create-home \
+ --gecos "lacme account user" \
+ --quiet lacme-account
+sed -ri 's|^#user\s*=\s*$|user = lacme-account|' /etc/lacme/lacme.conf
+sed -ri 's|^#group\s*=\s*$|group = lacme-account|' /etc/lacme/lacme.conf
+chown lacme-account: /etc/lacme/account.key
+
+install -oroot -groot -dm0755 -- "$STATUSDIR"
+install -olacme-account -groot -dm0700 -- "$STATUSDIR/accountd"
+install -o_lacme-client -groot -dm0700 -- "$STATUSDIR/client"
+install -o_lacme-www -groot -dm0700 -- "$STATUSDIR/webserver"
+
+# test with a group that's not the primary group (nogroup) of _lacme-www etc
+addgroup --system nogroup2
+sed -ri 's|^#?group\s*=\s*nogroup$|group = nogroup2|' /etc/lacme/lacme.conf
+sed -ri 's|^#?command\s*=.*/lacme-accountd$|command = /run/lacme-wrap /usr/bin/lacme-accountd|' /etc/lacme/lacme.conf
+sed -ri 's|^#?command\s*=.*/lacme/client$|command = /run/lacme-wrap /usr/libexec/lacme/client|' /etc/lacme/lacme.conf
+sed -ri 's|^#?command\s*=.*/lacme/webserver$|command = /run/lacme-wrap /usr/libexec/lacme/webserver|' /etc/lacme/lacme.conf
+sed -ri 's|^#?config\s*=\s*$|config = /etc/lacme/lacme-accountd.conf|' /etc/lacme/lacme.conf
+
+check_status() {
+ local path="$STATUSDIR/$1/status" user="$2" group="$3"
+ UID="$(getent passwd "$user" | cut -sd: -f3)"
+ GID="$(getent group "$group" | cut -sd: -f3)"
+ [ -n "$UID" -a -n "$GID" ] || return 1
+ grep -Ex "Uid:\\s+$UID\\s+$UID\\s+$UID\\s+$UID" "$path" || return 1
+ grep -Ex "Gid:\\s+$GID\\s+$GID\\s+$GID\\s+$GID" "$path" || return 1
+ grep -Ex "Groups:\s+$GID\s*" "$path" || return 1
+}
+check_cwd() {
+ local path="$STATUSDIR/$1/cwd" dir="$2" cwd
+ cwd="$(cat <"$path")" || return 1
+ [ "$cwd" = "$dir" ] || return 1
+}
+
+check_accountd() {
+ local socket_ino stderr prefix="$STATUSDIR/accountd"
+ check_status accountd lacme-account lacme-account || return 1
+ check_cwd accountd / || return 1
+
+ diff --label="a/accountd/environ" --label="b/accountd/environ" \
+ --color=auto --unified "$prefix/environ" - <<-EOF
+ HOME=/nonexistent
+ LOGNAME=lacme-account
+ PATH=/usr/bin:/bin
+ SHELL=/usr/sbin/nologin
+ TERM=$TERM
+ USER=lacme-account
+ EOF
+
+ stderr="$(readlink -e "/proc/$$/fd/2")"
+ socket_ino="$(sed -rn '/^0 .* socket:\[([0-9]+)\]$/ {s//\1/p;q}' "$prefix/fd")"
+ [ -n "$socket_ino" ] || return 1
+ grep -Fxq "0 0700 $UID:$GID socket:[$socket_ino]" "$prefix/fd" || return 1
+ grep -Fxq "1 0700 $UID:$GID socket:[$socket_ino]" "$prefix/fd" || return 1
+ grep -Fxq "2 0700 $UID:$GID $stderr" "$prefix/fd" || return 1
+ sed -ri '\#^[012] #d' "$prefix/fd"
+ ! test -s "$prefix/fd" || return 1
+}
+check_client() {
+ local command="$1" cwd="$2" UID GID stdout stderr prefix="$STATUSDIR/client"
+ check_status client _lacme-client nogroup2
+ check_cwd client "$cwd"
+
+ diff --label="a/client/environ" --label="b/client/environ" \
+ --color=auto --unified "$prefix/environ" - <<-EOF
+ DEBUG=0
+ HOME=/nonexistent
+ LOGNAME=_lacme-client
+ PATH=/usr/bin:/bin
+ SHELL=/usr/sbin/nologin
+ TERM=$TERM
+ USER=_lacme-client
+ EOF
+
+ stdout="$(readlink -e "/proc/$$/fd/1")"
+ stderr="$(readlink -e "/proc/$$/fd/2")"
+ if [ "$command" = "account" ]; then # no pipe
+ grep -Fxq "0 0500 $UID:$GID /dev/null" "$prefix/fd" || return 1
+ grep -Fxq "1 0700 $UID:$GID $stdout" "$prefix/fd" || return 1
+ elif [ "$command" = "order" ]; then
+ grep -Exq "0 0500 $UID:$GID pipe:\[[0-9]+\]" "$prefix/fd" || return 1
+ grep -Exq "1 0300 $UID:$GID pipe:\[[0-9]+\]" "$prefix/fd" || return 1
+ else
+ exit 1
+ fi
+ grep -Fxq "2 0700 $UID:$GID $stderr" "$prefix/fd" || return 1
+ sed -ri '\#^[012] #d' "$prefix/fd"
+
+ grep -Exq "[0-9]+ 0700 $UID:$GID socket:\[[0-9]+\]" "$prefix/fd" || return 1
+ sed -ri '0,\#^[0-9]+ .* socket:\[[0-9]+\]$# {//d}' "$prefix/fd"
+
+ grep -Exq "[0-9]+ 0500 $UID:$GID /etc/lacme/lacme\.conf" "$prefix/fd" || return 1
+ sed -ri '0,\#^[0-9]+ .* /etc/lacme/lacme\.conf$# {//d}' "$prefix/fd"
+ ! test -s "$prefix/fd" || return 1
+}
+check_webserver() {
+ local cwd="$1" UID GID stdout stderr prefix="$STATUSDIR/webserver"
+ check_status webserver _lacme-www nogroup2
+ check_cwd webserver "$cwd"
+
+ diff --label="a/webserver/environ" --label="b/webserver/environ" \
+ --color=auto --unified "$prefix/environ" - <<-EOF
+ DEBUG=0
+ HOME=/nonexistent
+ LOGNAME=_lacme-www
+ PATH=/usr/bin:/bin
+ SHELL=/usr/sbin/nologin
+ TERM=$TERM
+ USER=_lacme-www
+ EOF
+
+ stdout="$(readlink -e "/proc/$$/fd/1")"
+ stderr="$(readlink -e "/proc/$$/fd/2")"
+ grep -Fxq "0 0500 $UID:$GID /dev/null" "$prefix/fd" || return 1
+ grep -Fxq "1 0700 $UID:$GID $stdout" "$prefix/fd" || return 1
+ grep -Fxq "2 0700 $UID:$GID $stderr" "$prefix/fd" || return 1
+ sed -ri '\#^[012] #d' "$prefix/fd"
+
+ grep -Exq "[0-9]+ 0700 $UID:$GID socket:\[[0-9]+\]" "$prefix/fd" || return 1
+ sed -ri '0,\#^[0-9]+ .* socket:\[[0-9]+\]$# {//d}' "$prefix/fd"
+ ! test -s "$prefix/fd" || return 1
+}
+
+lacme account
+check_accountd
+check_client account /
+! test -e "$STATUSDIR/webserver/status" # account 'command' doesn't start the webserver
+
+lacme newOrder
+check_accountd
+challenge_dir="$(cat "$STATUSDIR/webserver/cwd")"
+[ "${challenge_dir#"/tmp/acme-challenge."}" != "$challenge_dir" ] || exit 1
+check_client order "$challenge_dir"
+check_webserver "$challenge_dir"
+
+# the temporary challenge directory is created with permissive mode
+diff --label="a/webserver/cwd" --label="b/webserver/cwd" \
+ --color=auto --unified "$STATUSDIR/webserver/cwd-mod" - <<-EOF
+ _lacme-client:root 0755
+EOF
+
+# vim: set filetype=sh :
diff --git a/tests/nginx-proxy b/tests/nginx-proxy
new file mode 100644
index 0000000..b16fd10
--- /dev/null
+++ b/tests/nginx-proxy
@@ -0,0 +1,35 @@
+# Use Nginx as reverse proxy for lacme's internal webserver using the
+# provided snippet
+
+# bind the webserver to the default listening address
+sed -i 's|^listen\s*=|#&|' /etc/lacme/lacme.conf
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends nginx-light curl
+cat >/etc/nginx/sites-enabled/default <<-EOF
+ server {
+ listen 80 default_server;
+ server_name _;
+ include /etc/lacme/nginx.conf;
+ }
+EOF
+nginx
+
+# ensure that requests to the root URI and challenge URIs yield 502 Bad Gateway before starting the webserver
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/)"; [ $rv -eq 502 ]
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/foo)"; [ $rv -eq 502 ]
+
+lacme --debug newOrder 2>"$STDERR" || fail
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+grepstderr -Fq "Forking ACME webserver bound to /run/lacme-www.socket, child PID "
+grepstderr -Fq "Forking lacme-accountd, child PID "
+grepstderr -Fq "Forking /usr/libexec/lacme/client, child PID "
+grepstderr -Fq "Shutting down lacme-accountd"
+grepstderr -Fq "Shutting down ACME webserver bound to /run/lacme-www.socket"
+grepstderr -Eq "Incoming connection: GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+$"
+
+# ensure nginx was indeed used to serve challenge responses (Let's Encrypt caches validation results)
+grep -E "\"GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+\" 200 .* \(([^)]+; )*Let's Encrypt validation server(; [^)]+)*\)\"$" \
+ /var/log/nginx/access.log
+
+# vim: set filetype=sh :
diff --git a/tests/nginx-static b/tests/nginx-static
new file mode 100644
index 0000000..4bb0e0d
--- /dev/null
+++ b/tests/nginx-static
@@ -0,0 +1,48 @@
+# Use Nginx to directly serve ACME challenge responses using the
+# provided snippet
+
+# bind the webserver to the default listening address
+sed -i 's|^listen\s*=|#&|' /etc/lacme/lacme.conf
+
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends nginx-light curl
+cat >/etc/nginx/sites-enabled/default <<-EOF
+ server {
+ listen 80 default_server;
+ server_name _;
+ include /etc/lacme/nginx-static.conf;
+ }
+EOF
+nginx
+
+# 'challenge-directory' set to a non-existent directory
+sed -ri 's|^#?challenge-directory\s*=.*|challenge-directory = /var/www/acme-challenge|' /etc/lacme/lacme.conf
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fqx "opendir(/var/www/acme-challenge): No such file or directory"
+
+# ensure that requests to the root URI and challenge URIs respectively yield 403 Forbidden (no index) and 404 Not Found
+install -o_lacme-client -gwww-data -m0750 -d /var/www/acme-challenge
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/)"; [ $rv -eq 403 ]
+rv="$(curl -w"%{http_code}" -so/dev/null http://127.0.0.1/.well-known/acme-challenge/foo)"; [ $rv -eq 404 ]
+
+# 'challenge-directory' set to a non-empty directory
+touch /var/www/acme-challenge/.stamp
+! lacme newOrder 2>"$STDERR" || fail
+grepstderr -Fqx "Error: Refusing to use non-empty challenge directory /var/www/acme-challenge"
+
+rm -f /var/www/acme-challenge/.stamp
+lacme --debug newOrder 2>"$STDERR" || fail
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+ngrepstderr -Fq "Forking ACME webserver"
+grepstderr -Fq "Using existing webserver on /var/www/acme-challenge"
+grepstderr -Fq "Forking lacme-accountd, child PID "
+grepstderr -Fq "Forking /usr/libexec/lacme/client, child PID "
+grepstderr -Fq "Shutting down lacme-accountd"
+ngrepstderr -Fq "Shutting down ACME webserver"
+ngrepstderr -Eq "Incoming connection( from \S+)?: GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+$"
+
+# ensure nginx was indeed used to serve challenge responses (Let's Encrypt caches validation results)
+grep -E "\"GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+\" 200 .* \(([^)]+; )*Let's Encrypt validation server(; [^)]+)*\)\"$" \
+ /var/log/nginx/access.log
+
+# vim: set filetype=sh :
diff --git a/tests/old-accountd b/tests/old-accountd
new file mode 100644
index 0000000..b44f7ec
--- /dev/null
+++ b/tests/old-accountd
@@ -0,0 +1,30 @@
+# IPC test between lacme(8) and ancient lacme-accountd(1) 0.2 from Debian jessie
+
+adduser --disabled-password \
+ --home /home/lacme-account \
+ --gecos "lacme account user" \
+ --quiet lacme-account
+
+install -olacme-account -glacme-account -Ddm0700 -- ~lacme-account/.config/lacme
+chown lacme-account: /etc/lacme/account.key
+
+cat >~lacme-account/.config/lacme/lacme-accountd.conf <<-EOF
+ privkey = file:/etc/lacme/account.key
+EOF
+
+echo "deb http://deb.debian.org/debian stretch main" >>/etc/apt/sources.list
+DEBIAN_FRONTEND="noninteractive" apt update
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends \
+ --reinstall --allow-downgrades \
+ -oDPkg::Options::="--force-confdef" -oDPkg::Options::="--force-overwrite" \
+ lacme-accountd/stretch
+
+SOCKET=~lacme-account/S.lacme
+runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" & PID=$!
+lacme --socket="$SOCKET" account
+lacme --socket="$SOCKET" newOrder
+
+kill $PID
+wait
+
+# vim: set filetype=sh :
diff --git a/tests/old-lacme b/tests/old-lacme
new file mode 100644
index 0000000..fa7d827
--- /dev/null
+++ b/tests/old-lacme
@@ -0,0 +1,36 @@
+# IPC test between recent lacme-accountd(1) and ancient lacme(8) 0.5 from Debian buster
+# (we don't try earlier versions as we need v2 support of the ACME API)
+
+adduser --disabled-password \
+ --home /home/lacme-account \
+ --gecos "lacme account user" \
+ --quiet lacme-account
+
+install -olacme-account -glacme-account -Ddm0700 -- ~lacme-account/.config/lacme
+chown lacme-account: /etc/lacme/account.key
+mv -f /usr/share/lacme/ca-certificates.crt /usr/share/lacme/ca-certificates.crt.back
+
+cat >~lacme-account/.config/lacme/lacme-accountd.conf <<-EOF
+ privkey = file:/etc/lacme/account.key
+EOF
+
+echo "deb http://deb.debian.org/debian buster main" >>/etc/apt/sources.list
+DEBIAN_FRONTEND="noninteractive" apt update
+DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends \
+ --reinstall --allow-downgrades \
+ -oDPkg::Options::="--force-confdef" -oDPkg::Options::="--force-overwrite" \
+ lacme/buster
+
+# restore staging environment
+mv -f /usr/share/lacme/ca-certificates.crt.back /usr/share/lacme/ca-certificates.crt
+
+SOCKET=~lacme-account/S.lacme
+runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" & PID=$!
+sed -ri "s/^\[accountd]$/#&/" /etc/lacme/lacme.conf # https://bugs.debian.org/955767
+lacme --socket="$SOCKET" account
+lacme --socket="$SOCKET" newOrder
+
+kill $PID
+wait
+
+# vim: set filetype=sh :
diff --git a/tests/register b/tests/register
new file mode 100644
index 0000000..0273377
--- /dev/null
+++ b/tests/register
@@ -0,0 +1,8 @@
+# Register new account key (or update the contact/ToS URI)
+
+lacme account --tos-agreed --register "mailto:noreply+lacme=$$@guilhem.org"
+
+# should return info about the account
+lacme account
+
+# vim: set filetype=sh :
diff --git a/tests/spec-expansion b/tests/spec-expansion
new file mode 100644
index 0000000..a268637
--- /dev/null
+++ b/tests/spec-expansion
@@ -0,0 +1,130 @@
+# %-specifiers expansion
+
+# lacme --config=, all specifiers, root privileges
+! lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /var/cache /etc /run /root /tmp root 0 root 0 %.conf: No such file or directory"
+
+# lacme --config=, all specifiers, root privileges, defined XDG_*
+! env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /var/cache /etc /run /root /foo/tmp root 0 root 0 %.conf: No such file or directory"
+
+# lacme --config=, all specifiers, non-root, unset XDG_RUNTIME_DIR
+! runuser -u nobody -- lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Undefined expansion %t in \"%C %E %t %h %T %g %G %u %U %%.conf\""
+
+# lacme --config=, all specifiers, non-root, defined XDG_RUNTIME_DIR, no other XDG_*
+! runuser -u nobody -g www-data -- env XDG_RUNTIME_DIR=/foo/run \
+ lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /nonexistent/.cache /nonexistent/.config /foo/run /nonexistent /tmp www-data 33 nobody 65534 %.conf: No such file or directory"
+
+# lacme --config=, all specifiers, non-root, defined XDG_*
+! runuser -u nobody -- env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Can't open /foo/cache /foo/config /foo/run /nonexistent /foo/tmp nogroup 65534 nobody 65534 %.conf: No such file or directory"
+
+# lacme --socket=
+! lacme --config="%E/lacme/lacme.conf" --socket="%t/S.lacme2" account --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/lacme.conf"
+grepstderr -Fxq "Can't stat /run/S.lacme2: No such file or directory (Is lacme-accountd running?)"
+
+# 'challenge-directory' setting (expands before privilege drop)
+sed -ri 's|^#?challenge-directory\s*=.*|challenge-directory = /nonexistent/%u:%g|' /etc/lacme/lacme.conf
+! lacme newOrder --debug 2>"$STDERR" || fail
+grepstderr -Fq "Using existing webserver on /nonexistent/root:root"
+
+# lacme --config-certs= and 'config-certs' settings (expands before privilege drop)
+! lacme newOrder --debug nonexistent 2>"$STDERR" || fail
+grepstderr -Fxq "Reading /etc/lacme/lacme-certs.conf"
+
+sed -ri 's|^#?config-certs\s*=.*|config-certs = /nonexistent/%u:%g.conf|' /etc/lacme/lacme.conf
+! lacme newOrder --debug nonexistent 2>"$STDERR" || fail
+grepstderr -Fxq "Reading /nonexistent/root:root.conf"
+
+! lacme newOrder --config-certs="%E/lacme/certs.conf.d" --debug nonexistent 2>"$STDERR" || fail
+grepstderr -vFxq "Reading /etc/lacme/lacme-certs.conf"
+grepstderr -Fxq "Reading /etc/lacme/certs.conf.d"
+
+# 'config' setting in [accountd] section (expands after privilege drop)
+sed -ri 's|^#?config\s*=\s*$|config = /nonexistent/%u:%g.conf|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Failed to open file '/nonexistent/root:root.conf' for reading: No such file or directory"
+
+sed -ri 's|^#?user\s*=\s*$|user = nobody|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Failed to open file '/nonexistent/nobody:root.conf' for reading: No such file or directory"
+
+# 'command' setting in [accountd] section (expands after privilege drop)
+sed -ri 's|^#?command\s*=.*/lacme-accountd$|command = /usr/bin/lacme-accountd --%u|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Fxq "Unknown option: nobody"
+
+sed -ri 's|^#?command\s*=.*/lacme-accountd .*|command = /nonexistent/%u/%g %u %g|' /etc/lacme/lacme.conf
+! lacme account 2>"$STDERR" || fail
+grepstderr -Eq "^Can't exec \"/nonexistent/nobody/root\": No such file or directory"
+
+
+#######################################################################
+
+# lacme-accountd --config=, all specifiers, root privileges
+! lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Failed to open file '/var/cache /etc /run /root /tmp root 0 root 0 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --config=, all specifiers, root privileges, defined XDG_*
+! env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Failed to open file '/var/cache /etc /run /root /foo/tmp root 0 root 0 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --config=, all specifiers, non-root, unset XDG_RUNTIME_DIR
+! runuser -u nobody -- lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" account 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Undefined expansion %t in \"%C %E %t %h %T %g %G %u %U %%.conf\""
+
+# lacme-accountd --config=, all specifiers, non-root, defined XDG_RUNTIME_DIR, no other XDG_*
+! runuser -u nobody -g www-data -- env XDG_RUNTIME_DIR=/foo/run \
+ lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Failed to open file '/nonexistent/.cache /nonexistent/.config /foo/run /nonexistent /tmp www-data 33 nobody 65534 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --config=, all specifiers, non-root, defined XDG_*
+! runuser -u nobody -- env XDG_CACHE_HOME=/foo/cache XDG_CONFIG_HOME=/foo/config XDG_RUNTIME_DIR=/foo/run HOME=/foo/home USER=myuser TMPDIR=/foo/tmp \
+ lacme-accountd --config="%C %E %t %h %T %g %G %u %U %%.conf" 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Failed to open file '/foo/cache /foo/config /foo/run /nonexistent /foo/tmp nogroup 65534 nobody 65534 %.conf' for reading: No such file or directory"
+
+# lacme-accountd --privkey=
+! lacme-accountd --privkey="file:%h/lacme-accountd.key" --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Can't open /root/lacme-accountd.key: No such file or directory"
+
+# lacme-accountd, default socket location
+lacme-accountd --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /run/S.lacme"
+grepstderr -Fxq "Unlinking /run/S.lacme"
+
+# lacme-accountd --config= --socket= --privkey=
+ln -s lacme-accountd.conf /etc/lacme/accountd.conf
+lacme-accountd --config="%E/lacme/accountd.conf" --socket="%t/S.lacme2" --privkey="file:%E/lacme/account.key" --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /run/S.lacme2"
+grepstderr -Fxq "Unlinking /run/S.lacme2"
+
+# lacme-accountd, custom 'socket' setting
+sed -ri 's|^#?socket\s*=.*|socket = %t/S.lacme3|' /etc/lacme/lacme-accountd.conf
+lacme-accountd --debug 2>"$STDERR" & PID=$!
+sleep 1
+kill $PID || fail
+wait || fail
+grepstderr -Fxq "Using configuration file: /etc/lacme/lacme-accountd.conf"
+grepstderr -Fxq "Starting lacme Account Key Manager at /run/S.lacme3"
+grepstderr -Fxq "Unlinking /run/S.lacme3"
+
+# lacme-accountd, custom 'privkey' setting
+sed -ri 's|^privkey\s*=.*|privkey = file:%h/lacme-accountd.key|' /etc/lacme/lacme-accountd.conf
+! lacme-accountd --debug 2>"$STDERR" || fail
+grepstderr -Fxq "Error: Can't open /root/lacme-accountd.key: No such file or directory"
+
+# vim: set filetype=sh :
diff --git a/tests/webservers b/tests/webservers
new file mode 100644
index 0000000..0cadea5
--- /dev/null
+++ b/tests/webservers
@@ -0,0 +1,16 @@
+# Multiple webservers (each binding to a different address)
+
+sed -ri 's|^#?listen\s*=.*|listen = [::]:80 127.0.0.1:8080|' /etc/lacme/lacme.conf
+lacme --debug newOrder 2>"$STDERR" || fail newOrder
+test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key
+
+grepstderr -Fq "Forking ACME webserver bound to [::]:80, child PID "
+grepstderr -Fq "Forking ACME webserver bound to 127.0.0.1:8080, child PID "
+grepstderr -Fq "Forking lacme-accountd, child PID "
+grepstderr -Fq "Forking /usr/libexec/lacme/client, child PID "
+grepstderr -Fq "Shutting down lacme-accountd"
+grepstderr -Fq "Shutting down ACME webserver bound to [::]:80"
+grepstderr -Fq "Shutting down ACME webserver bound to 127.0.0.1:8080"
+grepstderr -Eq "Incoming connection( from \S+)?: GET /\.well-known/acme-challenge/\S+ HTTP/[0-9.]+$"
+
+# vim: set filetype=sh :