From 11d971bc07ceb4359565e6611ae03a0c0134d153 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 23:22:15 +0100 Subject: Add test suite against Let's Encrypt's staging environment. https://letsencrypt.org/docs/staging-environment/ --- tests/accountd | 64 ++++++++++++++++++ tests/apache2-proxy | 34 ++++++++++ tests/apache2-static | 47 ++++++++++++++ tests/cert-extensions | 91 ++++++++++++++++++++++++++ tests/cert-install | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++ tests/cert-renew | 21 ++++++ tests/cert-revoke | 32 +++++++++ tests/cert-verify | 43 ++++++++++++ tests/drop-privileges | 166 +++++++++++++++++++++++++++++++++++++++++++++++ tests/nginx-proxy | 35 ++++++++++ tests/nginx-static | 48 ++++++++++++++ tests/register | 8 +++ tests/webservers | 16 +++++ 13 files changed, 781 insertions(+) create mode 100644 tests/accountd create mode 100644 tests/apache2-proxy create mode 100644 tests/apache2-static create mode 100644 tests/cert-extensions create mode 100644 tests/cert-install create mode 100644 tests/cert-renew create mode 100644 tests/cert-revoke create mode 100644 tests/cert-verify create mode 100644 tests/drop-privileges create mode 100644 tests/nginx-proxy create mode 100644 tests/nginx-static create mode 100644 tests/register create mode 100644 tests/webservers (limited to 'tests') diff --git a/tests/accountd b/tests/accountd new file mode 100644 index 0000000..2f3985f --- /dev/null +++ b/tests/accountd @@ -0,0 +1,64 @@ +# 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 "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?)" + +####################################################################### + +install -olacme-account -glacme-account -Ddm0700 ~lacme-account/.config/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:/home/lacme-account/.config/lacme/account.key +EOF + +# non-existent parent directory +! runuser -u lacme-account -- lacme-accountd --socket="/nonexistent/S.lacme" 2>"$STDERR" || fail +grepstderr -Fxq "stat(/nonexistent): No such file or directory" + +# word-writable parent directory +! runuser -u lacme-account -- lacme-accountd --socket="/tmp/S.lacme" account 2>"$STDERR" || fail +grepstderr -Fxq "Error: insecure permissions on /tmp" + +# non-existent $XDG_RUNTIME_DIR +! runuser -u lacme-account -- env XDG_RUNTIME_DIR="/nonexistent" lacme-accountd 2>"$STDERR" || fail +grepstderr -Fxq "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" + +# spawn accountd +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" account 2>"$STDERR" || fail +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" + +# vim: set filetype=sh : diff --git a/tests/apache2-proxy b/tests/apache2-proxy new file mode 100644 index 0000000..5ae17ee --- /dev/null +++ b/tests/apache2-proxy @@ -0,0 +1,34 @@ +# 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 + +ln -fs /etc/lacme/apache2.conf /etc/apache2/conf-available/lacme.conf +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 "\$prefix/status" + pwd >"\$prefix/cwd" + stat -c "%U:%G %#a" . >"\$prefix/cwd-mod" + sort -z "\$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/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/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 : -- cgit v1.2.3 From 95534d55bd27ec9311a484ddc4e4a550191aa496 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 20 Feb 2021 20:15:20 +0100 Subject: Add tests for OpenSSL- and GnuPG-encrypted account keys. These tests are not interactive! --- test | 3 ++- tests/account-encrypted-gpg | 15 +++++++++++++++ tests/account-encrypted-openssl | 10 ++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/account-encrypted-gpg create mode 100644 tests/account-encrypted-openssl (limited to 'tests') diff --git a/test b/test index ffee0ec..50b7382 100755 --- a/test +++ b/test @@ -55,7 +55,8 @@ if [ $# -eq 0 ]; then # always start with registration, the account key might be new TESTS+=( "register" ) for t in tests/*; do - if [ "$t" != "tests/register" ] && [ -f "$t" ]; then + if [ "$t" != "tests/register" ] && [ "${t#tests/account-encrypted-}" = "$t" ] && [ -f "$t" ]; then + # skip registration and non-interactive tests TESTS+=( "${t#tests/}" ) fi done 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 " +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.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 : -- cgit v1.2.3 From 57afbf387cf812945ad14cd03570b0746fc4c865 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 12:22:37 +0100 Subject: Document spawning a remote lacme-accountd(1) instance. And add a test case for this. --- lacme-accountd.1.md | 6 ++++++ lacme.8.md | 18 ++++++++++++++++++ tests/accountd-remote | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 tests/accountd-remote (limited to 'tests') diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 5303418..9377e23 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -135,6 +135,10 @@ execute [`lacme`(8)] remotely: $ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:$XDG_RUNTIME_DIR/S.lacme user@example.org \ sudo lacme --socket=/path/to/remote.sock newOrder +Consult the [`lacme`(8) manual][`lacme`(8)] for a solution involving +connecting to `lacme-accountd` on a dedicated remote host. Doing so +enables automatic renewal via [`crontab`(5)] or [`systemd.timer`(5)]. + See also ======== @@ -147,3 +151,5 @@ See also [OpenSSH]: https://www.openssh.com/ [`ssh`(1)]: https://man.openbsd.org/ssh [`genpkey`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-genpkey.html +[`crontab`(5)]: https://linux.die.net/man/5/crontab +[`systemd.timer`(5)]: https://www.freedesktop.org/software/systemd/man/systemd.timer.html diff --git a/lacme.8.md b/lacme.8.md index 3852b13..6218d36 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -435,6 +435,21 @@ Examples $ sudo lacme newOrder $ sudo lacme revokeCert /path/to/service.crt +Automatic renewal can be scheduled via [`crontab`(5)] or +[`systemd.timer`(5)]. In order to avoid deploying a single account key +onto multiple nodes and/or dealing with multiple account keys, one can +install a single [`lacme-accountd`(1)] instance on a dedicated host, +generate a single account key there (and keep it well), and set the +following in the [`[accountd]` section](#accountd-section): + + command = ssh -T lacme@account.example.net lacme-accountd + +If the user running `lacme` can connect to `lacme@account.example.net` +using (passwordless) key authentication, this setting will spawn a +remote [`lacme-accountd`(1)] and use it to sign [ACME] requests. +Further hardening can be achieved my means of [`authorized_keys`(5)] +restrictions. + See also ======== @@ -448,3 +463,6 @@ See also [`x509v3_config`(5ssl)]: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html [`genpkey`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-genpkey.html [`req`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-req.html +[`crontab`(5)]: https://linux.die.net/man/5/crontab +[`systemd.timer`(5)]: https://www.freedesktop.org/software/systemd/man/systemd.timer.html +[`authorized_keys`(5)]: https://man.openbsd.org/sshd.8#AUTHORIZED_KEYS_FILE_FORMAT diff --git a/tests/accountd-remote b/tests/accountd-remote new file mode 100644 index 0000000..71bb8e2 --- /dev/null +++ b/tests/accountd-remote @@ -0,0 +1,33 @@ +# 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 + +# vim: set filetype=sh : -- cgit v1.2.3 From 8d7b50989d1c446b81c73e8ababfce6f0351ee59 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 16:11:24 +0100 Subject: Symlink $(sysconfdir)/apache2/conf-available/lacme.conf → ../../lacme/apache2.conf. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is useful for enabling the snippet with `a2enconf lacme`, cf. https://bugs.debian.org/955859 . --- Makefile | 3 +++ tests/apache2-proxy | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/Makefile b/Makefile index 0fdd9df..3a8e461 100644 --- a/Makefile +++ b/Makefile @@ -87,11 +87,14 @@ install: all install -m0644 -vDt $(man8dir) -- $(BUILDDIR)/lacme.8 install -m0755 -vDt $(sbindir) -- $(BUILDDIR)/lacme install -m0755 -vDt $(bindir) -- $(BUILDDIR)/lacme-accountd + install -m0755 -vdD -- $(sysconfdir)/apache2/conf-available + ln -sv -- ../../lacme/apache2.conf $(sysconfdir)/apache2/conf-available/lacme.conf uninstall: rm -vf -- $(bindir)/lacme-accountd $(sbindir)/lacme rm -vf -- $(man1dir)/lacme-accountd.1 $(man8dir)/lacme.8 rm -rvf -- $(sysconfdir)/lacme $(datadir)/lacme $(libexecdir)/lacme + rm -vf -- $(sysconfdir)/apache2/conf-available/lacme.conf clean: rm -rvf -- $(BUILDDIR) diff --git a/tests/apache2-proxy b/tests/apache2-proxy index 5ae17ee..016b426 100644 --- a/tests/apache2-proxy +++ b/tests/apache2-proxy @@ -6,7 +6,6 @@ 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.conf /etc/apache2/conf-available/lacme.conf a2enmod proxy_http a2enconf lacme -- cgit v1.2.3 From 0ef94d85e58497dcb2c4c954cadcac918032467a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 21:07:01 +0100 Subject: Add %-specifiers support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lacme(8): for --config=, --socket=, --config-certs= (and ‘socket’/ ‘config-certs’/‘challenge-directory’ configuration options *before* privilege drop; and for the [accountd] section ‘command’/‘config’ configuration options *after* privilege drop). lacme-accountd(1): for --config=, --socket= and --privkey= (and ‘socket’/‘privkey’ configuration options). This also changes the default configuration file location. lacme(8) and lacme-accountd(1) now respectively use /etc/lacme/lacme.conf resp. /etc/lacme/lacme-accountd.conf when running as root, and $XDG_CONFIG_HOME/lacme/lacme.conf resp. $XDG_CONFIG_HOME/lacme/lacme-accountd.conf when running as a normal user. There is no fallback to /etc anymore. --- Changelog | 23 ++++++-- INSTALL | 1 - Makefile | 1 + config/lacme-accountd.conf | 4 +- config/lacme.conf | 4 +- lacme | 39 +++++++++++--- lacme-accountd | 35 +++++++++--- lacme-accountd.1.md | 67 ++++++++++++++++++----- lacme.8.md | 90 +++++++++++++++++++++++-------- tests/accountd | 13 ++++- tests/spec-expansion | 130 +++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 341 insertions(+), 66 deletions(-) create mode 100644 tests/spec-expansion (limited to 'tests') diff --git a/Changelog b/Changelog index 8952ba6..966b0b0 100644 --- a/Changelog +++ b/Changelog @@ -19,9 +19,16 @@ lacme (0.7.1) upstream; validate provided X.509 chains using that self-contained bundle, regardless of which CAs is marqued as trusted under /etc/ssl/certs. This change bumps the minimum OpenSSL version to 1.1.0. - * Breaking change: lacme(8) resp. lacme-accountd(1) no longer consider - ./lacme.conf resp. ./lacme-accountd.conf as default location for the - configuration file. + * Breaking change: lacme(8) and lacme-accountd(1) respectively load + their configuration file from /etc/lacme/lacme.conf resp. + /etc/lacme/lacme-accountd.conf when running as root, and + $XDG_CONFIG_HOME/lacme/lacme.conf resp. + $XDG_CONFIG_HOME/lacme/lacme-accountd.conf when running as a normal + user. There is no fallback to /etc anymore, and the lookup in the + current directory as prefered choice is removed too. However + lacme-accountd(1) can be used without configuration file under + ~/.config/lacme as it treats a non-existent default location as an + empty file. * The client, webserver, and accountd commands are now split on whitespace. This doesn't change the default behavior but allows using `ssh -T lacme@account.example.net lacme-accountd` to spawn a @@ -30,11 +37,17 @@ lacme (0.7.1) upstream; https://letsencrypt.org/docs/staging-environment/ . * lacme(8)'s 'config' option in the [accountd] section no longer have a default value. The previous default /etc/lacme/lacme-accountd.conf - is still honored when there is the user running lacme doesn't have a - ~/.config/lacme/lacme-account.conf configuration file. + is still honored when root privileges are preserved (the default). * Deprecate setting 'privkey' in [accountd] section of the lacme(8) configuration file. One need to use the lacme-accountd(1) configuration file for that instead. + * lacme(8): add %-specifiers support for --config=, --socket=, + --config-certs= (and 'socket'/'config-certs'/'challenge-directory' + configuration options *before* privilege drop; and for the [accountd] + section 'command'/'config' configuration options *after* privilege + drop). + * lacme-accountd(1): add %-specifiers support for --config=, --socket= + and --privkey= (and 'socket'/'privkey' configuration options). + Improve nginx/apache2 snippets for direct serving of challenge files (with the new 'challenge-directory' logic symlinks can be disabled). + Split Nginx and Apapche2 static configuration snippets into seperate diff --git a/INSTALL b/INSTALL index 85bd0c2..092ef16 100644 --- a/INSTALL +++ b/INSTALL @@ -7,7 +7,6 @@ lacme-accountd depends on the following Perl modules: - File::Basename (core module) - Getopt::Long (core module) - JSON (optionally C/XS-accelerated with JSON::XS) - - List::Util (core module) - MIME::Base64 (core module) - Socket (core module) diff --git a/Makefile b/Makefile index 2ac524b..a4caff0 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,7 @@ $(BUILDDIR)/%: % s#@@sbindir@@#$(sbindir)#g; \ s#@@libexecdir@@#$(libexecdir)#g; \ s#@@datadir@@#$(datadir)#g; \ + s#@@localstatedir@@#$(localstatedir)#g; \ s#@@runstatedir@@#$(runstatedir)#g; \ s#@@sysconfdir@@#$(sysconfdir)#g; \ s#@@lacme_www_user@@#$(lacme_www_user)#g; \ diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf index 10f332e..f31cf67 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -17,10 +17,8 @@ # for signature requests from the ACME client. An error is raised if # the path exists or if its parent directory is writable by other # users. -# Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR -# environment variable is set. # -#socket = /run/user/1000/S.lacme +#socket = %t/S.lacme # Be quiet. Possible values: "Yes"/"No". # diff --git a/config/lacme.conf b/config/lacme.conf index 98ecacb..198729d 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -10,13 +10,11 @@ # UNIX-domain socket to connect to for signature requests from the ACME # client. lacme(8) aborts if the socket is readable or writable by # other users, or if its parent directory is writable by other users. -# Default: "$XDG_RUNTIME_DIR/S.lacme" if the XDG_RUNTIME_DIR environment -# variable is set. # This setting is ignored when lacme-accountd(1) is spawned by lacme(8), # since the two processes communicate through a socket pair. See the # "accountd" section below for details. # -#socket = +#socket = %t/S.lacme # username to drop privileges to (setting both effective and real uid). # Skip privilege drop if the value is empty (not recommended). diff --git a/lacme b/lacme index 9a62cbb..ad7e1d8 100755 --- a/lacme +++ b/lacme @@ -75,13 +75,33 @@ $COMMAND = $COMMAND =~ /\A(account|newOrder|new-cert|revokeCert|revoke-cert)\z/ : usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND @ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV +sub env_fallback($$) { + my $v = $ENV{ shift() }; + return (defined $v and $v ne "") ? $v : shift; +} +sub spec_expand($) { + my $str = shift; + $str =~ s#%(.)# my $x = + $1 eq "C" ? ($< == 0 ? "@@localstatedir@@/cache" : env_fallback(XDG_CACHE_HOME => "$ENV{HOME}/.cache")) + : $1 eq "E" ? ($< == 0 ? "@@sysconfdir@@" : env_fallback(XDG_CONFIG_HOME => "$ENV{HOME}/.config")) + : $1 eq "g" ? (getgrgid((split /\s/,$()[0]))[0] + : $1 eq "G" ? $( =~ s/\s.*//r + : $1 eq "h" ? (getpwuid($<))[7] + : $1 eq "u" ? (getpwuid($<))[0] + : $1 eq "U" ? $< + : $1 eq "t" ? ($< == 0 ? "@@runstatedir@@" : $ENV{XDG_RUNTIME_DIR}) + : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") + : $1 eq "%" ? "%" + : die "Error: \"$str\" has unknown specifier %$1\n"; + die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x; + $x; + #ge; + return $str; +} + sub set_FD_CLOEXEC($$); -my $CONFFILENAME = $OPTS{config} // first { -f $_ } - ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" - , "@@sysconfdir@@/lacme/$NAME.conf" - ); +my $CONFFILENAME = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf"); do { - die "Error: Can't find configuration file\n" unless defined $CONFFILENAME; print STDERR "Using configuration file: $CONFFILENAME\n" if $OPTS{debug}; open $CONFFILE, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n"; my $conf = do { local $/ = undef; <$CONFFILE> }; @@ -93,7 +113,7 @@ do { my $accountd = defined $OPTS{socket} ? 0 : exists $h->{accountd} ? 1 : 0; my %valid = ( client => { - socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), + socket => '%t/S.lacme', user => '@@lacme_client_user@@', group => '@@lacme_client_group@@', command => '@@libexecdir@@/lacme/client', @@ -285,6 +305,7 @@ sub spawn_webserver() { # Use existing HTTPd to serve challenge files using 'challenge-directory' # as document root if (defined (my $dir = $conf->{'challenge-directory'})) { + $dir = spec_expand($dir); print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; # lacme(8) doesn't have the list of challenge files to delete on # cleanup -- instead, we unlink all files and fails at @@ -513,8 +534,9 @@ sub acme_client($@) { set_FD_CLOEXEC($s, 1); $ENV{GPG_TTY} = $GPG_TTY if defined $GPG_TTY; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; + $_ = spec_expand($_) foreach ($cmd, @args); # expand %-specifiers after privilege drop and whitespace split push @args, '--stdio'; - push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; + push @args, '--config='.spec_expand($accountd->{config}) if $accountd->{config} ne ''; push @args, '--privkey='.$accountd->{privkey} if $accountd->{privkey} ne ''; # XXX deprecated in 0.8.0 push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; push @args, '--debug' if $OPTS{debug}; @@ -531,7 +553,7 @@ sub acme_client($@) { } else { my @stat; - my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; + my $sockname = spec_expand($OPTS{socket} // $conf->{socket}); $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname # ensure we're the only user with write access to the parent dir @@ -697,6 +719,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'} : defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ] : [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ]; + $_ = spec_expand($_) foreach @$conffiles; my ($conf, %defaults); foreach my $conffile (@$conffiles) { $conffile = dirname($CONFFILENAME) .'/'. $conffile unless $conffile =~ /\A\//; diff --git a/lacme-accountd b/lacme-accountd index b9a6e33..e170637 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -30,7 +30,6 @@ my $NAME = 'lacme-accountd'; use Errno 'EINTR'; use File::Basename 'dirname'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; -use List::Util 'first'; use MIME::Base64 'encode_base64url'; use Socket qw/PF_UNIX SOCK_STREAM SHUT_RDWR/; @@ -64,11 +63,32 @@ sub usage(;$$) { usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s stdio quiet|q debug help|h/); usage(0) if $OPTS{help}; +sub env_fallback($$) { + my $v = $ENV{ shift() }; + return (defined $v and $v ne "") ? $v : shift; +} +sub spec_expand($) { + my $str = shift; + $str =~ s#%(.)# my $x = + $1 eq "C" ? ($< == 0 ? "@@localstatedir@@/cache" : env_fallback(XDG_CACHE_HOME => "$ENV{HOME}/.cache")) + : $1 eq "E" ? ($< == 0 ? "@@sysconfdir@@" : env_fallback(XDG_CONFIG_HOME => "$ENV{HOME}/.config")) + : $1 eq "g" ? (getgrgid((split /\s/,$()[0]))[0] + : $1 eq "G" ? $( =~ s/\s.*//r + : $1 eq "h" ? (getpwuid($<))[7] + : $1 eq "u" ? (getpwuid($<))[0] + : $1 eq "U" ? $< + : $1 eq "t" ? ($< == 0 ? "@@runstatedir@@" : $ENV{XDG_RUNTIME_DIR}) + : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") + : $1 eq "%" ? "%" + : die "Error: \"$str\" has unknown specifier %$1\n"; + die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x; + $x; + #ge; + return $str; +} + do { - my $conffile = $OPTS{config} // first { -f $_ } - ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" - , "@@sysconfdir@@/lacme/$NAME.conf" - ); + my $conffile = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf"); if (defined $OPTS{config} or -e $conffile) { print STDERR "Using configuration file: $conffile\n" if $OPTS{debug}; @@ -94,7 +114,7 @@ do { # my ($JWK, $SIGN); if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { - my ($method, $filename) = ($1,$2); + my ($method, $filename) = ($1, spec_expand($2)); my ($fh, @command); if ($method eq 'file') { # generate with `openssl genpkey -algorithm RSA` @@ -142,8 +162,7 @@ my $JWK_STR = JSON::->new->encode($JWK); # delete the file manually. # unless (defined $OPTS{stdio}) { - my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef); - die "Missing socket option\n" unless defined $sockname; + my $sockname = spec_expand($OPTS{socket} // '%t/S.lacme'); $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname # ensure we're the only user with write access to the parent dir diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index cd6352c..4c494f2 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -41,9 +41,12 @@ Options `--config=`*filename* -: Use *filename* as configuration file. `lacme-accountd` fails when - `--config=` is used with a non-existent file, but a non-existent - default location is treated as if it were an empty file. +: Use *filename* as configuration file instead of + `%E/lacme/lacme-accountd.conf`. The value is subject to + [%-specifier expansion](#percent-specifiers). `lacme-accountd` + fails when `--config=` is used with a non-existent file, but a + non-existent default location is treated as if it were an empty + file. See the **[configuration file](#configuration-file)** section below for the configuration options. @@ -57,6 +60,8 @@ Options symmetrically encrypted) * `gpg:`*FILE*, for a [`gpg`(1)]-encrypted private key + *FILE* is subject to [%-specifier expansion](#percent-specifiers). + The [`genpkey`(1ssl)] command can be used to generate a new private (account) key: @@ -67,9 +72,14 @@ Options `--socket=`*path* -: Use *path* as the UNIX-domain socket to bind against for signature - requests from the [ACME] client. `lacme-accountd` aborts if *path* - exists or if its parent directory is writable by other users. +: Use *path* as the UNIX-domain socket to bind to for signature + requests from the [ACME] client. The value is subject to + [%-specifier expansion](#percent-specifiers). `lacme-accountd` + aborts if *path* exists or if its parent directory is writable by + other users. + Default: `%t/S.lacme` (omitting `--socket=` therefore yields an + error when `lacme-accountd` doesn't run as and the `XDG_RUNTIME_DIR` + environment variable is unset or empty). `-h`, `--help` @@ -86,12 +96,6 @@ Options Configuration file ================== -If `--config=` is not given, `lacme-accountd` uses the first existing -configuration file among *$XDG_CONFIG_HOME/lacme/lacme-accountd.conf* -(or *~/.config/lacme/lacme-accountd.conf* if the `XDG_CONFIG_HOME` -environment variable is not set), and -*@@sysconfdir@@/lacme/lacme-accountd.conf*. - When given on the command line, the `--privkey=`, `--socket=` and `--quiet` options take precedence over their counterpart (without leading `--`) in the configuration file. Valid settings are: @@ -110,13 +114,48 @@ leading `--`) in the configuration file. Valid settings are: *socket* : See `--socket=`. - Default: *$XDG_RUNTIME_DIR/S.lacme* if the `XDG_RUNTIME_DIR` - environment variable is set. *quiet* : Be quiet. Possible values: `Yes`/`No`. +%-specifiers {#percent-specifiers} +============ + +The value the `--config=`, `--privkey=` and `--socket=` CLI options (and +*privkey* and *socket* configuration options) are subject to %-expansion +for the following specifiers. + +---- ------------------------------------------------------------------ +`%C` `@@localstatedir@@/cache` for the root user, and `$XDG_CACHE_HOME` + for other users (or `$HOME/.cache` if the `XDG_CACHE_HOME` + environment variable is unset or empty). + +`%E` `@@sysconfdir@@` for the root user, and `$XDG_CONFIG_HOME` for + other users (or `$HOME/.config` if the `XDG_CONFIG_HOME` + environment variable is unset or empty). + +`%g` Current group name. + +`%G` Current group ID. + +`%h` Home directory of the current user. + +`%t` `@@runstatedir@@` for the root user, and `$XDG_RUNTIME_DIR` for + other users. Non-root users may only use `%t` when the + `XDG_RUNTIME_DIR` environment variable is set to a non-empty + value. + +`%T` `$TMPDIR`, or `/tmp` if the `TMPDIR` environment variable is unset + or empty. + +`%u` Current user name. + +`%U` Current user ID. + +`%%` A literal `%`. +---- ------------------------------------------------------------------ + Examples ======== diff --git a/lacme.8.md b/lacme.8.md index 4dfc67e..aab448f 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -100,16 +100,22 @@ Generic settings `--config=`*filename* -: Use *filename* as configuration file. See the **[configuration - file](#configuration-file)** section below for the configuration - options. +: Use *filename* as configuration file instead of + `%E/lacme/lacme.conf`. The value is subject to [%-specifier + expansion](#percent-specifiers). + + See the **[configuration file](#configuration-file)** section below + for the configuration options. `--socket=`*path* : Use *path* as the [`lacme-accountd`(1)] UNIX-domain socket to - connect to for signature requests from the [ACME] client. `lacme` - aborts if `path` is readable or writable by other users, or if its - parent directory is writable by other users. + connect to for signature requests from the [ACME] client. The value + is subject to [%-specifier expansion](#percent-specifiers). + `lacme` aborts if *path* exists or if its parent directory is + writable by other users. + Default: `%t/S.lacme`. + This command-line option overrides the *socket* setting of the [`[client]` section](#client-section) of the configuration file; it also causes the [`[accountd]` section](#accountd-section) to be @@ -130,10 +136,6 @@ Generic settings Configuration file ================== -If `--config=` is not given, `lacme` uses the first existing -configuration file among *$XDG_CONFIG_HOME/lacme/lacme.conf* (or -*~/.config/lacme/lacme.conf* if the `XDG_CONFIG_HOME` environment -variable is not set), and *@@sysconfdir@@/lacme/lacme.conf*. Valid settings are: Default section @@ -145,13 +147,15 @@ Default section space-separated list of certificate configuration files or directories to use (see the **[certificate configuration file](#certificate-configuration-file)** section below for the - configuration options). + configuration options). Each item in that list is independently + subject to [%-specifier expansion](#percent-specifiers). - Paths not starting with `/` are relative to the directory name of - the **[configuration filename](#configuration-file)**. The list of - files and directories is processed in order, with the later items - taking precedence. Files in a directory are processed in - lexicographic order, only considering the ones with suffix `.conf`. + Paths not starting with `/` (after %-expansion) are relative to the + parent directory of the **[configuration filename](#configuration-file)**. + The list of files and directories is processed in the specified + order, with the later items taking precedence. Files in a directory + are processed in lexicographic order, only considering the ones with + suffix `.conf`. Default: `lacme-certs.conf lacme-certs.conf.d/`. @@ -164,8 +168,6 @@ of [ACME] commands and dialogues with the remote [ACME] server). *socket* : See `--socket=`. - Default: *$XDG_RUNTIME_DIR/S.lacme* if the `XDG_RUNTIME_DIR` - environment variable is set. *user* @@ -247,7 +249,9 @@ served during certificate issuance. lacme client user (by default `@@lacme_client_user@@`) needs to be able to create files under it. - This setting is required when *listen* is empty. + This setting is required when *listen* is empty. Moreover its value + is subject to [%-specifier expansion](#percent-specifiers) _before_ + privilege drop. *user* @@ -308,13 +312,18 @@ UNIX-domain socket. the first item being the command to execute, the second its first argument etc. (Note that `lacme` appends more arguments when executing the command internally.) + Each item in that list is independently subject to [%-specifier + expansion](#percent-specifiers) _after_ privilege drop. + Default: `@@bindir@@/lacme-accountd`. + Use for instance `ssh -T lacme@account.example.net lacme-accountd` - in order to spawn a remote [`lacme-accountd`(1)] server. Default: - `@@bindir@@/lacme-accountd`. + in order to spawn a remote [`lacme-accountd`(1)] server. *config* -: Path to the [`lacme-accountd`(1)] configuration file. +: Path to the [`lacme-accountd`(1)] configuration file. The value is + subject to [%-specifier expansion](#percent-specifiers) _after_ + privilege drop. *quiet* @@ -428,6 +437,43 @@ Valid settings are: after successful installation of the *certificate* and/or *certificate-chain*. +%-specifiers {#percent-specifiers} +============ + +Some CLI options and configuration settings are subject to %-expansion +for the following specifiers. Check the documentation of each setting +to see which ones are affected. + +---- ------------------------------------------------------------------ +`%C` `@@localstatedir@@/cache` for the root user, and `$XDG_CACHE_HOME` + for other users (or `$HOME/.cache` if the `XDG_CACHE_HOME` + environment variable is unset or empty). + +`%E` `@@sysconfdir@@` for the root user, and `$XDG_CONFIG_HOME` for + other users (or `$HOME/.config` if the `XDG_CONFIG_HOME` + environment variable is unset or empty). + +`%g` Current group name. + +`%G` Current group ID. + +`%h` Home directory of the current user. + +`%t` `@@runstatedir@@` for the root user, and `$XDG_RUNTIME_DIR` for + other users. Non-root users may only use `%t` when the + `XDG_RUNTIME_DIR` environment variable is set to a non-empty + value. + +`%T` `$TMPDIR`, or `/tmp` if the `TMPDIR` environment variable is unset + or empty. + +`%u` Current user name. + +`%U` Current user ID. + +`%%` A literal `%`. +---- ------------------------------------------------------------------ + Examples ======== diff --git a/tests/accountd b/tests/accountd index 2f3985f..4626c78 100644 --- a/tests/accountd +++ b/tests/accountd @@ -20,12 +20,17 @@ grepstderr -Fxq "Can't stat $SOCKET: No such file or directory (Is lacme-account ####################################################################### +# 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 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:/home/lacme-account/.config/lacme/account.key + privkey = file:%E/lacme/account.key EOF # non-existent parent directory @@ -33,9 +38,13 @@ EOF grepstderr -Fxq "stat(/nonexistent): No such file or directory" # word-writable parent directory -! runuser -u lacme-account -- lacme-accountd --socket="/tmp/S.lacme" account 2>"$STDERR" || fail +! 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 "stat(/nonexistent): No such file or directory" diff --git a/tests/spec-expansion b/tests/spec-expansion new file mode 100644 index 0000000..722bdfc --- /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 "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 "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 "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 "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 "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 "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 : -- cgit v1.2.3 From 8e612e071b8c0fc99ebf91673f53ca5f0d6bdd11 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 20 Feb 2021 19:56:15 +0100 Subject: Document `lacme-accountd --stdio`. It's an internal flag, but can be useful for authorized_keys(5) restrictions. --- Changelog | 3 ++- lacme-accountd.1.md | 8 ++++++++ lacme.8.md | 6 ++++-- tests/accountd-remote | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/Changelog b/Changelog index 966b0b0..baf67b9 100644 --- a/Changelog +++ b/Changelog @@ -62,7 +62,8 @@ lacme (0.7.1) upstream; accountd. + accountd: replace internal option --conn-fd=FD with flag --stdio. Using stdin/stdout makes it possible to tunnel the accountd - connection through ssh. + connection through ssh. The new flag is documented to allow safe + usage is authorized_keys(5) restrictions. + Remove dependency on List::Util (core module). - lacme: delay webserver socket shutdown to after the process has terminated. diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 4c494f2..476a150 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -81,6 +81,14 @@ Options error when `lacme-accountd` doesn't run as and the `XDG_RUNTIME_DIR` environment variable is unset or empty). +`--stdio` + +: Read signature requests from the standard input and write signatures + to the standard output, instead of using a UNIX-domain socket for + communication with the [ACME] client. + This _internal_ flag should never be used by standalone + `lacme-accountd` instances, only for those [`lacme`(8)] spawns. + `-h`, `--help` : Display a brief help and exit. diff --git a/lacme.8.md b/lacme.8.md index aab448f..9a14d75 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -493,8 +493,10 @@ following in the [`[accountd]` section](#accountd-section): If the user running `lacme` can connect to `lacme@account.example.net` using (passwordless) key authentication, this setting will spawn a remote [`lacme-accountd`(1)] and use it to sign [ACME] requests. -Further hardening can be achieved my means of [`authorized_keys`(5)] -restrictions. +Further hardening can be achieved by means of [`authorized_keys`(5)] +restrictions: + + restrict,from="…",command="/usr/bin/lacme-accountd --stdio" ssh-rsa … See also ======== diff --git a/tests/accountd-remote b/tests/accountd-remote index 71bb8e2..bd5d99f 100644 --- a/tests/accountd-remote +++ b/tests/accountd-remote @@ -30,4 +30,21 @@ sed -ri 's|^#?config\s*=.*|config = /etc/lacme/lacme-accountd.conf|' /etc/lacme/ 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 --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 +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 +EOF + +lacme newOrder || fail +test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key + # vim: set filetype=sh : -- cgit v1.2.3 From 74c0a11722cf1e01b9a9834e89a07b55eaf01080 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 20 Feb 2021 22:05:18 +0100 Subject: lacme-accountd: new setting 'logfile' to log signature requests. Prefixed with a timestamp. --- Changelog | 2 + lacme | 6 +-- lacme-accountd | 110 +++++++++++++++++++++++++++++++------------------- lacme-accountd.1.md | 9 ++++- lacme.8.md | 2 +- tests/accountd | 27 +++++++++---- tests/accountd-remote | 11 +++-- tests/spec-expansion | 12 +++--- 8 files changed, 116 insertions(+), 63 deletions(-) (limited to 'tests') diff --git a/Changelog b/Changelog index c69d0d0..e6becda 100644 --- a/Changelog +++ b/Changelog @@ -50,6 +50,8 @@ lacme (0.7.1) upstream; and --privkey= (and 'socket'/'privkey' configuration options). * lacme-accountd(1): base64url-decode incoming signature requests shown in messages to the standard error. + * lacme-accountd(1): new setting 'logfile' to log (decoded) incoming + signature requests to a file. + Improve nginx/apache2 snippets for direct serving of challenge files (with the new 'challenge-directory' logic symlinks can be disabled). + Split Nginx and Apapche2 static configuration snippets into seperate diff --git a/lacme b/lacme index ad7e1d8..88ab78d 100755 --- a/lacme +++ b/lacme @@ -558,12 +558,12 @@ sub acme_client($@) { # ensure we're the only user with write access to the parent dir my $dirname = dirname($sockname); - @stat = stat($dirname) or die "stat($dirname): $!\n"; - die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + @stat = stat($dirname) or die "Error: stat($dirname): $!\n"; + die "Error: Insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; # ensure we're the only user with read/write access to the socket @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n"; - die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; + die "Error: Insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; # connect(2) to the socket socket($client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; diff --git a/lacme-accountd b/lacme-accountd index 0adfe38..c8c6d5e 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -63,6 +63,30 @@ sub usage(;$$) { usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s stdio quiet|q debug help|h/); usage(0) if $OPTS{help}; +my $LOG; +sub logmsg($@) { + my $lvl = shift // "all"; + if (defined $LOG) { + my $now = localtime; + $LOG->printflush("[", $now, "] ", @_, "\n") or warn "print: $!"; + } + unless (($lvl eq "debug" and !$OPTS{debug}) or ($lvl eq "noquiet" and $OPTS{quiet})) { + print STDERR @_, "\n" or warn "print: $!"; + } +} +sub info(@) { logmsg(all => @_); } +sub error(@) { + my @msg = ("Error: ", @_); + info(@msg); + die(@msg, "\n"); +} +sub panic(@) { + my @loc = caller; + my @msg = (@_, " at line $loc[2] in $loc[1]"); + info(@msg); + die(@msg, "\n"); +} + sub env_fallback($$) { my $v = $ENV{ shift() }; return (defined $v and $v ne "") ? $v : shift; @@ -80,8 +104,8 @@ sub spec_expand($) { : $1 eq "t" ? ($< == 0 ? "@@runstatedir@@" : $ENV{XDG_RUNTIME_DIR}) : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") : $1 eq "%" ? "%" - : die "Error: \"$str\" has unknown specifier %$1\n"; - die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x; + : error("\"$str\" has unknown specifier %$1"); + error("undefined expansion %$1 in \"$str\"") unless defined $x; $x; #ge; return $str; @@ -92,11 +116,16 @@ do { if (defined $OPTS{config} or -e $conffile) { print STDERR "Using configuration file: $conffile\n" if $OPTS{debug}; - my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n"; + my $h = Config::Tiny::->read($conffile) or error(Config::Tiny::->errstr()); my $h2 = delete $h->{_} // {}; - die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; - my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket quiet/; - die "Unknown option(s): ".join(', ', keys %$h2)."\n" if %$h2; + if (defined (my $logfile = $h2->{logfile})) { + $logfile = spec_expand($logfile); + die "Invalid log file name\n" unless $logfile =~ /\A(\p{Print}+)\z/; # untaint + open $LOG, ">>", $1 or die "Can't open $1: $!"; + } + error("Invalid section(s): ".join(', ', keys %$h)) if %$h; + my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket logfile quiet/; + error("Unknown option(s): ".join(', ', keys %$h2)) if %$h2; $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; } else { @@ -104,7 +133,7 @@ do { } $OPTS{quiet} = 0 if $OPTS{debug}; - die "Error: 'privkey' is not specified\n" unless defined $OPTS{privkey}; + error("'privkey' is not specified") unless defined $OPTS{privkey}; }; @@ -118,27 +147,27 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { my ($fh, @command); if ($method eq 'file') { # generate with `openssl genpkey -algorithm RSA` - open $fh, '<', $filename or die "Error: Can't open $filename: $!\n"; + open $fh, '<', $filename or error("Can't open $filename: $!"); } elsif ($method eq 'gpg') { @command = split /\s+/, ($OPTS{gpg} // 'gpg --quiet'); - open $fh, '-|', @command, qw/-o - --decrypt --/, $filename or die "fork: $!"; + open $fh, '-|', @command, qw/-o - --decrypt --/, $filename or panic("fork: $!"); } else { - die; # impossible + panic(); # impossible } my $str = do {local $/ = undef; <$fh>}; - close $fh or die $! ? - "close: $!" : - "Error: $command[0] exited with value ".($? >> 8)."\n"; + close $fh or ($! or !@command) ? + panic("close: $!") : + error("$command[0] exited with value ".($? >> 8)); require 'Crypt/OpenSSL/RSA.pm'; my $rsa = Crypt::OpenSSL::RSA->new_private_key($str); undef $str; - die "Error: $filename: Not a private key\n" unless $rsa->is_private(); - die "Error: $filename: Invalid key\n" unless $rsa->check_key(); + error("$filename: Not a private key") unless $rsa->is_private(); + error("$filename: Invalid key") unless $rsa->check_key(); $rsa->use_sha256_hash(); require 'Crypt/OpenSSL/Bignum.pm'; @@ -149,7 +178,7 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { $SIGN = sub($) { $rsa->sign($_[0]) }; } else { - die "Error: unsupported method: $OPTS{privkey}\n"; + error("unsupported method: $OPTS{privkey}"); } my $JWK_STR = JSON::->new->encode($JWK); @@ -163,24 +192,24 @@ my $JWK_STR = JSON::->new->encode($JWK); # unless (defined $OPTS{stdio}) { my $sockname = spec_expand($OPTS{socket} // '%t/S.lacme'); - $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname + $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : error("Invalid socket name"); # untaint # ensure we're the only user with write access to the parent dir my $dirname = dirname($sockname); - my @stat = stat($dirname) or die "stat($dirname): $!\n"; - die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + my @stat = stat($dirname) or error("stat($dirname): $!"); + error("Insecure permissions on $dirname") if ($stat[2] & 0022) != 0; - my $umask = umask(0177) // die "umask: $!"; + my $umask = umask(0177) // panic("umask: $!"); - print STDERR "Starting lacme Account Key Manager at $sockname\n" unless $OPTS{quiet}; - socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; - my $sockaddr = Socket::sockaddr_un($sockname) // die; - bind($sock, $sockaddr) or die "bind: $!"; + logmsg(noquiet => "Starting lacme Account Key Manager at $sockname"); + socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or panic("socket: $!"); + my $sockaddr = Socket::sockaddr_un($sockname) // panic(); + bind($sock, $sockaddr) or panic("bind: $!"); ($SOCKNAME, $S) = ($sockname, $sock); - listen($S, 1) or die "listen: $!"; + listen($S, 1) or panic("listen: $!"); - umask($umask) // die "umask: $!"; + umask($umask) // panic("umask: $!"); }; @@ -194,23 +223,22 @@ sub conn($$;$) { # sign whatever comes in while (defined (my $data = $in->getline())) { - $data =~ s/\r\n\z// or die; + $data =~ s/\r\n\z// or panic(); my ($protected, $payload) = split(/\./, $data, 2); unless (defined $protected and $protected =~ /\A[A-Za-z0-9\-_]+\z/) { - print STDERR "[$id] >>> Error: Malformed protected data, refusing to sign!\n"; + info("[$id] >>> Error: Malformed protected data, refusing to sign!"); last; } unless (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) { # payload can be empty, for instance for POST-as-GET - print STDERR "[$id] >>> Error: Malformed payload data, refusing to sign!\n"; + info("[$id] >>> Error: Malformed payload data, refusing to sign!"); last; } - print STDERR "[$id] >>> Incoming signature request for ", - "base64url(", decode_base64url($protected), ") . ", - "base64url(", decode_base64url($payload), ")" - unless $OPTS{quiet}; + logmsg(noquiet => "[$id] >>> Incoming signature request for ", + "base64url(", decode_base64url($protected), ") . ", + "base64url(", decode_base64url($payload), ")"); my $sig = $SIGN->($data); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; @@ -224,11 +252,11 @@ if (defined $OPTS{stdio}) { for (my $count = 0;; $count++) { accept(my $conn, $S) or do { next if $! == EINTR; # try again if accept(2) was interrupted by a signal - die "accept: $!"; + panic("accept: $!"); }; - print STDERR "[$count] >>> Accepted new connection\n" unless $OPTS{quiet}; + logmsg(noquiet => "[$count] >>> Accepted new connection"); conn($conn, $conn, $count); - print STDERR "[$count] >>> Connection terminated\n" unless $OPTS{quiet}; + logmsg(noquiet => "[$count] >>> Connection terminated"); $conn->close() or warn "close: $!"; } } @@ -238,12 +266,12 @@ if (defined $OPTS{stdio}) { # END { if (defined $SOCKNAME and -S $SOCKNAME) { - print STDERR "Unlinking $SOCKNAME\n" if $OPTS{debug}; - unlink $SOCKNAME or print STDERR "Couldn't unlink $SOCKNAME: $!\n"; + logmsg(debug => "Unlinking $SOCKNAME"); + unlink $SOCKNAME or info("Error: unlink($SOCKNAME): $!"); } if (defined $S) { - print STDERR "Shutting down and closing lacme Account Key Manager\n" unless $OPTS{quiet}; - shutdown($S, SHUT_RDWR) or warn "shutdown: $!"; - close $S or print STDERR "close: $!\n"; + logmsg(noquiet => "Shutting down and closing lacme Account Key Manager"); + shutdown($S, SHUT_RDWR) or info("Error: shutdown: $!"); + close $S or info("Error: close: $!"); } } diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 476a150..66ef222 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -119,6 +119,11 @@ leading `--`) in the configuration file. Valid settings are: [`gpg`(1)] to use, as well as some default options. Default: `gpg --quiet`. +*logfile* + +: An optional file where to log to. The value is subject to + [%-specifier expansion](#percent-specifiers). + *socket* : See `--socket=`. @@ -131,8 +136,8 @@ leading `--`) in the configuration file. Valid settings are: ============ The value the `--config=`, `--privkey=` and `--socket=` CLI options (and -*privkey* and *socket* configuration options) are subject to %-expansion -for the following specifiers. +*privkey*, *socket* and *logfile* settings in the configuration file) +are subject to %-expansion for the following specifiers. ---- ------------------------------------------------------------------ `%C` `@@localstatedir@@/cache` for the root user, and `$XDG_CACHE_HOME` diff --git a/lacme.8.md b/lacme.8.md index ca47470..7d66e79 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -497,7 +497,7 @@ remote [`lacme-accountd`(1)] and use it to sign [ACME] requests. Further hardening can be achieved by means of [`authorized_keys`(5)] restrictions: - restrict,from="…",command="/usr/bin/lacme-accountd --stdio" ssh-rsa … + restrict,from="…",command="/usr/bin/lacme-accountd --quiet --stdio" ssh-rsa … See also ======== diff --git a/tests/accountd b/tests/accountd index 4626c78..2798465 100644 --- a/tests/accountd +++ b/tests/accountd @@ -7,11 +7,11 @@ adduser --disabled-password \ # non-existent parent directory ! lacme --socket="/nonexistent/S.lacme" account 2>"$STDERR" || fail -grepstderr -Fxq "stat(/nonexistent): No such file or directory" +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" +grepstderr -Fxq "Error: Insecure permissions on /tmp" # missing socket SOCKET=~lacme-account/S.lacme @@ -25,21 +25,23 @@ grepstderr -Fxq "Can't stat $SOCKET: No such file or directory (Is lacme-account 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 +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 "stat(/nonexistent): No such file or directory" +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" +grepstderr -Fxq "Error: Insecure permissions on /tmp" # unset XDG_RUNTIME_DIR ! runuser -u lacme-account -- lacme-accountd 2>"$STDERR" || fail @@ -47,7 +49,7 @@ 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 "stat(/nonexistent): No such file or directory" +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=$! @@ -57,7 +59,11 @@ 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" -# spawn accountd +# 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 @@ -70,4 +76,11 @@ 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 ">>> Incoming signature request for " ~lacme-account/.local/share/lacme/accountd.log + # vim: set filetype=sh : diff --git a/tests/accountd-remote b/tests/accountd-remote index bd5d99f..05850c2 100644 --- a/tests/accountd-remote +++ b/tests/accountd-remote @@ -31,20 +31,25 @@ lacme newOrder --debug 2>"$STDERR" || fail # intentionally use --debug, ssh shou 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 --stdio\" &|" ~lacme-account/.ssh/authorized_keys +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 +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 || fail +lacme newOrder test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key +# ensure signature requests are logged +grep -F ">>> Incoming signature request for " ~lacme-account/.local/share/lacme/accountd.log + # vim: set filetype=sh : diff --git a/tests/spec-expansion b/tests/spec-expansion index 722bdfc..273fa51 100644 --- a/tests/spec-expansion +++ b/tests/spec-expansion @@ -48,11 +48,11 @@ 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 "Failed to open file '/nonexistent/root:root.conf' for reading: No such file or directory" +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 "Failed to open file '/nonexistent/nobody:root.conf' for reading: No such file or directory" +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 @@ -68,12 +68,12 @@ grepstderr -Eq "^Can't exec \"/nonexistent/nobody/root\": No such file or direct # 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 "Failed to open file '/var/cache /etc /run /root /tmp root 0 root 0 %.conf' for reading: No such file or directory" +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 "Failed to open file '/var/cache /etc /run /root /foo/tmp root 0 root 0 %.conf' for reading: No such file or directory" +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 @@ -82,12 +82,12 @@ grepstderr -Fxq "Error: undefined expansion %t in \"%C %E %t %h %T %g %G %u %U % # 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 "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" +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 "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" +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 -- cgit v1.2.3 From d56b957dbae6c8214d50ce88d0ea04eb4654b843 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 17:34:51 +0100 Subject: wording --- lacme | 2 +- lacme-accountd | 4 ++-- lacme.8.md | 2 +- tests/accountd | 2 +- tests/spec-expansion | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/lacme b/lacme index 88ab78d..1e51c6c 100755 --- a/lacme +++ b/lacme @@ -93,7 +93,7 @@ sub spec_expand($) { : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") : $1 eq "%" ? "%" : die "Error: \"$str\" has unknown specifier %$1\n"; - die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x; + die "Error: Undefined expansion %$1 in \"$str\"\n" unless defined $x; $x; #ge; return $str; diff --git a/lacme-accountd b/lacme-accountd index 6a6e90d..0f0b0d9 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -105,7 +105,7 @@ sub spec_expand($) { : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") : $1 eq "%" ? "%" : error("\"$str\" has unknown specifier %$1"); - error("undefined expansion %$1 in \"$str\"") unless defined $x; + error("Undefined expansion %$1 in \"$str\"") unless defined $x; $x; #ge; return $str; @@ -178,7 +178,7 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { $SIGN = sub($) { $rsa->sign($_[0]) }; } else { - error("unsupported method: $OPTS{privkey}"); + error("Unsupported method: $OPTS{privkey}"); } my $JWK_STR = JSON::->new->encode($JWK); diff --git a/lacme.8.md b/lacme.8.md index 30de221..ad6dab6 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -92,7 +92,7 @@ Commands : Request that the given certificate(s) *FILE*(s) be revoked. For this command, [`lacme-accountd`(1)] can be pointed to either the - account key or the server's private key. + account key or the certificate key. Command alias: `revoke-cert`. Generic settings diff --git a/tests/accountd b/tests/accountd index 2798465..c82a05d 100644 --- a/tests/accountd +++ b/tests/accountd @@ -45,7 +45,7 @@ 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\"" +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 diff --git a/tests/spec-expansion b/tests/spec-expansion index 273fa51..a268637 100644 --- a/tests/spec-expansion +++ b/tests/spec-expansion @@ -11,7 +11,7 @@ grepstderr -Fxq "Can't open /var/cache /etc /run /root /foo/tmp root 0 root 0 %. # 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\"" +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 \ @@ -77,7 +77,7 @@ grepstderr -Fxq "Error: Failed to open file '/var/cache /etc /run /root /foo/tmp # 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\"" +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 \ -- cgit v1.2.3 From 16f7d75ac8e46a7905779931c871ac85c7e1aa04 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 18:52:08 +0100 Subject: Add IPC tests with an old lacme(8) resp. lacme-accountd(1). --- tests/old-accountd | 29 +++++++++++++++++++++++++++++ tests/old-lacme | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/old-accountd create mode 100644 tests/old-lacme (limited to 'tests') diff --git a/tests/old-accountd b/tests/old-accountd new file mode 100644 index 0000000..e7ccc0a --- /dev/null +++ b/tests/old-accountd @@ -0,0 +1,29 @@ +# 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 \ + -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..16a647d --- /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 \ + -oDPkg::Options::="--force-confdef" -oDPkg::Options::="--force-overwrite" \ + lacme/buster + +# restore staging environment +sed -ri '0,\|^#?server\s*=.*| {s||server = https://acme-staging-v02.api.letsencrypt.org/directory|}' /etc/lacme/lacme.conf +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 : -- cgit v1.2.3 From 1bdaeae835b5c9914f9c2107efda150d643cda12 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 19:54:25 +0100 Subject: accountd: Improve log message for incoming requests. --- lacme-accountd | 14 +++++++------- tests/accountd | 2 +- tests/accountd-remote | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) (limited to 'tests') diff --git a/lacme-accountd b/lacme-accountd index d4521f9..d8c96b0 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -233,20 +233,20 @@ sub conn($$$) { while (defined (my $data = $in->getline())) { $data =~ s/\r\n\z// or panic(); - my ($protected, $payload) = split(/\./, $data, 2); - unless (defined $protected and $protected =~ /\A[A-Za-z0-9\-_]+\z/) { - info("[$id] >>> Error: Malformed protected data, refusing to sign!"); + my ($header, $payload) = split(/\./, $data, 2); + unless (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) { + info("[$id] >>> Error: Refusing to sign request: Malformed protected header"); last; } unless (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) { # POST-as-GET yields an empty payload - info("[$id] >>> Error: Malformed payload data, refusing to sign!"); + info("[$id] >>> Error: Refusing to sign request: Malformed payload"); last; } - logmsg(noquiet => "[$id] >>> Incoming signature request for ", - "base64url(", decode_base64url($protected), ") . ", - "base64url(", decode_base64url($payload), ")"); + logmsg(noquiet => "[$id] >>> OK signing request: ", + "header=base64url(", decode_base64url($header), "); ", + "playload=base64url(", decode_base64url($payload), ")"); my $sig = $SIGN->($data); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; diff --git a/tests/accountd b/tests/accountd index c82a05d..9acb33f 100644 --- a/tests/accountd +++ b/tests/accountd @@ -81,6 +81,6 @@ grep -Fq "Starting lacme Account Key Manager at /home/lacme-account/S.lacme" ~la 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 ">>> Incoming signature request for " ~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-remote b/tests/accountd-remote index 05850c2..9e7f812 100644 --- a/tests/accountd-remote +++ b/tests/accountd-remote @@ -50,6 +50,6 @@ lacme newOrder test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.key # ensure signature requests are logged -grep -F ">>> Incoming signature request for " ~lacme-account/.local/share/lacme/accountd.log +grep -F ">>> OK signing request:" ~lacme-account/.local/share/lacme/accountd.log # vim: set filetype=sh : -- cgit v1.2.3 From 9898b1877ce2973bbc336921969bd7f16d3698fa Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 18:49:14 +0100 Subject: lacme-accountd(1): new setting 'keyid'. This saves a round trip and provides a safeguard against malicious clients. --- Changelog | 2 ++ client | 56 +++++++++++++++++++++++++++++++------------ config/lacme-accountd.conf | 6 +++++ lacme-accountd | 7 +++++- lacme-accountd.1.md | 18 ++++++++++++-- tests/accountd-kid | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 130 insertions(+), 18 deletions(-) create mode 100644 tests/accountd-kid (limited to 'tests') diff --git a/Changelog b/Changelog index ffd9536..9c606fe 100644 --- a/Changelog +++ b/Changelog @@ -52,6 +52,8 @@ lacme (0.7.1) upstream; in messages to the standard error. * lacme-accountd(1): new setting 'logfile' to log (decoded) incoming signature requests to a file. + * lacme-accountd(1): new setting 'keyid' to easily revoke all account + management access from the client. + Improve nginx/apache2 snippets for direct serving of challenge files (with the new 'challenge-directory' logic symlinks can be disabled). + Split Nginx and Apapche2 static configuration snippets into seperate diff --git a/client b/client index 7a63259..d7874b7 100755 --- a/client +++ b/client @@ -90,7 +90,7 @@ do { print STDERR "WARN: Ignoring extra greeting data from accountd \"$extra\"\n"; } else { print STDERR "Received extra greeting data from accountd: $extra\n" if $ENV{DEBUG}; - ($JWK_thumbprint, $ALG) = @$h{qw/jwk-thumbprint alg/}; + ($JWK_thumbprint, $ALG, $KID) = @$h{qw/jwk-thumbprint alg kid/}; } } my $jwk_str = $S->getline() // die "ERROR: No JWK from lacme-accountd\n"; @@ -194,39 +194,50 @@ sub request_json_decode($;$$) { ############################################################################# -# JSON-encode the hash reference $h and send it to the ACME server $uri -# encapsulated it in a JSON Web Signature (JWS). +# JSON-encode the hash reference $payload and send it to the ACME server +# $url encapsulated it in a JSON Web Signature (JWS). $header MUST +# contain either "jwk" (JSON Web Key) or "kid" per RFC 8555 sec. 6.2 # https://tools.ietf.org/html/rfc8555 # -sub acme($;$) { - my ($uri, $h) = @_; - die "Missing nonce\n" unless defined $NONCE; +sub acme2($$;$) { + my ($url, $header, $payload) = @_; # Produce the JSON Web Signature: RFC 7515 section 5 - my %header = ( alg => $ALG, nonce => $NONCE, url => $uri ); - defined $KID ? ($header{kid} = $KID) : ($header{jwk} = $JWK); - my $payload = defined $h ? encode_base64url(json()->encode($h)) : ""; - my $protected = encode_base64url(json()->encode(\%header)); - my $data = $protected .'.'. $payload; - $S->printflush($data, "\r\n"); + $header->{alg} = $ALG; + $header->{nonce} = $NONCE // die "Missing nonce\n"; + $header->{url} = $url; + my $protected = encode_base64url(json()->encode($header)); + $payload = defined $payload ? encode_base64url(json()->encode($payload)) : ""; + + $S->printflush($protected, ".", $payload, "\r\n"); my $sig = $S->getline() // die "ERROR: No response from lacme-accountd\n"; $sig =~ s/\r\n\z// or die; undef $NONCE; # consume the nonce # Flattened JSON Serialization, RFC 7515 section 7.2.2 - request(POST => $uri, { + request(POST => $url, { payload => $payload, protected => $protected, signature => $sig }); } +# Like above, but always use "kid" +sub acme($;$) { + my ($url, $payload) = @_; + die "Missing KID\n" unless defined $KID; + acme2($url, {kid => $KID}, $payload) +} + my $SERVER_URI = $CONFIG->{server} // '@@acmeapi_server@@'; my %RES; # Get the resource URI from the directory sub acme_resource($%) { my $r = shift; + my %payload = @_; + my %protected; + unless (%RES) { # query the ACME directory to get resources URIs %RES = %{ request_json_decode(request(GET => $SERVER_URI)) }; @@ -235,12 +246,23 @@ sub acme_resource($%) { request(HEAD => $RES{newNonce}); } my $uri = $RES{$r} // die "Unknown resource '$r'\n"; - acme($uri, {@_}); + + if ($r eq "newAccount" or ($r eq "revokeCert" and !defined $KID)) { + # per RFC 8555 sec. 6.2 these requests MUST have a JWK + print STDERR "WARNING: lacme-accountd supplied an empty JWK; try removing 'keyid' ", + "setting from lacme-accountd.conf if the ACME resource request fails.\n" + unless %$JWK; + return acme2($uri, {jwk => $JWK}, \%payload); + } else { + # per RFC 8555 sec. 6.2 all other requests MUST have a KID + return acme($uri, \%payload); + } } # Set the key ID (registration URI) sub set_kid(;$) { my $die = shift // 1; + return if defined $KID; # already set my $r = acme_resource('newAccount', onlyReturnExisting => JSON::true ); if ($r->is_success()) { $KID = $r->header('Location'); @@ -269,6 +291,7 @@ if ($COMMAND eq 'account') { if ($r->is_success()) { $KID = $r->header('Location'); + print STDERR "Key ID: $KID\n"; $r = acme($KID, \%h); request_json_decode($r, 1, \*STDOUT) if $r->is_success() and $r->content_type() eq 'application/json'; @@ -391,7 +414,10 @@ elsif ($COMMAND eq 'revokeCert') { my $der = do { local $/ = undef; }; close STDIN or die "close: $!"; - # send a KID if the request is signed with the acccount key, otherwise send a JWK + # RFC 8555 sec. 6.2: send a KID if the request is signed with the + # acccount key, otherwise send a JWK + # We have no way to know which of the account key or certificate key + # is used, so we try to get a KID and fallback to sending the JWK set_kid(0); my $r = acme_resource('revokeCert', certificate => encode_base64url($der)); diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf index f31cf67..d31c6c8 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -20,6 +20,12 @@ # #socket = %t/S.lacme +# The "Key ID", as shown by `acme account`, to give the ACME client. +# A non-empty value revokes all account management access (status +# change, contact address updates etc.) from the client. +# +#keyid = + # Be quiet. Possible values: "Yes"/"No". # #quiet = Yes diff --git a/lacme-accountd b/lacme-accountd index d8c96b0..a842bce 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -125,7 +125,7 @@ do { open $LOG, ">>", $1 or die "Can't open $1: $!"; } error("Invalid section(s): ".join(', ', keys %$h)) if %$h; - my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket logfile quiet/; + my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket logfile keyid quiet/; error("Unknown option(s): ".join(', ', keys %$h2)) if %$h2; $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; @@ -183,6 +183,11 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { # use of SHA-256 digest in the thumbprint is hardcoded, see RFC 8555 sec. 8.1 $JWK_STR = JSON::->new->utf8->canonical->encode(\%jwk); $extra_greeting{"jwk-thumbprint"} = encode_base64url(sha256($JWK_STR)); + + if ((my $kid = $OPTS{keyid} // "") ne "") { + $extra_greeting{kid} = $kid; + $JWK_STR = "{}"; + } $EXTRA_GREETING_STR = JSON::->new->encode(\%extra_greeting); } else { diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index d0b2c6b..4933a78 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -119,14 +119,28 @@ leading `--`) in the configuration file. Valid settings are: [`gpg`(1)] to use, as well as some default options. Default: `gpg --quiet`. +*socket* + +: See `--socket=`. + *logfile* : An optional file where to log to. The value is subject to [%-specifier expansion](#percent-specifiers). -*socket* +*keyid* -: See `--socket=`. +: The "Key ID", as shown by `` `acme account` ``, to give the [ACME] + client. With an empty *keyid* (the default) the client forwards the + JSON Web Key (JWK) to the [ACME] server to retrieve the correct + value. A non-empty value therefore saves a round-trip. + + A non-empty value also causes `lacme-accountd` to send an empty JWK, + thereby revoking all account management access (status change, + contact address updates etc.) from the client: any `` `acme account` `` + command (or any command from [`lacme`(8)] before version 0.8.0) is + bound to be rejected by the [ACME] server. This provides a + safeguard against malicious clients. *quiet* 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 : -- cgit v1.2.3 From 9a6a7824a009e1fb1f97afd227181191bc10f0a6 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 02:43:48 +0100 Subject: Fix `./test --deb`. The staging environment wasn't set properly for the Debian packages. --- test | 30 ++++++++++++++++++------------ tests/old-accountd | 3 ++- tests/old-lacme | 4 ++-- 3 files changed, 22 insertions(+), 15 deletions(-) (limited to 'tests') diff --git a/test b/test index dc80751..81d910c 100755 --- a/test +++ b/test @@ -75,21 +75,10 @@ if [ "$MODE" = "deb" ]; then DISTRIBUTION="$(dpkg-parsechangelog -S Distribution)" [ "$DISTRIBUTION" != "UNRELEASED" ] || DISTRIBUTION="sid" PKG_DESTDIR="${XDG_CACHE_HOME:-"$HOME/.cache"}/build-area" -elif [ "$MODE" = "dev" ]; then - make all -- \ - BUILDDIR="$BUILDDIR" \ - DESTDIR="" \ - exec_prefix="/usr" \ - datadir="/usr/share" \ - runstatedir="/run" \ - lacme_www_user=_lacme-www \ - lacme_www_group=nogroup \ - lacme_client_user=_lacme-client \ - lacme_client_group=nogroup \ - acmeapi_server="https://acme-staging-v02.api.letsencrypt.org/directory" fi ACCOUNT_KEY="$BUILDDIR/account.key" +mkdir -pv -- "$BUILDDIR" if [ ! -f "$ACCOUNT_KEY" ]; then # keep the account key (up to `make clean`) to avoid hitting # rate-liming -- currently 50 registrations per 3h per IP, see @@ -115,6 +104,20 @@ run() { exit 1 fi + # Don't need to rebuild for each test, but editing the code at the + # same time might cause `make install` to rebuild a wrong version + make all -- \ + BUILDDIR="$BUILDDIR" \ + DESTDIR="" \ + exec_prefix="/usr" \ + datadir="/usr/share" \ + runstatedir="/run" \ + lacme_www_user=_lacme-www \ + lacme_www_group=nogroup \ + lacme_client_user=_lacme-client \ + lacme_client_group=nogroup \ + acmeapi_server="https://acme-staging-v02.api.letsencrypt.org/directory" + CHROOT="$(schroot -c "$DISTRIBUTION-$ARCH-sbuild" -b)" rootdir="/run/schroot/mount/$CHROOT" @@ -163,6 +166,9 @@ run() { sudo install -oroot -groot -m0644 -vt "$rootdir/usr/share/lacme" certs-staging/*.pem sudo install -oroot -groot -m0644 -vT "$BUILDDIR/certs-staging/ca-certificates.crt" \ "$rootdir/usr/share/lacme/ca-certificates.crt" + sudo schroot -d"/" -c "$CHROOT" -r -- \ + sed -ri '0,/^#?server\s*=.*/ {s||server = https://acme-staging-v02.api.letsencrypt.org/directory|}' \ + /etc/lacme/lacme.conf # install account key and configure lacme accordingly sudo install -oroot -groot -m0600 -vT -- "$BUILDDIR/account.key" \ diff --git a/tests/old-accountd b/tests/old-accountd index e7ccc0a..b44f7ec 100644 --- a/tests/old-accountd +++ b/tests/old-accountd @@ -14,7 +14,8 @@ 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 \ +DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends \ + --reinstall --allow-downgrades \ -oDPkg::Options::="--force-confdef" -oDPkg::Options::="--force-overwrite" \ lacme-accountd/stretch diff --git a/tests/old-lacme b/tests/old-lacme index 16a647d..fa7d827 100644 --- a/tests/old-lacme +++ b/tests/old-lacme @@ -16,12 +16,12 @@ 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 \ +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 -sed -ri '0,\|^#?server\s*=.*| {s||server = https://acme-staging-v02.api.letsencrypt.org/directory|}' /etc/lacme/lacme.conf mv -f /usr/share/lacme/ca-certificates.crt.back /usr/share/lacme/ca-certificates.crt SOCKET=~lacme-account/S.lacme -- cgit v1.2.3 From 903309cb8364047a2939fb6d3cb06da8ecc32726 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 03:19:38 +0100 Subject: tests: Check presence of extra greeting data. --- tests/accountd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/accountd b/tests/accountd index 9acb33f..a603c16 100644 --- a/tests/accountd +++ b/tests/accountd @@ -67,7 +67,8 @@ 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" account 2>"$STDERR" || fail +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 -- cgit v1.2.3