From 647d28bf9b8da2ce47a888aad71ab5264eea6c6d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 18:28:03 +0100 Subject: lacme: delay webserver socket shutdown. To after the process has terminated. This solves a race condition spewing accept: Invalid argument at /usr/libexec/lacme/webserver line 80. (harmless) errors. Closes: deb#970458 --- Changelog | 7 +++++++ lacme | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 13db236..a565440 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,10 @@ +lacme (0.7.1) upstream; + + - lacme: delay webserver socket shutdown to after the process has + terminated. + + -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 + lacme (0.7) upstream; * Breaking change: the certificate indicated by 'CAfile' is no longer diff --git a/lacme b/lacme index 07ebb45..088e393 100755 --- a/lacme +++ b/lacme @@ -346,9 +346,9 @@ sub spawn_webserver() { set_FD_CLOEXEC($sock, 1); push @CLEANUP, sub() { print STDERR "[$$] Shutting down ACME webserver bound to $p\n" if $OPTS{debug}; - shutdown($sock, SHUT_RDWR) or warn "shutdown: $!"; kill 15 => $pid; waitpid $pid => 0; + shutdown($sock, SHUT_RDWR) or warn "shutdown: $!"; }; # on dual-stack ipv4/ipv6, we'll need to open the port for the -- cgit v1.2.3 From c93443364ce23ced97a80bfda8f8bb35ec19fcdb Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 19:02:44 +0100 Subject: documentation: suggest to generate private key material with genpkey(1ssl). * Also suggest a command to generate an ECDSA key not just RSA. * Hint at which key algorithms are supported. --- Changelog | 3 +++ lacme-accountd | 2 +- lacme-accountd.1.md | 10 +++++++--- lacme.8.md | 26 ++++++++++++++++++++++---- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/Changelog b/Changelog index a565440..39df738 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,9 @@ lacme (0.7.1) upstream; - lacme: delay webserver socket shutdown to after the process has terminated. + - documentation: suggest to generate private key material with + genpkey(1ssl); also suggest a command to generate an ECDSA key not + just RSA; hint at which key algorithms are supported. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/lacme-accountd b/lacme-accountd index af64168..deccfa2 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -94,7 +94,7 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { my ($method, $filename) = ($1,$2); my ($fh, @command); if ($method eq 'file') { - # generate with `openssl genrsa 4096 | install --mode=0600 /dev/stdin /tmp/privkey` + # generate with `openssl genpkey -algorithm RSA` open $fh, '<', $filename or die "Error: Can't open $filename: $!\n"; } elsif ($method eq 'gpg') { diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index a967b67..24e73eb 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -55,10 +55,13 @@ Options * `gpg:`*FILE*, to specify a [`gpg`(1)]-encrypted private key (in PEM format). - The following command can be used to generate a new 4096-bits RSA - key in PEM format with mode 0600: + The [`genpkey`(1ssl)] command can be used to generate a new private + (account) key: - openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/account.key + $ install -vm0600 /dev/null /path/to/account.key + $ openssl genpkey -algorithm RSA -out /path/to/account.key + + Currently `lacme-accountd` only supports RSA account keys. `--socket=`*path* @@ -141,3 +144,4 @@ See also [`gpg`(1)]: https://www.gnupg.org/documentation/manpage.en.html [OpenSSH]: https://www.openssh.com/ [`ssh`(1)]: https://man.openbsd.org/ssh +[`genpkey`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-genpkey.html diff --git a/lacme.8.md b/lacme.8.md index 4098662..c1bea54 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -332,11 +332,28 @@ Valid options are: *certificate-key* -: Path the service's private key. This option is required. The - following command can be used to generate a new 4096-bits RSA key in - PEM format with mode 0600: +: Path to the service's private key. This option is required. The + [`genpkey`(1ssl)] command can be used to generate a new service RSA + key: - openssl genrsa 4096 | install -m0600 /dev/stdin /path/to/srv.key + $ install -vm0600 /dev/null /path/to/service.rsa.key + $ openssl genpkey -algorithm RSA -out /path/to/service.rsa.key + + Alternatively, for an ECDSA key using the NIST P-256 curve: + + $ install -vm0600 /dev/null /path/to/service.ecdsa.key + $ openssl genpkey -algorithm EC -out /path/to/service.ecdsa.key \ + -pkeyopt ec_paramgen_curve:P-256 \ + -pkeyopt ec_param_enc:named_curve + + `lacme` supports any key algorithm than the underlying libssl + (OpenSSL) version is able to manipulate, but the [ACME] server might + reject CSRs associated with private keys of deprecated and/or + “exotic” algorithms. + + For a dual cert setup (for instance RSA+ECDSA), duplicate the + certificate section and use a distinct *certificate-key* resp. + *certificate* (or *certificate-chain*) value for each key algorithm. *min-days* @@ -407,3 +424,4 @@ See also [`iptables`(8)]: https://linux.die.net/man/8/iptables [`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html [`x509v3_config`(5ssl)]: https://www.openssl.org/docs/manmaster/apps/x509v3_config.html +[`genpkey`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-genpkey.html -- cgit v1.2.3 From f4b7c7b6130722535cb87c123d11ba554e7806c7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 19:20:13 +0100 Subject: wibble --- lacme-accountd.1.md | 6 +++--- lacme.8.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 24e73eb..359a6d1 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -120,17 +120,17 @@ Examples Run `lacme-accountd` in a first terminal: - ~$ lacme-accountd --privkey=file:/path/to/account.key --socket=$XDG_RUNTIME_DIR/S.lacme + $ lacme-accountd --privkey=file:/path/to/account.key --socket=$XDG_RUNTIME_DIR/S.lacme Then, while `lacme-accountd` is running, execute locally [`lacme`(8)] in another terminal: - ~$ sudo lacme --socket=$XDG_RUNTIME_DIR/S.lacme newOrder + $ sudo lacme --socket=$XDG_RUNTIME_DIR/S.lacme newOrder Alternatively, use [OpenSSH] 6.7 or later to forward the socket and execute [`lacme`(8)] remotely: - ~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:$XDG_RUNTIME_DIR/S.lacme user@example.org \ + $ 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 See also diff --git a/lacme.8.md b/lacme.8.md index c1bea54..d98ec8e 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -410,9 +410,9 @@ Valid options are: Examples ======== - ~$ sudo lacme account --register --tos-agreed mailto:noreply@example.com - ~$ sudo lacme newOrder - ~$ sudo lacme revokeCert /path/to/server/certificate.pem + $ sudo lacme account --register --tos-agreed mailto:noreply@example.com + $ sudo lacme newOrder + $ sudo lacme revokeCert /path/to/service.crt See also ======== -- cgit v1.2.3 From e8980fb172221cbffd7fa672d65da0a806524e72 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 19:36:06 +0100 Subject: documentation: clarify that "file:/path/to/account.key" can point to a symmetrically-encrypted private key. --- Changelog | 2 ++ config/lacme-accountd.conf | 4 ++-- lacme-accountd.1.md | 11 +++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Changelog b/Changelog index 39df738..a2cd0bb 100644 --- a/Changelog +++ b/Changelog @@ -5,6 +5,8 @@ lacme (0.7.1) upstream; - documentation: suggest to generate private key material with genpkey(1ssl); also suggest a command to generate an ECDSA key not just RSA; hint at which key algorithms are supported. + - documentation: clarify that "file:/path/to/account.key" can point to + a symmetrically-encrypted private key. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf index 94d2556..7248eb5 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -1,8 +1,8 @@ # The value of "privkey" specifies the (private) account key to use # for signing requests. Currently supported values are: # -# - file:FILE, to specify an encrypted private key (in PEM format) -# - gpg:FILE, to specify a gpg-encrypted private key (in PEM format) +# - file:FILE, for a private key in PEM format (optionally encrypted) +# - gpg:FILE, for a gpg-encrypted private key # #privkey = gpg:/path/to/encrypted/account.key.gpg #privkey = file:/path/to/account.key diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 359a6d1..560cfac 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -45,15 +45,14 @@ Options file](#configuration-file)** section below for the configuration options. -`--privkey=`*arg* +`--privkey=`*value* : Specify the (private) account key to use for signing requests. - Currently supported *arg*uments are: + Currently supported *value*s are: - * `file:`*FILE*, to specify an encrypted private key (in PEM - format); and - * `gpg:`*FILE*, to specify a [`gpg`(1)]-encrypted private key (in - PEM format). + * `file:`*FILE*, for a private key in PEM format (optionally + symmetrically encrypted) + * `gpg:`*FILE*, for a [`gpg`(1)]-encrypted private key The [`genpkey`(1ssl)] command can be used to generate a new private (account) key: -- cgit v1.2.3 From 79edb6eea5e009e5b49876728f7477a8524e98ec Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 19:51:56 +0100 Subject: documentation: emphasize default values in the config file. Also, move the most common options ('hash', 'keyUsage', 'CAfile', 'min-days') to the default section. --- Changelog | 3 +++ config/lacme-certs.conf | 32 ++++++++++++++++++-------------- lacme.8.md | 14 +++++++++----- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/Changelog b/Changelog index a2cd0bb..f94e81e 100644 --- a/Changelog +++ b/Changelog @@ -7,6 +7,9 @@ lacme (0.7.1) upstream; just RSA; hint at which key algorithms are supported. - documentation: clarify that "file:/path/to/account.key" can point to a symmetrically-encrypted private key. + - documentation: emphasize default values in the config file, and move + the most common options ('hash', 'keyUsage', 'CAfile', 'min-days') to + the default section. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf index 232c85b..3e7a577 100644 --- a/config/lacme-certs.conf +++ b/config/lacme-certs.conf @@ -1,13 +1,27 @@ # Each non-default section refer to separate certificate issuance # requests. Options in the default section apply to each sections. -# Message digest to sign the Certificate Signing Request with. +# Message digest to sign the Certificate Signing Request with, +# overriding the req(1ssl) default. # -#hash = sha512 +#hash = -# Comma-separated list of Key Usages, see x509v3_config(5ssl). +# Comma-separated list of Key Usages, for instance "digitalSignature, +# keyEncipherment", to include in the Certificate Signing Request. +# See x509v3_config(5ssl) for a list of possible values. # -#keyUsage = digitalSignature, keyEncipherment +#keyUsage = + +# Path to the bundle of trusted issuer certificates. This is used for +# validating each certificate after issuance or renewal. Specifying an +# empty value skips certificate validation. +# +#CAfile = @@datadir@@/lacme/ca-certificates.crt + +# For an existing certificate, the minimum number of days before its +# expiration date the section is considered for re-issuance. +# +#min-days = 21 #[www] @@ -25,16 +39,6 @@ # #certificate-chain = /etc/nginx/ssl/srv.chain.pem -# For an existing certificate, the minimum number of days before its -# expiration date the section is considered for re-issuance. -# -#min-days = 21 - -# Path to trusted issuer certificates, used for validating each issued -# certificate. Specifying an empty value skips certificate validation. -# -#CAfile = @@datadir@@/lacme/ca-certificates.crt - # Subject field of the Certificate Signing Request. This option is # required. # diff --git a/lacme.8.md b/lacme.8.md index d98ec8e..5e32dcb 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -366,18 +366,21 @@ Valid options are: *CAfile* -: Path to trusted issuer certificates, used for validating each issued - certificate. Specifying an empty values skips certificate validation. +: Path to the bundle of trusted issuer certificates. This is used for + validating each certificate after issuance or renewal. Specifying + an empty value skips certificate validation. Default: `@@datadir@@/lacme/ca-certificates.crt`. *hash* -: Message digest algorithm to sign the Certificate Signing Request - with. +: Message digest to sign the Certificate Signing Request with, + overriding the [`req`(1ssl)] default. *keyUsage* -: Comma-separated list of Key Usages, see [`x509v3_config`(5ssl)]. +: Comma-separated list of Key Usages, for instance `digitalSignature, + keyEncipherment`, to include in the Certificate Signing Request. + See [`x509v3_config`(5ssl)] for a list of possible values. *subject* @@ -425,3 +428,4 @@ See also [`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html [`x509v3_config`(5ssl)]: https://www.openssl.org/docs/manmaster/apps/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 -- cgit v1.2.3 From 7af56dad957e75c4f2c3aceda868193a5e94210f Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 20:04:47 +0100 Subject: Fix broken URLs. --- lacme.8.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lacme.8.md b/lacme.8.md index 5e32dcb..3840455 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -425,7 +425,7 @@ See also [ACME]: https://tools.ietf.org/html/rfc8555 [`lacme-accountd`(1)]: lacme-accountd.1.html [`iptables`(8)]: https://linux.die.net/man/8/iptables -[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html -[`x509v3_config`(5ssl)]: https://www.openssl.org/docs/manmaster/apps/x509v3_config.html +[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html +[`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 -- cgit v1.2.3 From e751a1e0215342be52da2c086ad2e7bc8901229e Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 20:10:30 +0100 Subject: s/\.pem$/.crt/ --- config/lacme-certs.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf index 3e7a577..038d685 100644 --- a/config/lacme-certs.conf +++ b/config/lacme-certs.conf @@ -32,12 +32,12 @@ # Where to store the issued certificate (in PEM format). # -#certificate = /etc/nginx/ssl/srv.pem +#certificate = /etc/nginx/ssl/srv.crt # Where to store the issued certificate along with its chain of trust # (in PEM format). # -#certificate-chain = /etc/nginx/ssl/srv.chain.pem +#certificate-chain = /etc/nginx/ssl/srv.chain.crt # Subject field of the Certificate Signing Request. This option is # required. @@ -65,7 +65,7 @@ #[smtp] #certificate-key = /etc/postfix/ssl/srv.key -#certificate-chain = /etc/postfix/ssl/srv.pem +#certificate-chain = /etc/postfix/ssl/srv.crt #subject = /CN=smtp.example.org #notify = /bin/systemctl reload postfix -- cgit v1.2.3 From 61e4ad1347f51a84400cbf87633cc99f657f9ad7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 20:28:46 +0100 Subject: Make unprivileged user/group for the internal client resp. webserver configurable. --- Changelog | 2 ++ Makefile | 12 +++++++++++- config/lacme.conf | 20 ++++++++++---------- lacme | 8 ++++---- lacme.8.md | 22 ++++++++++------------ 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/Changelog b/Changelog index f94e81e..a9f137e 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,7 @@ lacme (0.7.1) upstream; + * Unprivileged user/group for the internal client resp. webserver are + now configurable at install time. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/Makefile b/Makefile index a4098de..afc5c71 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,11 @@ mandir ?= $(datarootdir)/man man1dir ?= $(mandir)/man1 man8dir ?= $(mandir)/man8 +lacme_www_user ?= www-data +lacme_www_group ?= www-data +lacme_client_user ?= nobody +lacme_client_group ?= nogroup + $(BUILDDIR)/%: % mkdir -pv -- $(dir $@) cp --no-dereference --preserve=mode,links,xattr -vfT -- "$<" "$@" @@ -43,7 +48,12 @@ $(BUILDDIR)/%: % s#@@libexecdir@@#$(libexecdir)#g; \ s#@@datadir@@#$(datadir)#g; \ s#@@runstatedir@@#$(runstatedir)#g; \ - s#@@sysconfdir@@#$(sysconfdir)#g;" -- "$@" + s#@@sysconfdir@@#$(sysconfdir)#g; \ + s#@@lacme_www_user@@#$(lacme_www_user)#g; \ + s#@@lacme_www_group@@#$(lacme_www_group)#g; \ + s#@@lacme_client_user@@#$(lacme_client_user)#g; \ + s#@@lacme_client_group@@#$(lacme_client_group)#g;" \ + -- "$@" install: all install -m0644 -vDt $(sysconfdir)/lacme $(BUILDDIR)/config/*.conf $(BUILDDIR)/snippets/*.conf diff --git a/config/lacme.conf b/config/lacme.conf index 9f4db72..cc8488d 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -19,15 +19,15 @@ #socket = # username to drop privileges to (setting both effective and real uid). -# Preserve root privileges if the value is empty (not recommended). +# Skip privilege drop if the value is empty (not recommended). # -#user = nobody +#user = @@lacme_client_user@@ # groupname to drop privileges to (setting both effective and real gid, # and also setting the list of supplementary gids to that single group). -# Preserve root privileges if the value is empty (not recommended). +# Skip privilege drop if the value is empty (not recommended). # -#group = nogroup +#group = @@lacme_client_group@@ # Path to the ACME client executable. # @@ -72,15 +72,15 @@ #challenge-directory = # username to drop privileges to (setting both effective and real uid). -# Preserve root privileges if the value is empty (not recommended). +# Skip privilege drop if the value is empty (not recommended). # -#user = www-data +#user = @@lacme_www_user@@ # groupname to drop privileges to (setting both effective and real gid, # and also setting the list of supplementary gids to that single group). -# Preserve root privileges if the value is empty (not recommended). +# Skip privilege drop if the value is empty (not recommended). # -#group = www-data +#group = @@lacme_www_group@@ # Path to the ACME webserver executable. # @@ -99,13 +99,13 @@ # an existing lacme-accountd(1) process via a UNIX-domain socket. # username to drop privileges to (setting both effective and real uid). -# Preserve root privileges if the value is empty. +# Skip privilege drop if the value is empty. # #user = # groupname to drop privileges to (setting both effective and real gid, # and also setting the list of supplementary gids to that single group). -# Preserve root privileges if the value is empty. +# Skip privilege drop if the value is empty. # #group = diff --git a/lacme b/lacme index 088e393..e4b8e01 100755 --- a/lacme +++ b/lacme @@ -91,8 +91,8 @@ do { my %valid = ( client => { socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), - user => 'nobody', - group => 'nogroup', + user => '@@lacme_client_user@@', + group => '@@lacme_client_group@@', command => '@@libexecdir@@/lacme/client', # the rest is for the ACME client map {$_ => undef} qw/server timeout SSL_verify SSL_version SSL_cipher_list/ @@ -100,8 +100,8 @@ do { webserver => { listen => '@@runstatedir@@/lacme-www.socket', 'challenge-directory' => undef, - user => 'www-data', - group => 'www-data', + user => '@@lacme_www_user@@', + group => '@@lacme_www_group@@', command => '@@libexecdir@@/lacme/webserver', iptables => 'No' diff --git a/lacme.8.md b/lacme.8.md index 3840455..ecf87c3 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -168,17 +168,16 @@ of [ACME] commands and dialogues with the remote [ACME] server). *user* : The username to drop privileges to (setting both effective and real - uid). Preserve root privileges if the value is empty (not - recommended). - Default: `nobody`. + uid). Skip privilege drop if the value is empty (not recommended). + Default: `@@lacme_client_user@@`. *group* : The groupname to drop privileges to (setting both effective and real gid, and also setting the list of supplementary gids to that single - group). Preserve root privileges if the value is empty (not + group). Skip privilege drop if the value is empty (not recommended). - Default: `nogroup`. + Default: `@@lacme_client_group@@`. *command* @@ -245,17 +244,16 @@ served during certificate issuance. *user* : The username to drop privileges to (setting both effective and real - uid). Preserve root privileges if the value is empty (not - recommended). - Default: `www-data`. + uid). Skip privilege drop if the value is empty (not recommended). + Default: `@@lacme_www_user@@`. *group* : The groupname to drop privileges to (setting both effective and real gid, and also setting the list of supplementary gids to that single - group). Preserve root privileges if the value is empty (not + group). Skip privilege drop if the value is empty (not recommended). - Default: `www-data`. + Default: `@@lacme_www_group@@`. *command* @@ -283,13 +281,13 @@ UNIX-domain socket. *user* : The username to drop privileges to (setting both effective and real - uid). Preserve root privileges if the value is empty. + uid). Skip privilege drop if the value is empty. *group* : The groupname to drop privileges to (setting both effective and real gid, and also setting the list of supplementary gids to that single - group). Preserve root privileges if the value is empty. + group). Skip privilege drop if the value is empty. *command* -- cgit v1.2.3 From 0f574f73182491fe793fcdfce6632372fab4d5c3 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 21:47:54 +0100 Subject: lacme: new flag `--force`. Which aliases to `--min-days=-1`, i.e., forces renewal regardless of the expiration date of existing certificates. --- Changelog | 3 +++ lacme | 7 ++++++- lacme.8.md | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index a9f137e..4168e58 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,9 @@ lacme (0.7.1) upstream; * Unprivileged user/group for the internal client resp. webserver are now configurable at install time. + * lacme: new flag `--force`, which aliases to `--min-days=-1`, i.e., + forces renewal regardless of the expiration date of existing + certificates. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/lacme b/lacme index e4b8e01..7f3d65d 100755 --- a/lacme +++ b/lacme @@ -63,7 +63,11 @@ sub usage(;$$) { } exit $rv; } -usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s register tos-agreed deactivate min-days=i quiet|q debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s + register tos-agreed deactivate + min-days=i force + quiet|q + debug help|h/); usage(0) if $OPTS{help}; $COMMAND = shift(@ARGV) // usage(1, "Missing command"); @@ -643,6 +647,7 @@ if ($COMMAND eq 'account') { # newOrder [SECTION ..] # elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { + $OPTS{'min-days'} = -1 if $OPTS{force}; $COMMAND = 'newOrder'; my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'} : defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ] diff --git a/lacme.8.md b/lacme.8.md index ecf87c3..2ffdc25 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -77,7 +77,7 @@ Commands Upon success, `lacme` prints the new or updated Account Object from the [ACME] server. -`lacme` [`--config-certs=`*FILE*] [`--min-days=`*INT*] `newOrder` [*SECTION* …] +`lacme newOrder` [`--config-certs=`*FILE*] [`--min-days=`*INT*|`--force`] [*SECTION* …] : Read the certificate configuration *FILE* (see the **[certificate configuration file](#certificate-configuration-file)** section below @@ -85,6 +85,9 @@ Commands for each of its sections (or the given list of *SECTION*s). Command alias: `new-order`. + The flag `--force` is an alias for `--min-days=-1`, which forces + renewal regardless of the expiration date of existing certificates. + `lacme` `revokeCert` *FILE* [*FILE* …] : Request that the given certificate(s) *FILE*(s) be revoked. For -- cgit v1.2.3 From 8c70ba081e9892217510b6b01f0402482161ef84 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 12 Feb 2021 22:11:01 +0100 Subject: Raise client timeout from 10 to 30s. --- Changelog | 1 + client | 2 +- config/lacme.conf | 2 +- lacme.8.md | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 4168e58..edf1d95 100644 --- a/Changelog +++ b/Changelog @@ -15,6 +15,7 @@ lacme (0.7.1) upstream; - documentation: emphasize default values in the config file, and move the most common options ('hash', 'keyUsage', 'CAfile', 'min-days') to the default section. + - Raise client timeout from 10 to 30s. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/client b/client index bacd4d6..c5788dd 100755 --- a/client +++ b/client @@ -272,7 +272,7 @@ if ($COMMAND eq 'account') { # elsif ($COMMAND eq 'newOrder') { die unless @ARGV; - my $timeout = $CONFIG->{timeout} // 10; + my $timeout = $CONFIG->{timeout} // 30; my $csr = do { local $/ = undef; }; set_kid(); diff --git a/config/lacme.conf b/config/lacme.conf index cc8488d..e49bd39 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -42,7 +42,7 @@ # Timeout in seconds after which the client stops polling the ACME # server and considers the request failed. # -#timeout = 10 +#timeout = 30 # Whether to verify the server certificate chain. # diff --git a/lacme.8.md b/lacme.8.md index 2ffdc25..3d46786 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -196,7 +196,7 @@ of [ACME] commands and dialogues with the remote [ACME] server). : Timeout in seconds after which the client stops polling the [ACME] server and considers the request failed. - Default: `10`. + Default: `30`. *SSL_verify* -- cgit v1.2.3 From b54d248515357297d84a01cf45a42a6787c21240 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 12 Feb 2021 22:06:43 +0100 Subject: Replace Types::Serialiser::true with JSON::true. This removes the dependency on Types::Serialiser. --- Changelog | 1 + INSTALL | 1 - client | 9 ++++----- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Changelog b/Changelog index edf1d95..269d246 100644 --- a/Changelog +++ b/Changelog @@ -16,6 +16,7 @@ lacme (0.7.1) upstream; the most common options ('hash', 'keyUsage', 'CAfile', 'min-days') to the default section. - Raise client timeout from 10 to 30s. + - Remove dependency on Types::Serialiser. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/INSTALL b/INSTALL index 155c7aa..4780989 100644 --- a/INSTALL +++ b/INSTALL @@ -32,7 +32,6 @@ lacme depends on OpenSSL and the following Perl modules: - MIME::Base64 (core module) - Net::SSLeay - POSIX (core module) - - Types::Serialiser - Socket (core module) On Debian GNU/Linux systems, these dependencies can be installed with diff --git a/client b/client index c5788dd..c1066c1 100755 --- a/client +++ b/client @@ -52,7 +52,6 @@ use MIME::Base64 qw/encode_base64 encode_base64url/; use Date::Parse (); use LWP::UserAgent (); -use Types::Serialiser (); use JSON (); use Config::Tiny (); @@ -226,7 +225,7 @@ sub acme_resource($%) { # Set the key ID (registration URI) sub set_kid(;$) { my $die = shift // 1; - my $r = acme_resource('newAccount', onlyReturnExisting => Types::Serialiser::true ); + my $r = acme_resource('newAccount', onlyReturnExisting => JSON::true ); if ($r->is_success()) { $KID = $r->header('Location'); } elsif ($die) { @@ -242,9 +241,9 @@ if ($COMMAND eq 'account') { my $flags = shift @ARGV; my %h = ( contact => \@ARGV ) if @ARGV; - $h{onlyReturnExisting} = Types::Serialiser::true unless $flags & 0x01; - $h{termsOfServiceAgreed} = Types::Serialiser::true if $flags & 0x02; - $h{status} = "deactivated" if $flags & 0x04; + $h{onlyReturnExisting} = JSON::true unless $flags & 0x01; + $h{termsOfServiceAgreed} = JSON::true if $flags & 0x02; + $h{status} = "deactivated" if $flags & 0x04; print STDERR "Requesting new registration ".(@ARGV ? ("for ".join(', ', @ARGV)) : "")."\n" if $flags & 0x01; -- cgit v1.2.3 From 49d90dbaf471931f85f9e97bc57ddc0fde1f2fb7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 12 Feb 2021 22:26:33 +0100 Subject: client: fail immediately when the accountd is unreachable. --- Changelog | 1 + client | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 269d246..34ace54 100644 --- a/Changelog +++ b/Changelog @@ -17,6 +17,7 @@ lacme (0.7.1) upstream; the default section. - Raise client timeout from 10 to 30s. - Remove dependency on Types::Serialiser. + - client: fail immediately when the accountd is unreachable. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/client b/client index c1066c1..b722d59 100755 --- a/client +++ b/client @@ -193,7 +193,7 @@ sub acme($;$) { my $protected = encode_base64url(json()->encode(\%header)); my $data = $protected .'.'. $payload; $S->printflush($data, "\r\n"); - my $sig = $S->getline(); + my $sig = $S->getline() // die "ERROR: No response from lacme-accountd\n"; $sig =~ s/\r\n\z// or die; undef $NONCE; # consume the nonce -- cgit v1.2.3 From abd23923fa4d9ea609ce4178695fb73b34b7a5ea Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 12 Feb 2021 22:26:55 +0100 Subject: wibble --- lacme-accountd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lacme-accountd b/lacme-accountd index deccfa2..e946135 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -128,7 +128,7 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { else { die "Error: unsupported method: $OPTS{privkey}\n"; } -$JWK = JSON::->new->encode($JWK); +my $JWK_STR = JSON::->new->encode($JWK); ############################################################################# @@ -173,7 +173,7 @@ if (defined $OPTS{'conn-fd'}) { sub conn($;$) { my $conn = shift; my $count = shift; - $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK, "\r\n" ); + $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ); # sign whatever comes in while (defined (my $data = $conn->getline())) { -- cgit v1.2.3 From 681bf10e103b84b278491e6fff88e9d600ada72d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 12 Feb 2021 22:13:31 +0100 Subject: Improve keyUsage documentation. --- config/lacme-certs.conf | 3 ++- lacme.8.md | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf index 038d685..91c2b3d 100644 --- a/config/lacme-certs.conf +++ b/config/lacme-certs.conf @@ -8,7 +8,8 @@ # Comma-separated list of Key Usages, for instance "digitalSignature, # keyEncipherment", to include in the Certificate Signing Request. -# See x509v3_config(5ssl) for a list of possible values. +# See x509v3_config(5ssl) for a list of possible values. Note that the +# ACME might override the value provided here. # #keyUsage = diff --git a/lacme.8.md b/lacme.8.md index 3d46786..ee6aedd 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -382,6 +382,8 @@ Valid options are: : Comma-separated list of Key Usages, for instance `digitalSignature, keyEncipherment`, to include in the Certificate Signing Request. See [`x509v3_config`(5ssl)] for a list of possible values. + See x509v3_config(5ssl) for a list of possible values. Note that + the ACME might override the value provided here. *subject* -- cgit v1.2.3 From 43283731d372ba21bc5418bd02150671f81ce633 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 12 Feb 2021 22:15:07 +0100 Subject: Improve user/group documentation. --- lacme.8.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lacme.8.md b/lacme.8.md index ee6aedd..384f21b 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -284,13 +284,13 @@ UNIX-domain socket. *user* : The username to drop privileges to (setting both effective and real - uid). Skip privilege drop if the value is empty. + uid). Skip privilege drop if the value is empty (the default). *group* : The groupname to drop privileges to (setting both effective and real gid, and also setting the list of supplementary gids to that single - group). Skip privilege drop if the value is empty. + group). Skip privilege drop if the value is empty (the default). *command* -- cgit v1.2.3 From 1005c094839b76dffde6a10138af978cb8d83375 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 11:39:33 +0100 Subject: Rename debian branch to debian/latest. For DEP-14 compliance. --- INSTALL | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/INSTALL b/INSTALL index 4780989..a0fcb72 100644 --- a/INSTALL +++ b/INSTALL @@ -49,11 +49,11 @@ the following command: However Debian GNU/Linux users can also use gbp(1) from git-buildpackage to build their own package: - $ git checkout debian + $ git checkout debian/latest $ AUTO_DEBSIGN=no gbp buildpackage Alternatively, for the development version: - $ git checkout debian + $ git checkout debian/latest $ git merge master $ AUTO_DEBSIGN=no gbp buildpackage --git-force-create --git-upstream-tree=BRANCH -- cgit v1.2.3 From 9dfb2cde7baf686113e49266c28940c8a564c1ca Mon Sep 17 00:00:00 2001 From: Benjamin Tietz Date: Wed, 23 Sep 2020 17:22:32 +0200 Subject: lacme: allow direct use challenge-directory .well-known/acme-challenge --- config/lacme.conf | 4 ++++ lacme | 26 ++++++++++++++++++++++---- lacme.8.md | 5 +++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/config/lacme.conf b/config/lacme.conf index e49bd39..2955984 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -71,6 +71,10 @@ # #challenge-directory = +# Do not symlink the challenge-directory, but copy the challenge-files +# explictly. +#hard-copy-challenge-directory = No + # 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 7f3d65d..d7ae8ce 100755 --- a/lacme +++ b/lacme @@ -28,6 +28,7 @@ my $NAME = 'lacme'; use Errno 'EINTR'; use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; use File::Temp (); +use File::Path 'remove_tree'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; use POSIX (); @@ -104,6 +105,7 @@ do { webserver => { listen => '@@runstatedir@@/lacme-www.socket', 'challenge-directory' => undef, + 'hard-copy-challenge-directory' => 'No', user => '@@lacme_www_user@@', group => '@@lacme_www_group@@', command => '@@libexecdir@@/lacme/webserver', @@ -289,10 +291,26 @@ sub spawn_webserver() { # serve ACME challenge reponses). if (defined (my $dir = $conf->{'challenge-directory'})) { print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; - symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; - push @CLEANUP, sub() { - print STDERR "Unlinking $dir\n" if $OPTS{debug}; - unlink $dir or warn "Warning: Can't unlink $dir: $!"; + if (lc ($conf->{'hard-copy-challenge-directory'} // 'No') eq 'yes') { + mkdir $dir or die "Can't create directory $dir: $!"; + $tmpdir = $dir; + push @CLEANUP, sub() { + my $error = undef; + remove_tree($dir, { safe => 1, error => \$error }); + if ($error && @$error) { + foreach my $e (@$error) { + my ($file, $message) = %$e; + my $msghead = $file?"Error removing $file in":"Error while removing"; + warn "$msghead challenge dir $dir: $message\n"; + } + } + } + } else { + symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; + push @CLEANUP, sub() { + print STDERR "Unlinking $dir\n" if $OPTS{debug}; + unlink $dir or warn "Warning: Can't unlink $dir: $!"; + } } } elsif (!@sockaddr) { diff --git a/lacme.8.md b/lacme.8.md index 384f21b..404180c 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -244,6 +244,11 @@ served during certificate issuance. authorization) as static files. This option is required when *listen* is empty. +*hard-copy-challenge-directory* + +: Do not symlink the challenge-directory, but copy the challenge-files + explictly. + *user* : The username to drop privileges to (setting both effective and real -- cgit v1.2.3 From a903ea92dd736c560d21fe45063d4914765fa173 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 17:01:17 +0100 Subject: challenge-directory now needs to be set to an *existing* directory. Since lacme(8) spawns a builtin webserver by default the change doesn't affect default configurations. See https://bugs.debian.org/970800 for the rationale. --- Changelog | 7 +++++ config/lacme.conf | 13 ++++----- lacme | 82 ++++++++++++++++++++++++++++++----------------------- lacme.8.md | 16 +++++------ snippets/nginx.conf | 2 +- 5 files changed, 66 insertions(+), 54 deletions(-) diff --git a/Changelog b/Changelog index 34ace54..39249b4 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,12 @@ lacme (0.7.1) upstream; + * Breaking change: 'challenge-directory' now needs to be set to an + *existing* directory (writable by the lacme client user). Since + lacme(8) spawns a builtin webserver by default the change doesn't + affect default configurations. + Thanks to Benjamin Tietz for the idea and initial patch. + * Breaking change: the 'iptables' option is now ignored unless the + builtin webserver is used. * Unprivileged user/group for the internal client resp. webserver are now configurable at install time. * lacme: new flag `--force`, which aliases to `--min-days=-1`, i.e., diff --git a/config/lacme.conf b/config/lacme.conf index 2955984..4c7dc86 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -64,17 +64,14 @@ # #listen = @@runstatedir@@/lacme-www.socket -# Non-existent directory under which an external HTTP daemon is -# configured to serve GET requests for challenge files under -# "/.well-known/acme-challenge/" (for each virtual host requiring -# authorization) as static files. +# Directory under which an external HTTP daemon is configured to serve +# GET requests for challenge files under "/.well-known/acme-challenge/" +# (for each virtual host requiring authorization) as static files. +# NOTE: the directory must exist and be writable by the lacme client +# user. # #challenge-directory = -# Do not symlink the challenge-directory, but copy the challenge-files -# explictly. -#hard-copy-challenge-directory = No - # 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 d7ae8ce..7ad7aa8 100755 --- a/lacme +++ b/lacme @@ -26,9 +26,8 @@ our $VERSION = '0.3'; my $NAME = 'lacme'; use Errno 'EINTR'; -use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; +use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC O_CREAT O_EXCL O_WRONLY SEEK_SET/; use File::Temp (); -use File::Path 'remove_tree'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; use POSIX (); @@ -105,7 +104,6 @@ do { webserver => { listen => '@@runstatedir@@/lacme-www.socket', 'challenge-directory' => undef, - 'hard-copy-challenge-directory' => 'No', user => '@@lacme_www_user@@', group => '@@lacme_www_group@@', command => '@@libexecdir@@/lacme/webserver', @@ -258,15 +256,6 @@ sub set_FD_CLOEXEC($$) { # The temporary challenge directory is returned. # sub spawn_webserver() { - # create a temporary directory; give write access to the ACME client - # and read access to the webserver - my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; - chmod 0755, $tmpdir or die "Can't chmod: $!"; - if ((my $username = $CONFIG->{client}->{user}) ne '') { - my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; - chown($uid, -1, $tmpdir) or die "Can't chown: $!"; - } - my $conf = $CONFIG->{webserver}; # parse and pack addresses to listen to @@ -286,35 +275,56 @@ sub spawn_webserver() { push @sockaddr, $sockaddr; } - # symlink the 'challenge-directory' configuration option to the - # temporary challenge directory (so an existing httpd can directly - # serve ACME challenge reponses). + # Use existing HTTPd to serve challenge files using 'challenge-directory' + # as document root if (defined (my $dir = $conf->{'challenge-directory'})) { print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; - if (lc ($conf->{'hard-copy-challenge-directory'} // 'No') eq 'yes') { - mkdir $dir or die "Can't create directory $dir: $!"; - $tmpdir = $dir; - push @CLEANUP, sub() { - my $error = undef; - remove_tree($dir, { safe => 1, error => \$error }); - if ($error && @$error) { - foreach my $e (@$error) { - my ($file, $message) = %$e; - my $msghead = $file?"Error removing $file in":"Error while removing"; - warn "$msghead challenge dir $dir: $message\n"; - } + # lacme(8) doesn't have the list of challenge files to delete on + # cleanup -- instead, we unlink all files and fails at + # initialization stage when the challenge directory is not empty + + opendir my $dh, $dir or die "opendir($dir): $!\n"; + while (readdir $dh) { + die "Error: Refusing to use non-empty challenge directory $dir\n" + unless $_ eq '.' or $_ eq '..'; + } + closedir $dh or die "close: $!"; + undef $dh; + + # use a "lock file" (NFS-friendly) to avoid concurrent usages + my $lockfile = ".$NAME.lock"; + sysopen(my $fh, "$dir/$lockfile", O_CREAT|O_EXCL|O_WRONLY, 0600) + or die "Can't create lockfile in challenge directory: $!"; + print $fh $$, "\n"; + close $fh or die "close: $!"; + undef $fh; + + push @CLEANUP, sub() { + if (opendir(my $dh, $dir)) { + my @files = grep { $_ ne '.' and $_ ne '..' and $_ ne $lockfile } readdir $dh; + closedir $dh or warn "close: $!"; + push @files, $lockfile; # unlink $lockfile last + foreach (@files) { + die unless /\A(.+)\z/; # untaint + unlink "$dir/$1" or warn "unlink($dir/$1): $!"; } + } else { + warn "opendir($dir): $!\n"; } - } else { - symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; - push @CLEANUP, sub() { - print STDERR "Unlinking $dir\n" if $OPTS{debug}; - unlink $dir or warn "Warning: Can't unlink $dir: $!"; - } - } + }; + return $dir; # ignore 'listen' and 'iptables' } - elsif (!@sockaddr) { - die "'challenge-directory' option of section [webserver] is required when 'listen' is empty\n"; + + die "'challenge-directory' option is required in section [webserver] when 'listen' is empty\n" + unless @sockaddr; + + # create a temporary directory; give write access to the ACME client + # and read access to the webserver + my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; + chmod 0755, $tmpdir or die "Can't chmod: $!"; + if ((my $username = $CONFIG->{client}->{user}) ne '') { + my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; + chown($uid, -1, $tmpdir) or die "Can't chown: $!"; } # create socket(s) and spawn webserver(s) diff --git a/lacme.8.md b/lacme.8.md index 404180c..76cdd0d 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -238,16 +238,13 @@ served during certificate issuance. *challenge-directory* -: Specify a non-existent directory under which an external HTTP daemon - is configured to serve `GET` requests for challenge files under - `/.well-known/acme-challenge/` (for each virtual host requiring - authorization) as static files. - This option is required when *listen* is empty. - -*hard-copy-challenge-directory* +: Directory under which an external HTTP daemon is configured to serve `GET` + requests for challenge files under `/.well-known/acme-challenge/` (for + each virtual host requiring authorization) as static files. + `lacme` _must_ exist beforehand, _must_ be empty, and be writable by the + lacme client user (by default @@lacme_client_user@@). -: Do not symlink the challenge-directory, but copy the challenge-files - explictly. + This option is required when *listen* is empty. *user* @@ -275,6 +272,7 @@ served during certificate issuance. : Whether to automatically install temporary [`iptables`(8)] rules to open the `ADDRESS[:PORT]` specified with *listen*. The rules are automatically removed once `lacme` exits. + This option is ignored when *challenge-directory* is set. Default: `No`. `[accountd]` section diff --git a/snippets/nginx.conf b/snippets/nginx.conf index 6775489..af2e92e 100644 --- a/snippets/nginx.conf +++ b/snippets/nginx.conf @@ -13,6 +13,6 @@ location ^~ /.well-known/acme-challenge/ { ## lacme's configuration file # alias /var/www/acme-challenge/; # default_type application/jose+json; - # disable_symlinks on from=$document_root; + # disable_symlinks on; # autoindex off; } -- cgit v1.2.3 From 956764d11c9445c835f992a782d90d8de90fe565 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 20:40:29 +0100 Subject: Improve nginx/apache2 snippets for direct serving of challenge files. With the new 'challenge-directory' logic symlinks can be disabled. --- Changelog | 2 ++ snippets/apache2.conf | 30 ++++++++++++++++++++++++------ snippets/nginx.conf | 4 +++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Changelog b/Changelog index 39249b4..72e4be6 100644 --- a/Changelog +++ b/Changelog @@ -12,6 +12,8 @@ lacme (0.7.1) upstream; * lacme: new flag `--force`, which aliases to `--min-days=-1`, i.e., forces renewal regardless of the expiration date of existing certificates. + + Improve nginx/apache2 snippets for direct serving of challenge files + (with the new 'challenge-directory' logic symlinks can be disabled). - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/snippets/apache2.conf b/snippets/apache2.conf index 45d7c7f..69d80a7 100644 --- a/snippets/apache2.conf +++ b/snippets/apache2.conf @@ -1,11 +1,29 @@ -# Use Apache2 to serve ACME requests by passing them over to a -# locally-bound lacme webserver component. +# Use Apache2 to serve ACME requests; either directly, or by passing +# them over to a locally-bound lacme webserver component. # # This file needs to be sourced to the server directives (at least the # non-ssl one) of each virtual host requiring authorization. +# Alternatively, run `a2enconf lacme` and reload apache2. - - ProxyPass unix://@@runstatedir@@/lacme-www.socket|http://localhost/.well-known/acme-challenge/ - Require all granted - +# Pass ACME requests to lacme's webserver component + + + ProxyPass unix://@@runstatedir@@/lacme-www.socket|http://localhost/.well-known/acme-challenge/ + Require all granted + + + + +## Alternatively, you can let Apache2 serve the requests by +## setting 'challenge-directory' to '/var/www/acme-challenge' in +## lacme's configuration file and uncomment the following: + +# +# Alias /.well-known/acme-challenge/ /var/www/acme-challenge/ +# +# Options none +# AllowOverride none +# Require all granted +# +# diff --git a/snippets/nginx.conf b/snippets/nginx.conf index af2e92e..76309f0 100644 --- a/snippets/nginx.conf +++ b/snippets/nginx.conf @@ -8,9 +8,11 @@ location ^~ /.well-known/acme-challenge/ { # Pass ACME requests to lacme's webserver component proxy_pass http://unix:@@runstatedir@@/lacme-www.socket; + ## Alternatively, you can let nginx serve the requests by ## setting 'challenge-directory' to '/var/www/acme-challenge' in - ## lacme's configuration file + ## lacme's configuration file and uncomment the following: + # alias /var/www/acme-challenge/; # default_type application/jose+json; # disable_symlinks on; -- cgit v1.2.3 From 2c1a396728a381685923f7b1c4dea53d225112fc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 22:59:11 +0100 Subject: Add (self-signed) ISRG Roots to the CA bundle. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to fully validate provided X.509 chains using that self-contained bundle, regardless of which CAs is marqued as trusted under /etc/ssl/certs. Also, remove cross-signed intermediate CAs from the bundle as they're useless in a self-contained bundle. Also, remove decomissioned intermediate CAs Authority X3 and X4 from the bundle. This change bumps the minimum OpenSSL version to 1.1.0 (for verify(1ssl)'s ‘-trusted’ and ‘-show_chain’ options). --- Changelog | 7 +++++++ INSTALL | 2 +- Makefile | 7 +++---- certs/isrg-root-x2.pem | 14 ++++++++++++++ certs/isrgrootx1.pem | 31 +++++++++++++++++++++++++++++++ lacme | 18 +++++++++++------- 6 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 certs/isrg-root-x2.pem create mode 100644 certs/isrgrootx1.pem diff --git a/Changelog b/Changelog index 72e4be6..7cef63c 100644 --- a/Changelog +++ b/Changelog @@ -12,6 +12,13 @@ lacme (0.7.1) upstream; * lacme: new flag `--force`, which aliases to `--min-days=-1`, i.e., forces renewal regardless of the expiration date of existing certificates. + * Remove decomissioned intermediate CAs Authority X3 and X4 from the + bundle. + * Remove cross-signed intermediate CAs from the bundle and add the + (self-signed) ISRG Root X1 and X2 instead. This allows us to fully + 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. + Improve nginx/apache2 snippets for direct serving of challenge files (with the new 'challenge-directory' logic symlinks can be disabled). - lacme: delay webserver socket shutdown to after the process has diff --git a/INSTALL b/INSTALL index a0fcb72..9ecb1bf 100644 --- a/INSTALL +++ b/INSTALL @@ -16,7 +16,7 @@ the following command: apt-get install libconfig-tiny-perl libcrypt-openssl-rsa-perl libcrypt-openssl-bignum-perl libjson-perl -lacme depends on OpenSSL and the following Perl modules: +lacme depends on OpenSSL ≥1.1.0 and the following Perl modules: - Config::Tiny - Digest::SHA (core module) diff --git a/Makefile b/Makefile index afc5c71..fa830c4 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,12 @@ $(MANUAL_FILES): $(BUILDDIR)/%: $(BUILDDIR)/%.md # used for validation, see https://letsencrypt.org/certificates/ $(BUILDDIR)/certs/ca-certificates.crt: \ - certs/letsencryptauthorityx[34].pem \ - certs/lets-encrypt-x[34]-cross-signed.pem \ + certs/isrgrootx1.pem \ + certs/isrg-root-x2.pem \ certs/lets-encrypt-r[34].pem \ - certs/lets-encrypt-r[34]-cross-signed.pem \ certs/lets-encrypt-e[12].pem mkdir -pv -- $(BUILDDIR)/certs - cat $^ >$@ + cat -- $^ >$@ prefix ?= $(DESTDIR) exec_prefix ?= $(prefix) diff --git a/certs/isrg-root-x2.pem b/certs/isrg-root-x2.pem new file mode 100644 index 0000000..7d903ed --- /dev/null +++ b/certs/isrg-root-x2.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- diff --git a/certs/isrgrootx1.pem b/certs/isrgrootx1.pem new file mode 100644 index 0000000..b85c803 --- /dev/null +++ b/certs/isrgrootx1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/lacme b/lacme index 7ad7aa8..480778f 100755 --- a/lacme +++ b/lacme @@ -784,13 +784,17 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { next; }; - # verify certificate validity against the CA - $conf->{CAfile} //= '@@datadir@@/lacme/ca-certificates.crt'; - if ($conf->{CAfile} ne '' and spawn({in => $x509}, 'openssl', 'verify', '-CAfile', $conf->{CAfile}, - qw/-purpose sslserver -x509_strict/)) { - print STDERR "[$s] Error: Received invalid X.509 certificate from ACME server!\n"; - $rv = 1; - next; + # verify certificate validity against the CA bundle + if ((my $CAfile = $conf->{CAfile} // '@@datadir@@/lacme/ca-certificates.crt') ne '') { + my %args = (in => $x509); + $args{out} = \*STDERR if $OPTS{debug}; + my @options = ('-trusted', $CAfile, '-purpose', 'sslserver', '-x509_strict'); + push @options, '-show_chain' if $OPTS{debug}; + if (spawn(\%args, 'openssl', 'verify', @options)) { + print STDERR "[$s] Error: Received invalid X.509 certificate from ACME server!\n"; + $rv = 1; + next; + } } # install certificate -- cgit v1.2.3 From f3e28985165e9ff30907d5da45a4a0bc8c0ccf31 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 17:02:31 +0100 Subject: Bump copyright years. --- README | 2 +- client | 2 +- lacme | 2 +- lacme-accountd | 2 +- webserver | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README b/README index eb46cdd..97b4c86 100644 --- a/README +++ b/README @@ -59,6 +59,6 @@ even on a smartcard. _______________________________________________________________________ -lacme is Copyright© 2016,2017 Guilhem Moulin ⟨guilhem@fripost.org⟩, and +lacme is Copyright © 2015-2021 Guilhem Moulin ⟨guilhem@fripost.org⟩, and licensed for use under the GNU General Public License version 3 or later. See ‘COPYING’ for specific terms and distribution information. diff --git a/client b/client index b722d59..0556450 100755 --- a/client +++ b/client @@ -2,7 +2,7 @@ #---------------------------------------------------------------------- # ACME client written with process isolation and minimal privileges in mind -# Copyright © 2015-2017 Guilhem Moulin +# Copyright © 2015-2021 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/lacme b/lacme index 480778f..bd4bd73 100755 --- a/lacme +++ b/lacme @@ -2,7 +2,7 @@ #---------------------------------------------------------------------- # ACME client written with process isolation and minimal privileges in mind -# Copyright © 2016-2017 Guilhem Moulin +# Copyright © 2015-2021 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/lacme-accountd b/lacme-accountd index e946135..d05fb9c 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -3,7 +3,7 @@ #---------------------------------------------------------------------- # ACME client written with process isolation and minimal privileges in mind # (account key manager) -# Copyright © 2016-2017 Guilhem Moulin +# Copyright © 2015-2021 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/webserver b/webserver index c16737f..94b86ca 100755 --- a/webserver +++ b/webserver @@ -3,7 +3,7 @@ #---------------------------------------------------------------------- # ACME client written with process isolation and minimal privileges in mind # (webserver component) -# Copyright © 2015-2017 Guilhem Moulin +# Copyright © 2015-2021 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -- cgit v1.2.3 From 5dcb74302029ffcfd076f9ab10329e2196f17f85 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 23:11:46 +0100 Subject: Add certs/letsencryptauthorityx[12].pem --- certs/letsencryptauthorityx1.pem | 32 ++++++++++++++++++++++++++++++++ certs/letsencryptauthorityx2.pem | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 certs/letsencryptauthorityx1.pem create mode 100644 certs/letsencryptauthorityx2.pem diff --git a/certs/letsencryptauthorityx1.pem b/certs/letsencryptauthorityx1.pem new file mode 100644 index 0000000..0a9a3ce --- /dev/null +++ b/certs/letsencryptauthorityx1.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRAOeTkL6SBwNJGF95dYHlyoMwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTIwMDIw +WhcNMjAwNjA0MTIwMDIwWjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDEwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX +NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf +89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl +Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc +Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz +uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo +SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +AGvM/XGv8yafGRGMPP6hnggoI9DGWGf4l0mzjBhuCkDVqoG/7rsH1ytzteePxiA3 +7kqSBo0fXu5GmbWOw09GpwPYyAAY0iWOMU6ybrTJHS466Urzoe/4IwLQoQc219EK +lh+4Ugu1q4KxNY1qMDA/1YX2Qm9M6AcAs1UvZKHSpJQAbsYrbN6obNoUGOeG6ONH +Yr8KRQz5FMfZYcA49fmdDTwKn/pyLOkJFeA/dm/oP99UmKCFoeOa5w9YJr2Vi7ic +Xd59CU8mprWhxFXnma1oU3T8ZNovjib3UHocjlEJfNbDy9zgKTYURcMVweo1dkbH +NbLc5mIjIk/kJ+RPD+chR+gJjy3Gh9xMNkDrZQKfsIO93hxTsZMmgZQ4c+vujC1M +jSak+Ai87YZeYQPh1fCGMSTno5III37DUCtIn8BJxJixuPeOMKsjLLD5AtMVy0fp +d19lcUek4bjDY8/Ujb5/wfn2+Kk7z72SxWdekjtHOWBmKxqq8jDuuMw4ymg1g5n7 +R7TZ/Y3y4bTpWUDkBHFo03xNM21wBFDIrCZZeVhvDW4MtT6+Ass2bcpoHwYcGol2 +gaLDa5k2dkG41OGtXa0fY+TjdryY4cOcstJUKjv2MJku4yaTtjjECX1rJvFLnqYe +wC+FmxjgWPuyRNuLDAWK30mmpcJZ3CmD6dFtAi4h7H37 +-----END CERTIFICATE----- diff --git a/certs/letsencryptauthorityx2.pem b/certs/letsencryptauthorityx2.pem new file mode 100644 index 0000000..3a8e77c --- /dev/null +++ b/certs/letsencryptauthorityx2.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIRAJY2TKc4C+SL3JDGzeC33mgwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTIwMDMx +WhcNMjAwNjA0MTIwMDMxWjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDhJHRCe7eRMdlz/ziq2M5EXLc5 +CtxErg29RbmXN2evvVBPX9MQVGv3QdqOY+ZtW8DoQKmMQfzRA4n/YmEJYNYHBXia +kL0aZD5P3M93L4lry2evQU3FjQDAa/6NhNy18pUxqOj2kKBDSpN0XLM+Q2lLiSJH +dFE+mWTDzSQB+YQvKHcXIqfdw2wITGYvN3TFb5OOsEY3FmHRUJjIsA9PWFN8rPba +LZZhUK1D3AqmT561Urmcju9O30azMdwg/GnCoyB1Puw4GzZOZmbS3/VmpJMve6YO +lD5gPUpLHG+6tE0cPJFYbi9NxNpw2+0BOXbASefpNbUUBpDB5ZLiEP1rubSFAgMB +AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU +BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB +FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBTF +satOTLHNZDCTfsGEmQWr5gPiJTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js +LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF +BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG +AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD +VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB +AA4eqMjSEJKCF6XRR5pEutkS/e7xgy2vCYYbw1ospQiGQ4FO5TtbvO+5K4v7WR3b +1peMQ03rX0Dr+ylmGNypZahNxTqDiO0X2sHBwJWj/k61+MYq3bRYxKwI6cduTDXb +YQxilGTDNGZUIFKKIloz4zGAl68sj+8pLg534EqKgl8+rWSxclToS1KrydJezokE +dQRXfxu79iscWA3PIj1vbaUBB16lnWJxA3LhTGhUrhZrCnFuOZ93KO8kCKPM7EVo +7c4FCYKI8eWDsf0FF49A4xMUmxPJAPIyZkwQ8KkjpzcTHOmT4CEXUhNu9eMI9qBK +VSFDDMifJ8HzCaVLyMvY1Kf7iR+840EkX1EGC+Z39EaK1hjm314LYpLoYGvYYLJO +/J76XAx8ZgpofqHz1gAEfiMLMLxLQkOjKLXqoUEd5KdnzaO3aLH91gnasy8aD4D5 +9RfEO2xcaozD2rbYsoAMVzcZZHw0Smdmobaz2YazMBjFRcqGntg6s5Xqwusaleiy +snjMCC/9mvIPqGyuVnBPTBaUDFDEhX6qD2MX4dzODL91Z0ogYDWcFLN+uLnZKHje +4JoNuzkJ2FXWOREcsW93KXb+3T8COjhTDKvK4H6ufdrZxxusx60ajJAMBzW0XTf5 +nm2yGEDtyVoMgJLp0rkiPlormgHxSkFDOJbY94J7yxRK +-----END CERTIFICATE----- -- cgit v1.2.3 From f62a66c6ce82d9a1af241dc3952250362e601d45 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 23:46:40 +0100 Subject: Add support for TLS Feature extension from RFC 7633. This is mostly useful for OCSP Must-Staple. --- Changelog | 2 ++ lacme | 5 +++-- lacme.8.md | 33 ++++++++++++++++++++------------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Changelog b/Changelog index 7cef63c..a622803 100644 --- a/Changelog +++ b/Changelog @@ -21,6 +21,8 @@ lacme (0.7.1) upstream; This change bumps the minimum OpenSSL version to 1.1.0. + Improve nginx/apache2 snippets for direct serving of challenge files (with the new 'challenge-directory' logic symlinks can be disabled). + + Add support for TLS Feature extension from RFC 7633; this is mostly + useful for OCSP Must-Staple. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/lacme b/lacme index bd4bd73..045c5b4 100755 --- a/lacme +++ b/lacme @@ -159,6 +159,7 @@ sub gen_csr(%) { ); $config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; + $config->print("tlsfeature = $args{tlsfeature}\n") if defined $args{tlsfeature}; $config->close() or die "Can't close: $!"; my @args = (qw/-new -batch -key/, $args{'certificate-key'}); @@ -703,7 +704,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { my $def = delete $h->{_} // {}; $defaults{$_} = $def->{$_} foreach keys %$def; my @valid = qw/certificate certificate-chain certificate-key min-days CAfile - hash keyUsage subject subjectAltName chown chmod notify/; + hash keyUsage subject subjectAltName tlsfeature chown chmod notify/; foreach my $s (keys %$h) { $conf->{$s} = { map { $_ => delete $h->{$s}->{$_} } @valid }; die "Unknown option(s) in [$s]: ".join(', ', keys %{$h->{$s}})."\n" if %{$h->{$s}}; @@ -744,7 +745,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } # generate the CSR - my $csr = gen_csr(map {$_ => $conf->{$_}} qw/certificate-key subject subjectAltName keyUsage hash/) // do { + my $csr = gen_csr(map {$_ => $conf->{$_}} qw/certificate-key keyUsage subject subjectAltName tlsfeature hash/) // do { print STDERR "[$s] Warning: Couldn't generate CSR, skipping\n"; $rv = 1; next; diff --git a/lacme.8.md b/lacme.8.md index 76cdd0d..00a62a2 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -368,6 +368,18 @@ Valid options are: Default: the value of the CLI option `--min-days`, or `21` if there is no such option. +*subject* + +: Subject field of the Certificate Signing Request, in the form + `/type0=value0/type1=value1/type2=…`. This option is required. + +*subjectAltName* + +: Comma-separated list of Subject Alternative Names, in the form + `type0:value1,type1:value1,type2:…` + The only `type` currently supported is `DNS`, to specify an + alternative domain name. + *CAfile* : Path to the bundle of trusted issuer certificates. This is used for @@ -384,21 +396,15 @@ Valid options are: : Comma-separated list of Key Usages, for instance `digitalSignature, keyEncipherment`, to include in the Certificate Signing Request. - See [`x509v3_config`(5ssl)] for a list of possible values. - See x509v3_config(5ssl) for a list of possible values. Note that - the ACME might override the value provided here. - -*subject* + See [`x509v3_config`(5ssl)] for a list of possible values. Note + that the ACME server might override the value provided here. -: Subject field of the Certificate Signing Request, in the form - `/type0=value0/type1=value1/type2=…`. This option is required. +*tlsfeature* -*subjectAltName* - -: Comma-separated list of Subject Alternative Names, in the form - `type0:value1,type1:value1,type2:…` - The only `type` currently supported is `DNS`, to specify an - alternative domain name. +: Comma-separated list of [TLS extension][TLS Feature extension] + identifiers, such as `status_request` for OCSP Must-Staple. + See [`x509v3_config`(5ssl)] for a list of possible values. Note + that the ACME server might override the value provided here. *chown* @@ -429,6 +435,7 @@ See also [`lacme-accountd`(1)] [ACME]: https://tools.ietf.org/html/rfc8555 +[TLS Feature extension]: https://tools.ietf.org/html/rfc7633 [`lacme-accountd`(1)]: lacme-accountd.1.html [`iptables`(8)]: https://linux.die.net/man/8/iptables [`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html -- cgit v1.2.3 From 91881351597ce28d8b6448a712396d4432d4a8ba Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 23:51:30 +0100 Subject: Makefile: new 'release' target. --- Makefile | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fa830c4..3c8c6bc 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,20 @@ $(BUILDDIR)/%: % s#@@lacme_client_group@@#$(lacme_client_group)#g;" \ -- "$@" +release: + @if ! git diff HEAD --quiet -- ./Changelog ./lacme ./lacme-accountd; then \ + echo "Dirty state, refusing to release!" >&2; \ + exit 1; \ + fi + VERS=$$(dpkg-parsechangelog -l Changelog -SVersion 2>/dev/null) && \ + if git rev-parse -q --verify "refs/tags/v$$VERS" >/dev/null; then echo "tag exists" 2>/dev/null; exit 1; fi && \ + sed -ri "0,/^( -- .*) .*/ s//\\1 $(shell date -R)/" ./Changelog && \ + sed -ri "0,/^(our\\s+\\\$$VERSION\\s*=\\s*)'[0-9.]+'\\s*;/ s//\\1'$$VERS';/" \ + -- ./lacme ./lacme-accountd && \ + git commit -m "Prepare new release v$$VERS." \ + -- ./Changelog ./lacme ./lacme-accountd && \ + git tag -sm "Release version $$VERS" "v$$VERS" + install: all install -m0644 -vDt $(sysconfdir)/lacme $(BUILDDIR)/config/*.conf $(BUILDDIR)/snippets/*.conf install -vd $(sysconfdir)/lacme/lacme-certs.conf.d @@ -72,4 +86,4 @@ uninstall: clean: rm -rvf -- $(BUILDDIR) -.PHONY: all doc manual install uninstall clean +.PHONY: all doc manual release install uninstall clean -- cgit v1.2.3 From db34a391d745bd3f94fbc3bba1cc8743a8831202 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 15 Feb 2021 00:01:13 +0100 Subject: typofix --- lacme.8.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lacme.8.md b/lacme.8.md index 00a62a2..cea5298 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -241,8 +241,9 @@ served during certificate issuance. : Directory under which an external HTTP daemon is configured to serve `GET` requests for challenge files under `/.well-known/acme-challenge/` (for each virtual host requiring authorization) as static files. - `lacme` _must_ exist beforehand, _must_ be empty, and be writable by the - lacme client user (by default @@lacme_client_user@@). + The directory _must_ exist beforehand, _must_ be empty, and the + lacme client user (by default `@@lacme_client_user@@`) needs to be + able to create files under it. This option is required when *listen* is empty. -- cgit v1.2.3 From a8bc34b6ff5eccabef7420d5d5deeb8e1a9e2816 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 16 Feb 2021 16:18:16 +0100 Subject: Add certs-staging/fake*.pem for tests using the staging environment. See https://letsencrypt.org/docs/staging-environment/ . --- Makefile | 13 +++++++++++-- certs-staging/fakeleintermediatex1.pem | 28 ++++++++++++++++++++++++++++ certs-staging/fakelerootx1.pem | 29 +++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 certs-staging/fakeleintermediatex1.pem create mode 100644 certs-staging/fakelerootx1.pem diff --git a/Makefile b/Makefile index 3c8c6bc..c37c45b 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ DESTDIR ?= /usr/local BUILDDIR ?= ./build MANUAL_FILES = $(addprefix $(BUILDDIR)/,$(patsubst ./%.md,%,$(wildcard ./*.[1-9].md))) -all: manual $(addprefix $(BUILDDIR)/,lacme lacme-accountd client webserver $(wildcard certs/* config/* snippets/*) certs/ca-certificates.crt) +all: manual $(addprefix $(BUILDDIR)/,lacme lacme-accountd client webserver \ + $(wildcard certs/* config/* snippets/*) \ + certs/ca-certificates.crt certs-staging/ca-certificates.crt) doc: manual manual: $(MANUAL_FILES) @@ -17,7 +19,14 @@ $(BUILDDIR)/certs/ca-certificates.crt: \ certs/isrg-root-x2.pem \ certs/lets-encrypt-r[34].pem \ certs/lets-encrypt-e[12].pem - mkdir -pv -- $(BUILDDIR)/certs + mkdir -pv -- $(dir $@) + cat -- $^ >$@ + +# Staging Environment for tests, see https://letsencrypt.org/docs/staging-environment/ +$(BUILDDIR)/certs-staging/ca-certificates.crt: \ + certs-staging/fakelerootx1.pem \ + certs-staging/fakeleintermediatex1.pem + mkdir -pv -- $(dir $@) cat -- $^ >$@ prefix ?= $(DESTDIR) diff --git a/certs-staging/fakeleintermediatex1.pem b/certs-staging/fakeleintermediatex1.pem new file mode 100644 index 0000000..2c09b96 --- /dev/null +++ b/certs-staging/fakeleintermediatex1.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw +GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 +MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 +8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym +oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 +ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN +xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 +dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 +AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw +HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 +BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu +b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu +Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq +hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF +UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 +AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp +DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 +IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf +zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI +PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w +SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em +2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 +WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt +n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= +-----END CERTIFICATE----- + diff --git a/certs-staging/fakelerootx1.pem b/certs-staging/fakelerootx1.pem new file mode 100644 index 0000000..8c1c4b1 --- /dev/null +++ b/certs-staging/fakelerootx1.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw +GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2 +MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ +diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP +xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG +TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj +EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd +O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa +aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0 +A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr +IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe +Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb +Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50 +qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA +A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln +uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H +sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm +dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd +oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV +/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ +zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc +VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1 +Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4 +8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c +idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng== +-----END CERTIFICATE----- -- cgit v1.2.3 From 35ba3f6919fbd4a724383169d16f9eec43a27989 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 16 Feb 2021 01:06:07 +0100 Subject: typofix --- client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client b/client index 0556450..e457df1 100755 --- a/client +++ b/client @@ -367,7 +367,7 @@ elsif ($COMMAND eq 'newOrder') { ############################################################################# # revokeCert # The certificate to revoke is passed (in DER format) to STDIN; this -# is required since the ACME client might not have read access to the +# is needed since the ACME client might not have read access to the # X.509 file # elsif ($COMMAND eq 'revokeCert') { -- cgit v1.2.3 From c75bc6c37840b8fc2c57424d24c06a0bfe399de6 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 16 Feb 2021 01:06:01 +0100 Subject: client: use "lacme-client/$VERSION" as User-Agent header. --- Changelog | 1 + Makefile | 6 +++--- client | 4 +++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index a622803..efefb1e 100644 --- a/Changelog +++ b/Changelog @@ -23,6 +23,7 @@ lacme (0.7.1) upstream; (with the new 'challenge-directory' logic symlinks can be disabled). + Add support for TLS Feature extension from RFC 7633; this is mostly useful for OCSP Must-Staple. + + client: use "lacme-client/$VERSION" as User-Agent header. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/Makefile b/Makefile index c37c45b..7b0cd1a 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ $(BUILDDIR)/%: % -- "$@" release: - @if ! git diff HEAD --quiet -- ./Changelog ./lacme ./lacme-accountd; then \ + @if ! git diff HEAD --quiet -- ./Changelog ./lacme ./lacme-accountd ./client; then \ echo "Dirty state, refusing to release!" >&2; \ exit 1; \ fi @@ -72,9 +72,9 @@ release: if git rev-parse -q --verify "refs/tags/v$$VERS" >/dev/null; then echo "tag exists" 2>/dev/null; exit 1; fi && \ sed -ri "0,/^( -- .*) .*/ s//\\1 $(shell date -R)/" ./Changelog && \ sed -ri "0,/^(our\\s+\\\$$VERSION\\s*=\\s*)'[0-9.]+'\\s*;/ s//\\1'$$VERS';/" \ - -- ./lacme ./lacme-accountd && \ + -- ./lacme ./lacme-accountd ./client && \ git commit -m "Prepare new release v$$VERS." \ - -- ./Changelog ./lacme ./lacme-accountd && \ + -- ./Changelog ./lacme ./lacme-accountd ./client && \ git tag -sm "Release version $$VERS" "v$$VERS" install: all diff --git a/client b/client index e457df1..bcdf6cd 100755 --- a/client +++ b/client @@ -43,7 +43,9 @@ use warnings; # instance own by another user and created with umask 0177) is not a # problem since SOCKET_FD can be bound as root prior to the execve(2). +our $VERSION = '0.3'; my $PROTOCOL_VERSION = 1; +my $NAME = 'lacme-client'; use Errno 'EEXIST'; use Fcntl qw/O_CREAT O_EXCL O_WRONLY/; @@ -103,7 +105,7 @@ my $UA = do { my $verify = lc (delete $args{SSL_verify} // 'Yes') eq 'no' ? 0 : 1; my %ssl_opts = ( verify_hostname => $verify ); $ssl_opts{$_} = $args{$_} foreach grep /^SSL_/, keys %args; - LWP::UserAgent::->new( ssl_opts => \%ssl_opts ); + LWP::UserAgent::->new( agent => "$NAME/$VERSION", ssl_opts => \%ssl_opts ); } // die "Can't create LWP::UserAgent object"; $UA->default_header( 'Accept-Language' => 'en' ); -- cgit v1.2.3 From 2efd4458f4db7f489ecc81f4039b8e8103edf9d9 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 16 Feb 2021 17:24:31 +0100 Subject: Don't load configuration files from ./ by default. This is a breaking change: lacme(8) resp. lacme-accountd(1) no longer consider ./lacme.conf resp. ./lacme-accountd.conf as default location for the configuration file. Doing so has security implications when running these program from insecure directories. --- Changelog | 3 +++ lacme | 3 +-- lacme-accountd | 3 +-- lacme-accountd.1.md | 8 ++++---- lacme.8.md | 7 +++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Changelog b/Changelog index efefb1e..2ccb0e1 100644 --- a/Changelog +++ b/Changelog @@ -19,6 +19,9 @@ 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. + Improve nginx/apache2 snippets for direct serving of challenge files (with the new 'challenge-directory' logic symlinks can be disabled). + Add support for TLS Feature extension from RFC 7633; this is mostly diff --git a/lacme b/lacme index 045c5b4..33f947c 100755 --- a/lacme +++ b/lacme @@ -77,8 +77,7 @@ $COMMAND = $COMMAND =~ /\A(account|newOrder|new-cert|revokeCert|revoke-cert)\z/ sub set_FD_CLOEXEC($$); my $CONFFILENAME = $OPTS{config} // first { -f $_ } - ( "./$NAME.conf" - , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME.conf" + ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" , "@@sysconfdir@@/lacme/$NAME.conf" ); do { diff --git a/lacme-accountd b/lacme-accountd index d05fb9c..36e9d9f 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -65,8 +65,7 @@ usage(0) if $OPTS{help}; do { my $conffile = $OPTS{config} // first { -f $_ } - ( "./$NAME.conf" - , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME.conf" + ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" , "@@sysconfdir@@/lacme/$NAME.conf" ); die "Error: Can't find configuration file\n" unless defined $conffile; diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 560cfac..e628476 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -84,10 +84,10 @@ Configuration file ================== If `--config=` is not given, `lacme-accountd` uses the first existing -configuration file among *./lacme-accountd.conf*, -*$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*. +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 diff --git a/lacme.8.md b/lacme.8.md index cea5298..bc711ed 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -131,10 +131,9 @@ Configuration file ================== If `--config=` is not given, `lacme` uses the first existing -configuration file among *./lacme.conf*, -*$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*. +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 options are: Default section -- cgit v1.2.3 From bddbc17b87f3de29657f1dd2b9a065901e955c15 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 11:34:33 +0100 Subject: Makefile: set executable bit for $(bindir)/lacme-accountd and $(sbindir)/lacme. --- Changelog | 2 ++ Makefile | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 2ccb0e1..51f9728 100644 --- a/Changelog +++ b/Changelog @@ -40,6 +40,8 @@ lacme (0.7.1) upstream; - Raise client timeout from 10 to 30s. - Remove dependency on Types::Serialiser. - client: fail immediately when the accountd is unreachable. + - Makefile: set executable bit for $(bindir)/lacme-accountd and + $(sbindir)/lacme. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/Makefile b/Makefile index 7b0cd1a..06e8cb2 100644 --- a/Makefile +++ b/Makefile @@ -84,8 +84,8 @@ install: all install -m0755 -vDt $(libexecdir)/lacme $(BUILDDIR)/client $(BUILDDIR)/webserver install -m0644 -vDt $(man1dir) $(BUILDDIR)/lacme-accountd.1 install -m0644 -vDt $(man8dir) $(BUILDDIR)/lacme.8 - install -m0644 -vDt $(bindir) $(BUILDDIR)/lacme-accountd - install -m0644 -vDt $(sbindir) $(BUILDDIR)/lacme + install -m0755 -vDt $(bindir) $(BUILDDIR)/lacme-accountd + install -m0755 -vDt $(sbindir) $(BUILDDIR)/lacme uninstall: rm -vf -- $(bindir)/lacme-accountd $(sbindir)/lacme -- cgit v1.2.3 From 0ca64b6236f8fe767181214a97d8428d473b8e32 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 11:36:49 +0100 Subject: client: avoid "Use of uninitialized value in pattern match (m//)" perl warnings. When the accountd socket can't be reached. --- Changelog | 2 ++ client | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 51f9728..ac8102c 100644 --- a/Changelog +++ b/Changelog @@ -42,6 +42,8 @@ lacme (0.7.1) upstream; - client: fail immediately when the accountd is unreachable. - Makefile: set executable bit for $(bindir)/lacme-accountd and $(sbindir)/lacme. + - client: avoid "Use of uninitialized value in pattern match (m//)" + perl warnings when the accountd socket can't be reached. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/client b/client index bcdf6cd..e29d2a0 100755 --- a/client +++ b/client @@ -75,8 +75,11 @@ open (my $S, '+<&=', $1+0) or die "fdopen $1: $!"; # Read the protocol version and JSON Web Key (RFC 7517) from the # lacme-accountd socket # -die "Error: Invalid client version\n" unless - $S->getline() =~ /\A(\d+) OK(?:.*)\r\n\z/ and $1 == $PROTOCOL_VERSION; +do { + my $greeting = $S->getline(); + die "Error: Invalid client version\n" unless defined $greeting and + $greeting =~ /\A(\d+) OK(?:.*)\r\n\z/ and $1 == $PROTOCOL_VERSION; +}; my $JWK = JSON::->new->decode($S->getline()); my $KID; -- cgit v1.2.3 From 3a5c3f0596398d64bb34498f40becbcd32ffa5de Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 11:42:18 +0100 Subject: Consolidate error messages for consistency. --- Changelog | 1 + client | 8 ++-- lacme | 116 ++++++++++++++++++++++++++++----------------------------- lacme-accountd | 16 ++++---- 4 files changed, 71 insertions(+), 70 deletions(-) diff --git a/Changelog b/Changelog index ac8102c..cda155f 100644 --- a/Changelog +++ b/Changelog @@ -27,6 +27,7 @@ lacme (0.7.1) upstream; + Add support for TLS Feature extension from RFC 7633; this is mostly useful for OCSP Must-Staple. + client: use "lacme-client/$VERSION" as User-Agent header. + + Consolidate error messages for consistency. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/client b/client index e29d2a0..a5490f8 100755 --- a/client +++ b/client @@ -97,7 +97,7 @@ my $NONCE; my $CONFIG = do { my $conf = do { local $/ = undef; <$CONFFILE> }; - close $CONFFILE or die "Can't close: $!"; + close $CONFFILE or die "close: $!"; my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; $h->{_} //= {}; $h->{client}->{$_} //= $h->{_}->{$_} foreach keys %{$h->{_}}; # add defaults @@ -303,11 +303,11 @@ elsif ($COMMAND eq 'newOrder') { # serve $keyAuthorization at http://$domain/.well-known/acme-challenge/$challenge->{token} if (sysopen(my $fh, $challenge->{token}, O_CREAT|O_EXCL|O_WRONLY, 0644)) { $fh->print($keyAuthorization); - $fh->close() or die "Can't close: $!"; + $fh->close() or die "close: $!"; } elsif ($! == EEXIST) { print STDERR "WARNING: File exists: $challenge->{token}\n"; } else { - die "Can't open $challenge->{token}: $!"; + die "open($challenge->{token}): $!"; } my $r = acme($challenge->{url}, {}); request_json_decode($r); @@ -378,7 +378,7 @@ elsif ($COMMAND eq 'newOrder') { elsif ($COMMAND eq 'revokeCert') { die if @ARGV; my $der = do { local $/ = undef; }; - close STDIN or die "Can't close: $!"; + close STDIN or die "close: $!"; # send a KID if the request is signed with the acccount key, otherwise send a JWK set_kid(0); diff --git a/lacme b/lacme index 33f947c..f0beac1 100755 --- a/lacme +++ b/lacme @@ -159,7 +159,7 @@ sub gen_csr(%) { $config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; $config->print("tlsfeature = $args{tlsfeature}\n") if defined $args{tlsfeature}; - $config->close() or die "Can't close: $!"; + $config->close() or die "close: $!"; my @args = (qw/-new -batch -key/, $args{'certificate-key'}); push @args, "-$args{hash}" if defined $args{hash}; @@ -167,20 +167,20 @@ sub gen_csr(%) { open my $fh, '-|', qw/openssl req -outform DER/, @args or die "fork: $!"; my $csr = do { local $/ = undef; <$fh> }; - close $fh or $! ? die "Can't close: $!" : return; + close $fh or $! ? die "close: $!" : return; if ($OPTS{debug}) { # print out the CSR in text form pipe my $rd, my $wd or die "pipe: $!"; my $pid = fork // die "fork: $!"; unless ($pid) { - open STDIN, '<&', $rd or die "Can't dup: $!"; - open STDOUT, '>&', \*STDERR or die "Can't dup: $!"; + open STDIN, '<&', $rd or die "dup: $!"; + open STDOUT, '>&', \*STDERR or die "dup: $!"; exec qw/openssl req -noout -text -inform DER/ or die; } - $rd->close() or die "Can't close: $!"; + $rd->close() or die "close: $!"; $wd->print($csr); - $wd->close() or die "Can't close: $!"; + $wd->close() or die "close: $!"; waitpid $pid => 0; die $? if $? > 0; @@ -220,21 +220,21 @@ sub drop_privileges($$$) { # set effective and real gid; also set the list of supplementary gids to that single gid if ($group ne '') { - my $gid = getgrnam($group) // die "Can't getgrnam($group): $!"; + my $gid = getgrnam($group) // die "getgrnam($group): $!"; $) = "$gid $gid"; - die "Can't setgroups: $!" if $@; - POSIX::setgid($gid) or die "Can't setgid: $!"; + die "setgroups: $!" if $@; + POSIX::setgid($gid) or die "setgid: $!"; die "Couldn't setgid/setguid" unless $( eq "$gid $gid" and $) eq "$gid $gid"; # safety check } # set effective and real uid if ($user ne '') { - my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; - POSIX::setuid($uid) or die "Can't setuid: $!"; + my $uid = getpwnam($user) // die "getpwnam($user): $!"; + POSIX::setuid($uid) or die "setuid: $!"; die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check } - chdir $dir or die "Can't chdir to $dir: $!"; + chdir $dir or die "chdir($dir): $!"; } @@ -243,10 +243,10 @@ sub drop_privileges($$$) { # sub set_FD_CLOEXEC($$) { my ($fd, $set) = @_; - my $flags = fcntl($fd, F_GETFD, 0) or die "Can't fcntl F_GETFD: $!"; + my $flags = fcntl($fd, F_GETFD, 0) or die "fcntl F_GETFD: $!"; my $flags2 = $set ? ($flags | FD_CLOEXEC) : ($flags & ~FD_CLOEXEC); return if $flags == $flags2; - fcntl($fd, F_SETFD, $flags2) or die "Can't fcntl F_SETFD: $!"; + fcntl($fd, F_SETFD, $flags2) or die "fcntl F_SETFD: $!"; } @@ -321,10 +321,10 @@ sub spawn_webserver() { # create a temporary directory; give write access to the ACME client # and read access to the webserver my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; - chmod 0755, $tmpdir or die "Can't chmod: $!"; + chmod 0755, $tmpdir or die "chmod: $!"; if ((my $username = $CONFIG->{client}->{user}) ne '') { - my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; - chown($uid, -1, $tmpdir) or die "Can't chown: $!"; + my $uid = getpwnam($username) // die "getpwnam($username): $!"; + chown($uid, -1, $tmpdir) or die "chown: $!"; } # create socket(s) and spawn webserver(s) @@ -353,7 +353,7 @@ sub spawn_webserver() { bind($sock, $sockaddr) or die "Couldn't bind to $p: $!"; push @CLEANUP, sub() { print STDERR "Unlinking $path\n" if $OPTS{debug}; - unlink $path or warn "Warning: Can't unlink $path: $!"; + unlink $path or warn "Warning: Couldn't unlink $path: $!"; }; umask($umask) // die "umask: $!"; } @@ -428,8 +428,8 @@ sub iptables_save($@) { my $pid = fork() // die "fork: $!"; unless ($pid) { - open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; - open STDOUT, '>&', $iptables_tmp or die "Can't dup: $!"; + open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; + open STDOUT, '>&', $iptables_tmp or die "dup: $!"; $| = 1; # turn off buffering for STDOUT exec "/usr/sbin/$iptables_bin-save", "-c" or die; } @@ -440,14 +440,14 @@ sub iptables_save($@) { # handle and not from the file. XXX if there was a way in Perl to # use open(2) with the O_TMPFILE flag we would use that to avoid # creating a file to start with - seek($iptables_tmp, SEEK_SET, 0) or die "Can't seek: $!"; + seek($iptables_tmp, SEEK_SET, 0) or die "seek: $!"; push @CLEANUP, sub() { print STDERR "[$$] Restoring $iptables_bin\n" if $OPTS{debug}; my $pid = fork() // die "fork: $!"; unless ($pid) { - open STDIN, '<&', $iptables_tmp or die "Can't dup: $!"; - open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; + open STDIN, '<&', $iptables_tmp or die "dup: $!"; + open STDOUT, '>', '/dev/null' or die "open(/dev/null): $!"; exec "/usr/sbin/$iptables_bin-restore", "-c" or die; } waitpid $pid => 0; @@ -496,7 +496,7 @@ sub acme_client($@) { unless ($pid) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); set_FD_CLOEXEC($s, 0); - $client->close() or die "Can't close: $!"; + $client->close() or die "close: $!"; my @cmd = ($accountd->{command}, '--conn-fd='.fileno($s)); push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; @@ -505,7 +505,7 @@ sub acme_client($@) { exec { $cmd[0] } @cmd or die; } print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; - $s->close() or die "Can't close: $!"; + $s->close() or die "close: $!"; $cleanup = sub() { print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug}; shutdown($client, SHUT_RDWR) or warn "shutdown: $!"; @@ -520,11 +520,11 @@ sub acme_client($@) { # ensure we're the only user with write access to the parent dir my $dirname = $sockname =~ s/[^\/]+$//r; - @stat = stat($dirname) or die "Can't stat $dirname: $!"; + @stat = stat($dirname) or die "stat($dirname): $!"; 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"; + @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; # connect(2) to the socket @@ -543,7 +543,7 @@ sub acme_client($@) { my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); - seek($CONFFILE, SEEK_SET, 0) or die "Can't seek: $!"; + seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; $ENV{DEBUG} = $OPTS{debug}; }}, $conf->{command}, $COMMAND, @fileno, @args); @@ -572,18 +572,18 @@ sub spawn($@) { # child $args->{child}->() if defined $args->{child}; if (defined $args->{in}) { - close $in_wd or die "Can't close: $!"; - open STDIN, '<&', $in_rd or die "Can't dup: $!"; + close $in_wd or die "close: $!"; + open STDIN, '<&', $in_rd or die "dup: $!"; } else { - open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; + open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; } if (!defined $args->{out}) { - open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; + open STDOUT, '>', '/dev/null' or die "open(/dev/null): $!"; } elsif (ref $args->{out} ne 'GLOB') { - close $out_rd or die "Can't close: $!"; - open STDOUT, '>&', $out_wd or die "Can't dup: $!"; + close $out_rd or die "close: $!"; + open STDOUT, '>&', $out_wd or die "dup: $!"; } elsif (fileno(STDOUT) != fileno($args->{out})) { - open STDOUT, '>&', $args->{out} or die "Can't dup: $!"; + open STDOUT, '>&', $args->{out} or die "dup: $!"; } exec { $exec[0] } @exec or die; } @@ -595,18 +595,18 @@ sub spawn($@) { # parent print STDERR "[$$] Forking $exec[0], child PID $pid\n" if $OPTS{debug}; if (defined $args->{in}) { - $in_rd->close() or die "Can't close: $!"; + $in_rd->close() or die "close: $!"; $in_wd->print($args->{in}); - $in_wd->close() or die "Can't close: $!"; + $in_wd->close() or die "close: $!"; } if (defined $args->{out} and ref $args->{out} ne 'GLOB') { - $out_wd->close() or die "Can't close: $!"; + $out_wd->close() or die "close: $!"; if (ref $args->{out} eq 'CODE') { $args->{out}->($out_rd); } elsif (ref $args->{out} eq 'SCALAR') { ${$args->{out}} = do { local $/ = undef; $out_rd->getline() }; } - $out_rd->close() or die "Can't close: $!"; + $out_rd->close() or die "close: $!"; } waitpid $pid => 0; pop @CLEANUP; @@ -631,31 +631,31 @@ sub install_cert($$;$) { chmod(0644 &~ $umask, $fh) or die "chmod: $!"; if ($leafonly) { # keep only the leaf certificate - pipe my $rd, my $wd or die "Can't pipe: $!"; - my $pid = fork // die "Can't fork: $!"; + pipe my $rd, my $wd or die "pipe: $!"; + my $pid = fork // die "fork: $!"; unless ($pid) { - open STDIN, '<&', $rd or die "Can't dup: $!"; - open STDOUT, '>&', $fh or die "Can't dup: $!"; + open STDIN, '<&', $rd or die "dup: $!"; + open STDOUT, '>&', $fh or die "dup: $!"; exec qw/openssl x509 -outform PEM/ or die; } - $rd->close() or die "Can't close: $!"; + $rd->close() or die "close: $!"; $wd->print($chain); - $wd->close() or die "Can't close: $!"; + $wd->close() or die "close: $!"; waitpid $pid => 0; die $? if $? > 0; } else { - $fh->print($chain) or die "Can't print: $!"; + $fh->print($chain) or die "print: $!"; } - $fh->close() or die "Can't close: $!"; + $fh->close() or die "close: $!"; }; my $path = $fh->filename(); if ($@) { print STDERR "Unlinking $path\n" if $OPTS{debug}; - unlink $path or warn "Can't unlink $path: $!"; + unlink $path or warn "unlink($path): $!"; die $@; } - rename($path, $filename) or die "Can't rename $path to $filename: $!"; + rename($path, $filename) or die "rename($path, $filename): $!"; } @@ -687,7 +687,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { unless ($conffile =~ s#/\z## or -d $conffile) { @filenames = ($conffile); } else { - opendir my $dh, $conffile or die "Can't opendir $conffile: $!\n"; + opendir my $dh, $conffile or die "opendir($conffile): $!\n"; while (readdir $dh) { if (/\.conf\z/) { push @filenames, "$conffile/$_"; @@ -723,7 +723,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { if ($OPTS{debug}) { print STDERR "Configuration option for $s:\n"; - print " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); + print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); } my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; @@ -809,16 +809,16 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { if (defined $conf->{chown}) { my ($user, $group) = split /:/, $conf->{chown}, 2; - my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; - my $gid = defined $group ? (getgrnam($group) // die "Can't getgrnam($group): $!") : -1; + my $uid = getpwnam($user) // die "getpwnam($user): $!"; + my $gid = defined $group ? (getgrnam($group) // die "getgrnam($group): $!") : -1; foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chown($uid, $gid, $_) or die "Can't chown: $!"; + chown($uid, $gid, $_) or die "chown: $!"; } } if (defined $conf->{chmod}) { my $mode = oct($conf->{chmod}) // die; foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chmod($mode, $_) or die "Can't chown: $!"; + chmod($mode, $_) or die "chown: $!"; } } @@ -827,7 +827,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { or die "fork: $!"; print $fh $x509; close $fh or die $! ? - "Can't close: $!" : + "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; if (defined $conf->{notify}) { @@ -857,7 +857,7 @@ elsif ($COMMAND eq 'revokeCert' or $COMMAND eq 'revoke-cert') { open my $fh, '-|', qw/openssl x509 -outform DER -in/, $filename or die "fork: $!"; my $der = do { local $/ = undef; <$fh> }; close $fh or die $! ? - "Can't close: $!" : + "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump no_extensions/; @@ -865,7 +865,7 @@ elsif ($COMMAND eq 'revokeCert' or $COMMAND eq 'revoke-cert') { or die "fork: $!"; print $fh2 $der; close $fh2 or die $! ? - "Can't close: $!" : + "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; if (acme_client({in => $der})) { diff --git a/lacme-accountd b/lacme-accountd index 36e9d9f..c00530f 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -106,7 +106,7 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { my $str = do {local $/ = undef; <$fh>}; close $fh or die $! ? - "Can't close: $!" : + "close: $!" : "Error: $command[0] exited with value ".($? >> 8)."\n"; require 'Crypt/OpenSSL/RSA.pm'; @@ -140,7 +140,7 @@ my $JWK_STR = JSON::->new->encode($JWK); if (defined $OPTS{'conn-fd'}) { die "Invalid file descriptor" unless $OPTS{'conn-fd'} =~ /\A(\d+)\z/; # untaint and fdopen(3) our end of the socket pair - open ($S, '+<&=', $1+0) or die "fdopen $1: $!"; + open ($S, '+<&=', $1+0) or die "fdopen($1): $!"; } else { my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef); die "Missing socket option\n" unless defined $sockname; @@ -148,7 +148,7 @@ if (defined $OPTS{'conn-fd'}) { # ensure we're the only user with write access to the parent dir my $dirname = $sockname =~ s/[^\/]+$//r; - my @stat = stat($dirname) or die "Can't stat $dirname: $!"; + my @stat = stat($dirname) or die "stat($dirname): $!"; die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; my $umask = umask(0177) // die "umask: $!"; @@ -172,14 +172,14 @@ if (defined $OPTS{'conn-fd'}) { sub conn($;$) { my $conn = shift; my $count = shift; - $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ); + $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ) or warn "print: $!"; # sign whatever comes in while (defined (my $data = $conn->getline())) { $data =~ s/\r\n\z// or die; print STDERR "[$count] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; my $sig = $SIGN->($data); - $conn->printflush( encode_base64url($sig), "\r\n" ); + $conn->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; } } @@ -195,7 +195,7 @@ if (defined $OPTS{'conn-fd'}) { print STDERR "[$count] >>> Accepted new connection\n" unless $OPTS{quiet}; conn($conn, $count); print STDERR "[$count] >>> Connection terminated\n" unless $OPTS{quiet}; - close $conn or warn "Can't close: $!"; + $conn->close() or warn "close: $!"; } } @@ -205,11 +205,11 @@ if (defined $OPTS{'conn-fd'}) { END { if (defined $SOCKNAME and -S $SOCKNAME) { print STDERR "Unlinking $SOCKNAME\n" if $OPTS{debug}; - unlink $SOCKNAME or print STDERR "Can't unlink $SOCKNAME: $!\n"; + unlink $SOCKNAME or print STDERR "Couldn't unlink $SOCKNAME: $!\n"; } 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 "Can't close: $!\n"; + close $S or print STDERR "close: $!\n"; } } -- cgit v1.2.3 From 4886d0dd6c77d029209cc09a9e15a89ffb23b9fc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 19:03:00 +0100 Subject: Sanitize environment when spawning children. Set $HOME, $USER, $SHELL, $PATH, $LOGNAME to appropriate values (and perserve $TERM), which matches the login(1) behavior. --- Changelog | 2 ++ lacme | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Changelog b/Changelog index cda155f..019c076 100644 --- a/Changelog +++ b/Changelog @@ -28,6 +28,8 @@ lacme (0.7.1) upstream; useful for OCSP Must-Staple. + client: use "lacme-client/$VERSION" as User-Agent header. + Consolidate error messages for consistency. + + Sanitize environment when spawning the lacme client, webserver and + accountd. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/lacme b/lacme index f0beac1..a5ba9f4 100755 --- a/lacme +++ b/lacme @@ -234,6 +234,13 @@ sub drop_privileges($$$) { die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check } + # sanitize environment + my $term = $ENV{TERM}; + my @ent = getpwuid($>) or die "getpwuid($>): $!"; + %ENV = ( USER => $ent[0], LOGNAME => $ent[0], HOME => $ent[7], SHELL => $ent[8] ); + $ENV{PATH} = $> == 0 ? "/usr/sbin:/usr/bin:/sbin:/bin" : "/usr/bin:/bin"; + $ENV{TERM} = $term if defined $term; # preserve $TERM + chdir $dir or die "chdir($dir): $!"; } -- cgit v1.2.3 From d6f008f8a3a9724e5acbcb8b9df29304279f4106 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 20:54:19 +0100 Subject: Split Nginx and Apapche2 static configuration snippets into seperate files. That way users prefering that over reverse-proxying can just source/enable the relevant files without having to uncomment anything. --- Changelog | 4 ++++ snippets/apache2-static.conf | 16 ++++++++++++++++ snippets/apache2.conf | 19 +------------------ snippets/nginx-static.conf | 15 +++++++++++++++ snippets/nginx.conf | 19 ++++--------------- 5 files changed, 40 insertions(+), 33 deletions(-) create mode 100644 snippets/apache2-static.conf create mode 100644 snippets/nginx-static.conf diff --git a/Changelog b/Changelog index 019c076..35503e9 100644 --- a/Changelog +++ b/Changelog @@ -24,6 +24,10 @@ lacme (0.7.1) upstream; configuration 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 + files. That way users prefering that over reverse-proxying can just + source/enable the relevant files without having to uncomment + anything. + Add support for TLS Feature extension from RFC 7633; this is mostly useful for OCSP Must-Staple. + client: use "lacme-client/$VERSION" as User-Agent header. diff --git a/snippets/apache2-static.conf b/snippets/apache2-static.conf new file mode 100644 index 0000000..9262179 --- /dev/null +++ b/snippets/apache2-static.conf @@ -0,0 +1,16 @@ +# Use Apache2 to serve ACME requests directly. +# This snippet requires setting challenge-directory = /var/www/acme-challenge +# in /etc/lacme/lacme.config, and creating this file with write +# permissions for the lacme client user. +# +# This file needs to be sourced to the server directives (at least the +# non-ssl one) of each virtual host requiring authorization. + + + Alias /.well-known/acme-challenge/ /var/www/acme-challenge/ + + Options none + AllowOverride none + Require all granted + + diff --git a/snippets/apache2.conf b/snippets/apache2.conf index 69d80a7..31dd95a 100644 --- a/snippets/apache2.conf +++ b/snippets/apache2.conf @@ -1,29 +1,12 @@ -# Use Apache2 to serve ACME requests; either directly, or by passing -# them over to a locally-bound lacme webserver component. +# Use Apache2 to proxy ACME requests to a locally-bound lacme webserver. # # This file needs to be sourced to the server directives (at least the # non-ssl one) of each virtual host requiring authorization. # Alternatively, run `a2enconf lacme` and reload apache2. - -# Pass ACME requests to lacme's webserver component ProxyPass unix://@@runstatedir@@/lacme-www.socket|http://localhost/.well-known/acme-challenge/ Require all granted - - -## Alternatively, you can let Apache2 serve the requests by -## setting 'challenge-directory' to '/var/www/acme-challenge' in -## lacme's configuration file and uncomment the following: - -# -# Alias /.well-known/acme-challenge/ /var/www/acme-challenge/ -# -# Options none -# AllowOverride none -# Require all granted -# -# diff --git a/snippets/nginx-static.conf b/snippets/nginx-static.conf new file mode 100644 index 0000000..febe4dc --- /dev/null +++ b/snippets/nginx-static.conf @@ -0,0 +1,15 @@ +# Use Nginx to serve ACME requests directly. +# This snippet requires setting challenge-directory = /var/www/acme-challenge +# in /etc/lacme/lacme.config, and creating this file with write +# permissions for the lacme client user. +# +# One of the nginx*.conf file needs to be sourced to the server +# directives (at least the non-ssl one) of each virtual host requiring +# authorization. + +location ^~ /.well-known/acme-challenge/ { + alias /var/www/acme-challenge/; + default_type application/jose+json; + disable_symlinks on; + autoindex off; +} diff --git a/snippets/nginx.conf b/snippets/nginx.conf index 76309f0..891a834 100644 --- a/snippets/nginx.conf +++ b/snippets/nginx.conf @@ -1,20 +1,9 @@ -# Use Nginx to serve ACME requests; either directly, or by passing them -# over to a locally-bound lacme webserver component. +# Use Nginx to proxy ACME requests to a locally-bound lacme webserver. # -# This file needs to be sourced to the server directives (at least the -# non-ssl one) of each virtual host requiring authorization. +# One of the nginx*.conf file needs to be sourced to the server +# directives (at least the non-ssl one) of each virtual host requiring +# authorization. location ^~ /.well-known/acme-challenge/ { - # Pass ACME requests to lacme's webserver component proxy_pass http://unix:@@runstatedir@@/lacme-www.socket; - - - ## Alternatively, you can let nginx serve the requests by - ## setting 'challenge-directory' to '/var/www/acme-challenge' in - ## lacme's configuration file and uncomment the following: - - # alias /var/www/acme-challenge/; - # default_type application/jose+json; - # disable_symlinks on; - # autoindex off; } -- cgit v1.2.3 From 044a4cb8b4ba06c6355c5e9978cd5dbfe9df94b2 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:15:03 +0100 Subject: webserver: reopen stdin from /dev/null. Having both lacme(8) and its webserver component reading from the same standard input could yield starvation. --- Changelog | 1 + lacme | 1 + 2 files changed, 2 insertions(+) diff --git a/Changelog b/Changelog index 35503e9..c476ee1 100644 --- a/Changelog +++ b/Changelog @@ -51,6 +51,7 @@ lacme (0.7.1) upstream; $(sbindir)/lacme. - client: avoid "Use of uninitialized value in pattern match (m//)" perl warnings when the accountd socket can't be reached. + - webserver: reopen stdin from /dev/null. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/lacme b/lacme index a5ba9f4..e5f8715 100755 --- a/lacme +++ b/lacme @@ -374,6 +374,7 @@ sub spawn_webserver() { my $pid = fork() // "fork: $!"; unless ($pid) { drop_privileges($conf->{user}, $conf->{group}, $tmpdir); + open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; set_FD_CLOEXEC($sock, 0); $ENV{DEBUG} = $OPTS{debug}; # use execve(2) rather than a Perl pseudo-process to ensure that -- cgit v1.2.3 From 2e455335a9e8aa9aaace98bc4d61f53a2c93b930 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:19:51 +0100 Subject: Use 'acme-challenge.XXXXXXXXXX' as template for the temporary ACME challenge directory. --- Changelog | 2 ++ lacme | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index c476ee1..5dd416a 100644 --- a/Changelog +++ b/Changelog @@ -52,6 +52,8 @@ lacme (0.7.1) upstream; - client: avoid "Use of uninitialized value in pattern match (m//)" perl warnings when the accountd socket can't be reached. - webserver: reopen stdin from /dev/null. + - Use 'acme-challenge.XXXXXXXXXX' as template for the temporary ACME + challenge directory. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/lacme b/lacme index e5f8715..354a4a0 100755 --- a/lacme +++ b/lacme @@ -327,7 +327,7 @@ sub spawn_webserver() { # create a temporary directory; give write access to the ACME client # and read access to the webserver - my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; + my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1, TEMPLATE => "acme-challenge.XXXXXXXXXX") // die; chmod 0755, $tmpdir or die "chmod: $!"; if ((my $username = $CONFIG->{client}->{user}) ne '') { my $uid = getpwnam($username) // die "getpwnam($username): $!"; -- cgit v1.2.3 From 32c27cecbe7ab3bdf0cbc984c50b37fbe231e79d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:34:08 +0100 Subject: Set the DEBUG environment variable to 0/1 instead of ""/1. --- Changelog | 1 + lacme | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 5dd416a..0f9b6ca 100644 --- a/Changelog +++ b/Changelog @@ -54,6 +54,7 @@ lacme (0.7.1) upstream; - webserver: reopen stdin from /dev/null. - Use 'acme-challenge.XXXXXXXXXX' as template for the temporary ACME challenge directory. + - Set the DEBUG environment variable to 0/1 instead of ""/1. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/lacme b/lacme index 354a4a0..d2d8840 100755 --- a/lacme +++ b/lacme @@ -376,7 +376,7 @@ sub spawn_webserver() { drop_privileges($conf->{user}, $conf->{group}, $tmpdir); open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; set_FD_CLOEXEC($sock, 0); - $ENV{DEBUG} = $OPTS{debug}; + $ENV{DEBUG} = $OPTS{debug} // 0; # use execve(2) rather than a Perl pseudo-process to ensure that # the child doesn't have access to the parent's memory exec $conf->{command}, fileno($sock) or die; @@ -552,7 +552,7 @@ sub acme_client($@) { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; - $ENV{DEBUG} = $OPTS{debug}; + $ENV{DEBUG} = $OPTS{debug} // 0; }}, $conf->{command}, $COMMAND, @fileno, @args); if (defined $cleanup) { -- cgit v1.2.3 From d72df441f86f759bf143df745ff13fd9b90597bf Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:53:31 +0100 Subject: Split client/webserver/accountd commands on whitespace. This doesn't change the default behavior. --- Changelog | 2 ++ config/lacme.conf | 6 +++--- lacme | 25 ++++++++++++++----------- lacme.8.md | 20 +++++++++++++++----- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/Changelog b/Changelog index 0f9b6ca..68a1f83 100644 --- a/Changelog +++ b/Changelog @@ -22,6 +22,8 @@ lacme (0.7.1) upstream; * Breaking change: lacme(8) resp. lacme-accountd(1) no longer consider ./lacme.conf resp. ./lacme-accountd.conf as default location for the configuration file. + * The client, webserver, and accountd commands are now split on + whitespace. This doesn't change the default behavior. + 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/config/lacme.conf b/config/lacme.conf index 4c7dc86..3faed2b 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -29,7 +29,7 @@ # #group = @@lacme_client_group@@ -# Path to the ACME client executable. +# ACME client command. # #command = @@libexecdir@@/lacme/client @@ -83,7 +83,7 @@ # #group = @@lacme_www_group@@ -# Path to the ACME webserver executable. +# ACME webserver command. # #command = @@libexecdir@@/lacme/webserver @@ -110,7 +110,7 @@ # #group = -# Path to the lacme-accountd(1) executable. +# lacme-accountd(1) command. # #command = @@bindir@@/lacme-accountd diff --git a/lacme b/lacme index d2d8840..019a5e7 100755 --- a/lacme +++ b/lacme @@ -379,7 +379,8 @@ sub spawn_webserver() { $ENV{DEBUG} = $OPTS{debug} // 0; # use execve(2) rather than a Perl pseudo-process to ensure that # the child doesn't have access to the parent's memory - exec $conf->{command}, fileno($sock) or die; + my ($cmd, @args) = split(/\s+/, $conf->{command}) or die "Empty webserver command\n"; + exec { $cmd } $cmd, @args, fileno($sock) or die; } print STDERR "[$$] Forking ACME webserver bound to $p, child PID $pid\n" if $OPTS{debug}; @@ -505,12 +506,13 @@ sub acme_client($@) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); set_FD_CLOEXEC($s, 0); $client->close() or die "close: $!"; - my @cmd = ($accountd->{command}, '--conn-fd='.fileno($s)); - push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; - push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; - push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no'; - push @cmd, '--debug' if $OPTS{debug}; - exec { $cmd[0] } @cmd or die; + my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; + push @args, '--conn-fd='.fileno($s); + push @args, '--config='.$accountd->{config} if defined $accountd->{config}; + push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; + push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; + push @args, '--debug' if $OPTS{debug}; + exec { $cmd } $cmd, @args or die; } print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; $s->close() or die "close: $!"; @@ -546,6 +548,7 @@ sub acme_client($@) { # use execve(2) rather than a Perl pseudo-process to ensure that the # child doesn't have access to the parent's memory + my ($cmd, @args2) = split(/\s+/, $conf->{command}) or die "Empty client command\n"; my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno set_FD_CLOEXEC($client, 1); my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() { @@ -553,7 +556,7 @@ sub acme_client($@) { set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; $ENV{DEBUG} = $OPTS{debug} // 0; - }}, $conf->{command}, $COMMAND, @fileno, @args); + }}, $cmd, @args2, $COMMAND, @fileno, @args); if (defined $cleanup) { @CLEANUP = grep { $_ ne $cleanup } @CLEANUP; @@ -564,7 +567,7 @@ sub acme_client($@) { sub spawn($@) { my $args = shift; - my @exec = @_; + my ($cmd, @args) = @_; # create communication pipes if needed my ($in_rd, $in_wd, $out_rd, $out_wd); @@ -593,7 +596,7 @@ sub spawn($@) { } elsif (fileno(STDOUT) != fileno($args->{out})) { open STDOUT, '>&', $args->{out} or die "dup: $!"; } - exec { $exec[0] } @exec or die; + exec { $cmd } $cmd, @args or die; } push @CLEANUP, sub() { kill 15 => $pid; @@ -601,7 +604,7 @@ sub spawn($@) { }; # parent - print STDERR "[$$] Forking $exec[0], child PID $pid\n" if $OPTS{debug}; + print STDERR "[$$] Forking $cmd, child PID $pid\n" if $OPTS{debug}; if (defined $args->{in}) { $in_rd->close() or die "close: $!"; $in_wd->print($args->{in}); diff --git a/lacme.8.md b/lacme.8.md index bc711ed..69870c4 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -183,7 +183,10 @@ of [ACME] commands and dialogues with the remote [ACME] server). *command* -: Path to the [ACME] client executable. +: The [ACME] client command. It is split on whitespace, with the + first item being the command to execute, the second its first + argument etc. (Note that `lacme` might append more arguments when + executing the command internally.) Default: `@@libexecdir@@/lacme/client`. *server* @@ -262,9 +265,13 @@ served during certificate issuance. *command* -: Path to the [ACME] webserver executable. A separate process is - spawned for each address to *listen* on. (In particular no - webserver process is forked when the *listen* option is empty.) +: The [ACME] webserver command. It is split on whitespace, with the + first item being the command to execute, the second its first + argument etc. (Note that `lacme` might append more arguments when + executing the command internally.) + A separate process is spawned for each address to *listen* on. (In + particular no webserver process is forked when the *listen* option + is empty.) Default: `@@libexecdir@@/lacme/webserver`. *iptables* @@ -297,7 +304,10 @@ UNIX-domain socket. *command* -: Path to the [`lacme-accountd`(1)] executable. +: The [`lacme-accountd`(1)] command. It is split on whitespace, with + 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.) Default: `@@bindir@@/lacme-accountd`. *config* -- cgit v1.2.3 From 42a8f9813716ed3495b6f49edea429b127eef0f0 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 00:49:46 +0100 Subject: accountd: replace internal option --conn-fd=FD with flag --stdio. Using stdin/stdout makes it possible to tunnel the accountd connection through ssh. --- Changelog | 7 ++++++- lacme | 6 ++++-- lacme-accountd | 27 +++++++++++---------------- lacme.8.md | 4 +++- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Changelog b/Changelog index 68a1f83..4d18b38 100644 --- a/Changelog +++ b/Changelog @@ -23,7 +23,9 @@ lacme (0.7.1) upstream; ./lacme.conf resp. ./lacme-accountd.conf as default location for the configuration file. * The client, webserver, and accountd commands are now split on - whitespace. This doesn't change the default behavior. + whitespace. This doesn't change the default behavior but allows + using `ssh -T lacme@account.example.net lacme-accountd` to spawn a + remote lacme-accountd server for instance. + 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 @@ -36,6 +38,9 @@ lacme (0.7.1) upstream; + Consolidate error messages for consistency. + Sanitize environment when spawning the lacme client, webserver and accountd. + + accountd: replace internal option --conn-fd=FD with flag --stdio. + Using stdin/stdout makes it possible to tunnel the accountd + connection through ssh. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/lacme b/lacme index 019a5e7..3d3657f 100755 --- a/lacme +++ b/lacme @@ -504,10 +504,12 @@ sub acme_client($@) { my $pid = fork() // "fork: $!"; unless ($pid) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); - set_FD_CLOEXEC($s, 0); + set_FD_CLOEXEC($s, 1); $client->close() or die "close: $!"; + open STDIN, '<&', $s or die "dup: $!"; + open STDOUT, '>&', $s or die "dup: $!"; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; - push @args, '--conn-fd='.fileno($s); + push @args, '--stdio'; push @args, '--config='.$accountd->{config} if defined $accountd->{config}; push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; diff --git a/lacme-accountd b/lacme-accountd index c00530f..7b9b1ff 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -60,7 +60,7 @@ sub usage(;$$) { } exit $rv; } -usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s conn-fd=i quiet|q debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s stdio quiet|q debug help|h/); usage(0) if $OPTS{help}; do { @@ -137,11 +137,7 @@ my $JWK_STR = JSON::->new->encode($JWK); # to support the abstract namespace.) The downside is that we have to # delete the file manually. # -if (defined $OPTS{'conn-fd'}) { - die "Invalid file descriptor" unless $OPTS{'conn-fd'} =~ /\A(\d+)\z/; - # untaint and fdopen(3) our end of the socket pair - open ($S, '+<&=', $1+0) or die "fdopen($1): $!"; -} else { +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; $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname @@ -169,22 +165,21 @@ if (defined $OPTS{'conn-fd'}) { # For each new connection, send the protocol version and the account key's # public parameters, then sign whatever comes in # -sub conn($;$) { - my $conn = shift; - my $count = shift; - $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ) or warn "print: $!"; +sub conn($$;$) { + my ($in, $out, $id) = @_; + $out->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ) or warn "print: $!"; # sign whatever comes in - while (defined (my $data = $conn->getline())) { + while (defined (my $data = $in->getline())) { $data =~ s/\r\n\z// or die; - print STDERR "[$count] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; + print STDERR "[$id] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; my $sig = $SIGN->($data); - $conn->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; + $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; } } -if (defined $OPTS{'conn-fd'}) { - conn($S, $$); +if (defined $OPTS{stdio}) { + conn(\*STDIN, \*STDOUT, $$); } else { $SIG{PIPE} = 'IGNORE'; # ignore broken pipes for (my $count = 0;; $count++) { @@ -193,7 +188,7 @@ if (defined $OPTS{'conn-fd'}) { die "accept: $!"; }; print STDERR "[$count] >>> Accepted new connection\n" unless $OPTS{quiet}; - conn($conn, $count); + conn($conn, $conn, $count); print STDERR "[$count] >>> Connection terminated\n" unless $OPTS{quiet}; $conn->close() or warn "close: $!"; } diff --git a/lacme.8.md b/lacme.8.md index 69870c4..c354c1a 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -308,7 +308,9 @@ 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.) - 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`. *config* -- cgit v1.2.3 From baa7c25db322a9472c9155422057ec56aa93f439 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 00:06:49 +0100 Subject: Use File::Basename::dirname(). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To correctly extract the parent directory of the socket path. The previous returned an empty string when the socket path didn't contain ‘/’. --- Changelog | 2 ++ INSTALL | 2 ++ lacme | 7 ++++--- lacme-accountd | 5 +++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Changelog b/Changelog index 4d18b38..5bca11e 100644 --- a/Changelog +++ b/Changelog @@ -62,6 +62,8 @@ lacme (0.7.1) upstream; - Use 'acme-challenge.XXXXXXXXXX' as template for the temporary ACME challenge directory. - Set the DEBUG environment variable to 0/1 instead of ""/1. + - Use File::Basename::dirname() to correctly extract the parent + directory of the socket path. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/INSTALL b/INSTALL index 9ecb1bf..cb8d57f 100644 --- a/INSTALL +++ b/INSTALL @@ -4,6 +4,7 @@ lacme-accountd depends on the following Perl modules: - Crypt::OpenSSL::RSA (for PEM-encoded key material) - Crypt::OpenSSL::Bignum (for PEM-encoded key material) - Errno (core module) + - File::Basename (core module) - Getopt::Long (core module) - JSON (optionally C/XS-accelerated with JSON::XS) - List::Util (core module) @@ -23,6 +24,7 @@ lacme depends on OpenSSL ≥1.1.0 and the following Perl modules: - Date::Parse - Errno (core module) - Fcntl (core module) + - File::Basename (core module) - File::Temp (core module) - Getopt::Long (core module) - JSON (optionally C/XS-accelerated with JSON::XS) diff --git a/lacme b/lacme index 3d3657f..2f239e2 100755 --- a/lacme +++ b/lacme @@ -27,6 +27,7 @@ my $NAME = 'lacme'; use Errno 'EINTR'; use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC O_CREAT O_EXCL O_WRONLY SEEK_SET/; +use File::Basename 'dirname'; use File::Temp (); use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; @@ -531,8 +532,8 @@ sub acme_client($@) { $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 - my $dirname = $sockname =~ s/[^\/]+$//r; - @stat = stat($dirname) or die "stat($dirname): $!"; + my $dirname = dirname($sockname); + @stat = stat($dirname) or die "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 @@ -695,7 +696,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { : [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ]; my ($conf, %defaults); foreach my $conffile (@$conffiles) { - $conffile = ($CONFFILENAME =~ s#[^/]+\z##r).$conffile unless $conffile =~ /\A\//; + $conffile = dirname($CONFFILENAME) .'/'. $conffile unless $conffile =~ /\A\//; my @filenames; unless ($conffile =~ s#/\z## or -d $conffile) { @filenames = ($conffile); diff --git a/lacme-accountd b/lacme-accountd index 7b9b1ff..1dc5f03 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -28,6 +28,7 @@ my $PROTOCOL_VERSION = 1; 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'; @@ -143,8 +144,8 @@ unless (defined $OPTS{stdio}) { $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 - my $dirname = $sockname =~ s/[^\/]+$//r; - my @stat = stat($dirname) or die "stat($dirname): $!"; + 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 $umask = umask(0177) // die "umask: $!"; -- cgit v1.2.3 From c214f20a835d0da4bd0c5a85a4bd9089fc4febcb Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 23:15:52 +0100 Subject: Update staging hierarchy. Cf. https://community.letsencrypt.org/t/staging-hierarchy-new-root-cert/145677 . --- Makefile | 5 +++-- certs-staging/fakeleintermediatex1.pem | 28 --------------------------- certs-staging/fakelerootx1.pem | 29 ---------------------------- certs-staging/letsencrypt-stg-int-e1.pem | 19 ++++++++++++++++++ certs-staging/letsencrypt-stg-int-e2.pem | 19 ++++++++++++++++++ certs-staging/letsencrypt-stg-int-r3.pem | 31 ++++++++++++++++++++++++++++++ certs-staging/letsencrypt-stg-int-r4.pem | 31 ++++++++++++++++++++++++++++++ certs-staging/letsencrypt-stg-root-x1.pem | 32 +++++++++++++++++++++++++++++++ certs-staging/letsencrypt-stg-root-x2.pem | 15 +++++++++++++++ 9 files changed, 150 insertions(+), 59 deletions(-) delete mode 100644 certs-staging/fakeleintermediatex1.pem delete mode 100644 certs-staging/fakelerootx1.pem create mode 100644 certs-staging/letsencrypt-stg-int-e1.pem create mode 100644 certs-staging/letsencrypt-stg-int-e2.pem create mode 100644 certs-staging/letsencrypt-stg-int-r3.pem create mode 100644 certs-staging/letsencrypt-stg-int-r4.pem create mode 100644 certs-staging/letsencrypt-stg-root-x1.pem create mode 100644 certs-staging/letsencrypt-stg-root-x2.pem diff --git a/Makefile b/Makefile index 06e8cb2..3ad440c 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,9 @@ $(BUILDDIR)/certs/ca-certificates.crt: \ # Staging Environment for tests, see https://letsencrypt.org/docs/staging-environment/ $(BUILDDIR)/certs-staging/ca-certificates.crt: \ - certs-staging/fakelerootx1.pem \ - certs-staging/fakeleintermediatex1.pem + certs-staging/letsencrypt-stg-root-x[12].pem \ + certs-staging/letsencrypt-stg-int-r[34].pem \ + certs-staging/letsencrypt-stg-int-e[12].pem mkdir -pv -- $(dir $@) cat -- $^ >$@ diff --git a/certs-staging/fakeleintermediatex1.pem b/certs-staging/fakeleintermediatex1.pem deleted file mode 100644 index 2c09b96..0000000 --- a/certs-staging/fakeleintermediatex1.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw -GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 -MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 -8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym -oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 -ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN -xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 -dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 -AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw -HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 -BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu -b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu -Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq -hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF -UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 -AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp -DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 -IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf -zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI -PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w -SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em -2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 -WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt -n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= ------END CERTIFICATE----- - diff --git a/certs-staging/fakelerootx1.pem b/certs-staging/fakelerootx1.pem deleted file mode 100644 index 8c1c4b1..0000000 --- a/certs-staging/fakelerootx1.pem +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw -GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2 -MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq -hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ -diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP -xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG -TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj -EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd -O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa -aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0 -A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr -IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe -Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb -Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50 -qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB -Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA -A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln -uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H -sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm -dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd -oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV -/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ -zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc -VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1 -Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4 -8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c -idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng== ------END CERTIFICATE----- diff --git a/certs-staging/letsencrypt-stg-int-e1.pem b/certs-staging/letsencrypt-stg-int-e1.pem new file mode 100644 index 0000000..2d6290c --- /dev/null +++ b/certs-staging/letsencrypt-stg-int-e1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCApGgAwIBAgIRALRY4992FVxZJKOJ3bpffWIwCgYIKoZIzj0EAwMwaDEL +MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0 +eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj +b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTI1MDkxNTE2MDAwMFowVTELMAkGA1UE +BhMCVVMxIDAeBgNVBAoTFyhTVEFHSU5HKSBMZXQncyBFbmNyeXB0MSQwIgYDVQQD +ExsoU1RBR0lORykgRXJzYXR6IEVkYW1hbWUgRTEwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAT9v/PJUtHOTk28nXCXrpP665vI4Z094h8o7R+5E6yNajZa0UubqjpZFoGq +u785/vGXj6mdfIzc9boITGusZCSWeMj5ySMZGZkS+VSvf8VQqj+3YdEu4PLZEjBA +ivRFpEejggEQMIIBDDAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUH +AwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFOv5JcKA +KGbibQiSMvPC4a3D/zVFMB8GA1UdIwQYMBaAFN7Ro1lkDsGaNqNG7rAQdu+ul5Vm +MDYGCCsGAQUFBwEBBCowKDAmBggrBgEFBQcwAoYaaHR0cDovL3N0Zy14Mi5pLmxl +bmNyLm9yZy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3N0Zy14Mi5jLmxlbmNy +Lm9yZy8wIgYDVR0gBBswGTAIBgZngQwBAgEwDQYLKwYBBAGC3xMBAQEwCgYIKoZI +zj0EAwMDaAAwZQIwXcZbdgxcGH9rTErfSTkXfBKKygU0yO7OpbuNeY1id0FZ/hRY +N5fdLOGuc+aHfCsMAjEA0P/xwKr6NQ9MN7vrfGAzO397PApdqfM7VdFK18aEu1xm +3HMFKzIR8eEPsMx4smMl +-----END CERTIFICATE----- diff --git a/certs-staging/letsencrypt-stg-int-e2.pem b/certs-staging/letsencrypt-stg-int-e2.pem new file mode 100644 index 0000000..931ff9b --- /dev/null +++ b/certs-staging/letsencrypt-stg-int-e2.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCjCCApCgAwIBAgIQQuJJzkyQeLKT5OSWP41qRTAKBggqhkjOPQQDAzBoMQsw +CQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3VyaXR5 +IFJlc2VhcmNoIEdyb3VwMSQwIgYDVQQDExsoU1RBR0lORykgQm9ndXMgQnJvY2Nv +bGkgWDIwHhcNMjAwOTA0MDAwMDAwWhcNMjUwOTE1MTYwMDAwWjBVMQswCQYDVQQG +EwJVUzEgMB4GA1UEChMXKFNUQUdJTkcpIExldCdzIEVuY3J5cHQxJDAiBgNVBAMT +GyhTVEFHSU5HKSBFcnNhdHogRWRhbWFtZSBFMjB2MBAGByqGSM49AgEGBSuBBAAi +A2IABEpsxJnmT3EQu6hL6LeYyvVggZd1aOj6QepgX+mdhOYxCgAvb4etuL80y7EP +sUUJh3Y20WhgXsZW21jukrL+PzdKfctcRoOM7CFBBk+09Ubalyys69O99+B6doRx +GYWWp6OCARAwggEMMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcD +AgYIKwYBBQUHAwEwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUbPiNRb5I +GjoDUBH28pJdQ2OphogwHwYDVR0jBBgwFoAU3tGjWWQOwZo2o0busBB2766XlWYw +NgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzAChhpodHRwOi8vc3RnLXgyLmkubGVu +Y3Iub3JnLzArBgNVHR8EJDAiMCCgHqAchhpodHRwOi8vc3RnLXgyLmMubGVuY3Iu +b3JnLzAiBgNVHSAEGzAZMAgGBmeBDAECATANBgsrBgEEAYLfEwEBATAKBggqhkjO +PQQDAwNoADBlAjEAv19ESEwzY8fAt1WkE4Nkm6bJxQEJZwILGNnvPuEmAKlngKov +dm1feBw0q45Fl8MEAjA24IoWYt7txJSbPQpxETJfsjO8aLWxedQpqHWS1x0zEB4L +K5uFc99+L56DIgmqjKM= +-----END CERTIFICATE----- diff --git a/certs-staging/letsencrypt-stg-int-r3.pem b/certs-staging/letsencrypt-stg-int-r3.pem new file mode 100644 index 0000000..0282fc1 --- /dev/null +++ b/certs-staging/letsencrypt-stg-int-r3.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFWzCCA0OgAwIBAgIQTfQrldHumzpMLrM7jRBd1jANBgkqhkiG9w0BAQsFADBm +MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy +aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ +ZWFyIFgxMB4XDTIwMDkwNDAwMDAwMFoXDTI1MDkxNTE2MDAwMFowWTELMAkGA1UE +BhMCVVMxIDAeBgNVBAoTFyhTVEFHSU5HKSBMZXQncyBFbmNyeXB0MSgwJgYDVQQD +Ex8oU1RBR0lORykgQXJ0aWZpY2lhbCBBcHJpY290IFIzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAu6TR8+74b46mOE1FUwBrvxzEYLck3iasmKrcQkb+ +gy/z9Jy7QNIAl0B9pVKp4YU76JwxF5DOZZhi7vK7SbCkK6FbHlyU5BiDYIxbbfvO +L/jVGqdsSjNaJQTg3C3XrJja/HA4WCFEMVoT2wDZm8ABC1N+IQe7Q6FEqc8NwmTS +nmmRQm4TQvr06DP+zgFK/MNubxWWDSbSKKTH5im5j2fZfg+j/tM1bGaczFWw8/lS +nukyn5J2L+NJYnclzkXoh9nMFnyPmVbfyDPOc4Y25aTzVoeBKXa/cZ5MM+WddjdL +biWvm19f1sYn1aRaAIrkppv7kkn83vcth8XCG39qC2ZvaQIDAQABo4IBEDCCAQww +DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAS +BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTecnpI3zHDplDfn4Uj31c3S10u +ZTAfBgNVHSMEGDAWgBS182Xy/rAKkh/7PH3zRKCsYyXDFDA2BggrBgEFBQcBAQQq +MCgwJgYIKwYBBQUHMAKGGmh0dHA6Ly9zdGcteDEuaS5sZW5jci5vcmcvMCsGA1Ud +HwQkMCIwIKAeoByGGmh0dHA6Ly9zdGcteDEuYy5sZW5jci5vcmcvMCIGA1UdIAQb +MBkwCAYGZ4EMAQIBMA0GCysGAQQBgt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCN +DLam9yN0EFxxn/3p+ruWO6n/9goCAM5PT6cC6fkjMs4uas6UGXJjr5j7PoTQf3C1 +vuxiIGRJC6qxV7yc6U0X+w0Mj85sHI5DnQVWN5+D1er7mp13JJA0xbAbHa3Rlczn +y2Q82XKui8WHuWra0gb2KLpfboYj1Ghgkhr3gau83pC/WQ8HfkwcvSwhIYqTqxoZ +Uq8HIf3M82qS9aKOZE0CEmSyR1zZqQxJUT7emOUapkUN9poJ9zGc+FgRZvdro0XB +yphWXDaqMYph0DxW/10ig5j4xmmNDjCRmqIKsKoWA52wBTKKXK1na2ty/lW5dhtA +xkz5rVZFd4sgS4J0O+zm6d5GRkWsNJ4knotGXl8vtS3X40KXeb3A5+/3p0qaD215 +Xq8oSNORfB2oI1kQuyEAJ5xvPTdfwRlyRG3lFYodrRg6poUBD/8fNTXMtzydpRgy +zUQZh/18F6B/iW6cbiRN9r2Hkh05Om+q0/6w0DdZe+8YrNpfhSObr/1eVZbKGMIY +qKmyZbBNu5ysENIK5MPc14mUeKmFjpN840VR5zunoU52lqpLDua/qIM8idk86xGW +xx2ml43DO/Ya/tVZVok0mO0TUjzJIfPqyvr455IsIut4RlCR9Iq0EDTve2/ZwCuG +hSjpTUFGSiQrR2JK2Evp+o6AETUkBCO1aw0PpQBPDQ== +-----END CERTIFICATE----- diff --git a/certs-staging/letsencrypt-stg-int-r4.pem b/certs-staging/letsencrypt-stg-int-r4.pem new file mode 100644 index 0000000..7e482dc --- /dev/null +++ b/certs-staging/letsencrypt-stg-int-r4.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFWzCCA0OgAwIBAgIQaCYQ95QBw3BbcmLyhdXHzjANBgkqhkiG9w0BAQsFADBm +MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy +aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ +ZWFyIFgxMB4XDTIwMDkwNDAwMDAwMFoXDTI1MDkxNTE2MDAwMFowWTELMAkGA1UE +BhMCVVMxIDAeBgNVBAoTFyhTVEFHSU5HKSBMZXQncyBFbmNyeXB0MSgwJgYDVQQD +Ex8oU1RBR0lORykgQXJ0aWZpY2lhbCBBcHJpY290IFI0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA6J8Tmlh6z62axF2+KzRgHHmxf0c5LHGA+wVx9ukJ +nB9zkqdG+gyGfYBnwPlxny0JWBS/1/wu7ry+IhtYOqar1Rg+f+gD0+SqOYmngNW8 +IIw0WTjHhqYB2d2Fxsr9bPIpDwpHRbgE8HkozAKwrWs5xDthZlaMZfEyAKzdx8mC +PttZzKW4ubSptmNMoGHx5t/pBWrNGz5EFuTYcy0DkknMvKedkVJn+jJBxVQ/ef/y +Gep7+1WjpW/UQvwJ5H2sm6UtBRAfos5U2TubN7fiI9OGsRmIp73BP9TsyP0Mi1rZ +kVAfMEU6pI5dJXTNt6hmzuE6frt+NlHaC9yrs9iEG1m6DQIDAQABo4IBEDCCAQww +DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAS +BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSa7sAVo3Q7x+zxBWCmwpOn+U8a +NTAfBgNVHSMEGDAWgBS182Xy/rAKkh/7PH3zRKCsYyXDFDA2BggrBgEFBQcBAQQq +MCgwJgYIKwYBBQUHMAKGGmh0dHA6Ly9zdGcteDEuaS5sZW5jci5vcmcvMCsGA1Ud +HwQkMCIwIKAeoByGGmh0dHA6Ly9zdGcteDEuYy5sZW5jci5vcmcvMCIGA1UdIAQb +MBkwCAYGZ4EMAQIBMA0GCysGAQQBgt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCw +o/xtzOLhliWC6F6XTdgCqt2FrjLQAXRUnCwnwg44oVbD+Gh9qXREpdpRPVrlIS7T +tobw8Z329nsLZ+wiGvm3wkC3Ka3RWbThLE/MuwVMysaTi5jXLJcQLvHRlW00jHgM +/V8vtJPr5s443KuOX1TFUV5Z/0ZilrBY3sAbgVk2n/fgyonX/JCdiGXt3HODpBh3 +eJH1kKM/EIrVVZwxHCwG1x/LuKpcUpbelF+NDI11neL+AzhU6wmBhgplg4OxzYWD +Xg0LCi6W0/t73HnG8SaRiCAcPyJOGBZtLQLUgfVKSd6DtRMhVcTzM6EjFiJYZuv4 +JHj65p1yFh8+kgJL/kyIWp2+mHgj/QCDaQEJccmWl+dpXm0jyyNBbjKG7oOlCzvH +HEBmrYSlzWI4XcK5C2+1SmSQqXv7vPo8jEVXSftg4Z6mEA8e2S6dt/rvreiQ+fDm +gixNiQRd7lkUqPv1EZhiGMYxhW52taj9A3xlcnD9/tfD7BKFe38ilVblYTU423WY +zAslpz6oDEiftKCZ2VmaUzEsS6Hma/r/SkF5oorVlaz7hE1qcu9HLkRohXPJtw3k +XJcK1hT3rITPasaSTREfwKBQS+y6guxv+IpkZftMEiOUix2cRoICJesxsYDE5tei +Pwrakf/zlLfF4WDZVqrYKsddVqddAoa64LfXxS/B4Q== +-----END CERTIFICATE----- diff --git a/certs-staging/letsencrypt-stg-root-x1.pem b/certs-staging/letsencrypt-stg-root-x1.pem new file mode 100644 index 0000000..37655b2 --- /dev/null +++ b/certs-staging/letsencrypt-stg-root-x1.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFmDCCA4CgAwIBAgIQU9C87nMpOIFKYpfvOHFHFDANBgkqhkiG9w0BAQsFADBm +MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy +aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ +ZWFyIFgxMB4XDTE1MDYwNDExMDQzOFoXDTM1MDYwNDExMDQzOFowZjELMAkGA1UE +BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl +YXJjaCBHcm91cDEiMCAGA1UEAxMZKFNUQUdJTkcpIFByZXRlbmQgUGVhciBYMTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALbagEdDTa1QgGBWSYkyMhsc +ZXENOBaVRTMX1hceJENgsL0Ma49D3MilI4KS38mtkmdF6cPWnL++fgehT0FbRHZg +jOEr8UAN4jH6omjrbTD++VZneTsMVaGamQmDdFl5g1gYaigkkmx8OiCO68a4QXg4 +wSyn6iDipKP8utsE+x1E28SA75HOYqpdrk4HGxuULvlr03wZGTIf/oRt2/c+dYmD +oaJhge+GOrLAEQByO7+8+vzOwpNAPEx6LW+crEEZ7eBXih6VP19sTGy3yfqK5tPt +TdXXCOQMKAp+gCj/VByhmIr+0iNDC540gtvV303WpcbwnkkLYC0Ft2cYUyHtkstO +fRcRO+K2cZozoSwVPyB8/J9RpcRK3jgnX9lujfwA/pAbP0J2UPQFxmWFRQnFjaq6 +rkqbNEBgLy+kFL1NEsRbvFbKrRi5bYy2lNms2NJPZvdNQbT/2dBZKmJqxHkxCuOQ +FjhJQNeO+Njm1Z1iATS/3rts2yZlqXKsxQUzN6vNbD8KnXRMEeOXUYvbV4lqfCf8 +mS14WEbSiMy87GB5S9ucSV1XUrlTG5UGcMSZOBcEUpisRPEmQWUOTWIoDQ5FOia/ +GI+Ki523r2ruEmbmG37EBSBXdxIdndqrjy+QVAmCebyDx9eVEGOIpn26bW5LKeru +mJxa/CFBaKi4bRvmdJRLAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBS182Xy/rAKkh/7PH3zRKCsYyXDFDANBgkqhkiG +9w0BAQsFAAOCAgEAncDZNytDbrrVe68UT6py1lfF2h6Tm2p8ro42i87WWyP2LK8Y +nLHC0hvNfWeWmjZQYBQfGC5c7aQRezak+tHLdmrNKHkn5kn+9E9LCjCaEsyIIn2j +qdHlAkepu/C3KnNtVx5tW07e5bvIjJScwkCDbP3akWQixPpRFAsnP+ULx7k0aO1x +qAeaAhQ2rgo1F58hcflgqKTXnpPM02intVfiVVkX5GXpJjK5EoQtLceyGOrkxlM/ +sTPq4UrnypmsqSagWV3HcUlYtDinc+nukFk6eR4XkzXBbwKajl0YjztfrCIHOn5Q +CJL6TERVDbM/aAPly8kJ1sWGLuvvWYzMYgLzDul//rUF10gEMWaXVZV51KpS9DY/ +5CunuvCXmEQJHo7kGcViT7sETn6Jz9KOhvYcXkJ7po6d93A/jy4GKPIPnsKKNEmR +xUuXY4xRdh45tMJnLTUDdC9FIU0flTeO9/vNpVA8OPU1i14vCz+MU8KX1bV3GXm/ +fxlB7VBBjX9v5oUep0o/j68R/iDlCOM4VVfRa8gX6T2FU7fNdatvGro7uQzIvWof +gN9WUwCbEMBy/YhBSrXycKA8crgGg3x1mIsopn88JKwmMBa68oS7EHM9w7C4y71M +7DiA+/9Qdp9RBWJpTS9i/mDnJg1xvo8Xz49mrrgfmcAXTCJqXi24NatI3Oc= +-----END CERTIFICATE----- diff --git a/certs-staging/letsencrypt-stg-root-x2.pem b/certs-staging/letsencrypt-stg-root-x2.pem new file mode 100644 index 0000000..f627e1d --- /dev/null +++ b/certs-staging/letsencrypt-stg-root-x2.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICTjCCAdSgAwIBAgIRAIPgc3k5LlLVLtUUvs4K/QcwCgYIKoZIzj0EAwMwaDEL +MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0 +eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj +b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTQwMDkxNzE2MDAwMFowaDELMAkGA1UE +BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl +YXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Njb2xpIFgy +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOvS+w1kCzAxYOJbA06Aw0HFP2tLBLKPo +FQqR9AMskl1nC2975eQqycR+ACvYelA8rfwFXObMHYXJ23XLB+dAjPJVOJ2OcsjT +VqO4dcDWu+rQ2VILdnJRYypnV1MMThVxo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3tGjWWQOwZo2o0busBB2766XlWYwCgYI +KoZIzj0EAwMDaAAwZQIwRcp4ZKBsq9XkUuN8wfX+GEbY1N5nmCRc8e80kUkuAefo +uc2j3cICeXo1cOybQ1iWAjEA3Ooawl8eQyR4wrjCofUE8h44p0j7Yl/kBlJZT8+9 +vbtH7QiVzeKCOTQPINyRql6P +-----END CERTIFICATE----- -- cgit v1.2.3 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/ --- Changelog | 2 + test | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++ 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 ++++ 15 files changed, 1020 insertions(+) create mode 100755 test 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 diff --git a/Changelog b/Changelog index 5bca11e..9e58239 100644 --- a/Changelog +++ b/Changelog @@ -26,6 +26,8 @@ lacme (0.7.1) upstream; whitespace. This doesn't change the default behavior but allows using `ssh -T lacme@account.example.net lacme-accountd` to spawn a remote lacme-accountd server for instance. + * Add test suite against Let's Encrypt's staging environment + https://letsencrypt.org/docs/staging-environment/ . + 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/test b/test new file mode 100755 index 0000000..ffee0ec --- /dev/null +++ b/test @@ -0,0 +1,237 @@ +#!/bin/bash + +#---------------------------------------------------------------------- +# ACME client written with process isolation and minimal privileges in mind +# (test suite) +# Copyright © 2015-2021 Guilhem Moulin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#---------------------------------------------------------------------- + +set -ue +PATH="/usr/bin:/bin" +export PATH + +usage() { + local rv="${1-0}" + echo "Usage: $0 [--deb|--dev] [TEST..]" >&2 + exit $rv +} + +# Setup: for any subdomain under $DOMAINNAME, +# http://$subdomain.$DOMAINNAME/.well-known/acme-challenge/$challenge +# must be routed to this machine. +# This can be done with a wildcard DNS record and opening tcp/80 in firewall. +DOMAINNAME="lacme-test.guilhem.org" + +MODE="dev" +DISTRIBUTION="sid" +BUILDDIR="build/test" +while [ $# -gt 0 ]; do + case "$1" in + --deb) MODE="deb"; shift;; + --dev) MODE="dev"; shift;; + --help|-h) usage 0;; + -*) echo "Error: Unknown option $1" >&2; usage 1;; + --) shift; break;; + *) break; + esac +done + +cd "$(dirname -- "$0")" +declare -a TESTS=() +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 + TESTS+=( "${t#tests/}" ) + fi + done +else + for t in "$@"; do + if [ -f "tests/$t" ]; then + TESTS+=( "$t" ) + else + echo "Error: '$1': no such test" >&2 + exit 1 + fi + done +fi + +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 +fi + +ACCOUNT_KEY="$BUILDDIR/account.key" +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 + # https://letsencrypt.org/docs/staging-environment/ + echo "Generating account key $ACCOUNT_KEY..." >&2 + openssl genpkey -algorithm RSA -out "$ACCOUNT_KEY" +fi + +ARCH="$(dpkg-architecture -qDEB_BUILD_ARCH)" +CHROOT="" + +cleanup() { + if [ -n "$CHROOT" ]; then + schroot -c "$CHROOT" -e + fi +} +trap cleanup EXIT INT TERM + +run() { + local t="tests/$1" rootdir version sub + if [ ! -f "$t" ]; then + echo "Error: '$1': no such test" >&2 + exit 1 + fi + + CHROOT="$(schroot -c "$DISTRIBUTION-$ARCH-sbuild" -b)" + rootdir="/run/schroot/mount/$CHROOT" + + if [ "$MODE" = "deb" ]; then + version="$(dpkg-parsechangelog -S Version)" + echo "Installing lacme $version into $CHROOT..." >&2 + install -vt "$rootdir/dev/shm" -m0644 -- \ + "$PKG_DESTDIR/lacme_${version}_all.deb" \ + "$PKG_DESTDIR/lacme-accountd_${version}_all.deb" + sudo schroot -d"/" -c "$CHROOT" -r -- \ + env DEBIAN_FRONTEND="noninteractive" apt install -y \ + "/dev/shm/lacme_${version}_all.deb" \ + "/dev/shm/lacme-accountd_${version}_all.deb" + + elif [ "$MODE" = "dev" ]; then + echo "Installing lacme dev into $CHROOT..." >&2 + sudo make install -- \ + BUILDDIR="$BUILDDIR" \ + DESTDIR="$rootdir" \ + exec_prefix="$rootdir/usr" \ + datadir="$rootdir/usr/share" \ + runstatedir="$rootdir/run" + sudo schroot -d"/" -c "$CHROOT" -r -- \ + env DEBIAN_FRONTEND="noninteractive" apt install -y \ + adduser \ + libconfig-tiny-perl \ + libcrypt-openssl-rsa-perl \ + libjson-perl \ + libnet-ssleay-perl \ + libtimedate-perl \ + libwww-perl \ + openssl + sudo schroot -d"/" -c "$CHROOT" -r -- \ + adduser --force-badname --system \ + --home /nonexistent --no-create-home \ + --gecos "lacme www user" \ + --quiet _lacme-www + sudo schroot -d"/" -c "$CHROOT" -r -- \ + adduser --force-badname --system \ + --home /nonexistent --no-create-home \ + --gecos "lacme client user" \ + --quiet _lacme-client + fi + + # set up staging environment, see https://letsencrypt.org/docs/staging-environment/ + 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 -- perl -pi -e \ + 's|\b\Qhttps://acme-v02.api.letsencrypt.org/\E\b|https://acme-staging-v02.api.letsencrypt.org/|' \ + "/usr/libexec/lacme/client" "/etc/lacme/lacme.conf" + + # install account key and configure lacme accordingly + sudo install -oroot -groot -m0600 -vT -- "$BUILDDIR/account.key" \ + "$rootdir/etc/lacme/account.key" + sudo schroot -d"/" -c "$CHROOT" -r -- \ + sed -ri '0,\|^#?privkey\s*=.*| {s||privkey = file:/etc/lacme/account.key|}' \ + /etc/lacme/lacme-accountd.conf + + # use lacme's internal webserver bound to INADDR_ANY port 80 + sudo schroot -d"/" -c "$CHROOT" -r -- \ + sed -ri 's|^#?listen\s*=.*|listen = 0.0.0.0|' /etc/lacme/lacme.conf + + # use a sample lacme-certs.conf, with a random subdomain so we can + # verify that challenges are answered correctly + sub="$(head -c10 /dev/urandom | base32 -w0)" + sudo tee "$rootdir/etc/lacme/lacme-certs.conf.d/simpletest-rsa.conf" >/dev/null <<- EOF + [simpletest-rsa] + certificate-key = /etc/lacme/simpletest.rsa.key + certificate-chain = /etc/lacme/simpletest.rsa.crt + subject = /CN=${sub,,[A-Z]}.$DOMAINNAME + EOF + sudo schroot -d"/" -c "$CHROOT" -r -- \ + openssl genpkey -algorithm RSA -out /etc/lacme/simpletest.rsa.key + + # copy test wrapper and unit file + local testdir="/dev/shm/lacme.test" + sudo install -oroot -groot -m0700 -d -- "$rootdir$testdir" + sudo install -oroot -groot -m0755 -T -- /dev/stdin "$rootdir$testdir/run" <<-EOF + STDERR="$testdir/stderr" + touch "\$STDERR" + fail() { + set +x + local rv=\$? i + if [ \$rv -eq 0 ]; then rv=1; fi + echo "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" + cat <"\$STDERR" >&2 + echo "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + [ \$# -eq 0 ] || echo "Error: \$*" >&2 + exit \$rv + } + grepstderr() { + grep "\$@" <"\$STDERR" || fail + } + ngrepstderr() { + ! grep "\$@" <"\$STDERR" || fail + } + set -x + EOF + sudo tee -a "$rootdir$testdir/run" >/dev/null <"$t" + + sudo schroot -d"/" -c "$CHROOT" -r -- env -i \ + USER="root" \ + HOME="/root" \ + SHELL="/bin/sh" \ + LOGNAME="root" \ + TERM="$TERM" \ + PATH="/usr/sbin:/usr/bin:/sbin:/bin" \ + DOMAINNAME="$DOMAINNAME" \ + sh -ue "$testdir/run" || return $? + + # clean up + schroot -c "$CHROOT" -e + CHROOT="" +} + +for t in "${TESTS[@]}"; do + if ! run "$t"; then + echo "FAILED: $t" >&2 + exit 1 + fi +done 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 1c4fc8c431e69780625600a4ee8526e1a3cbb3f4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 01:04:45 +0100 Subject: lacme(8)'s 'config' option in the [accountd] section no longer have a default value. The previous default, namely /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. --- Changelog | 4 ++++ config/lacme.conf | 2 +- lacme | 4 ++-- lacme.8.md | 1 - 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index 9e58239..a700625 100644 --- a/Changelog +++ b/Changelog @@ -28,6 +28,10 @@ lacme (0.7.1) upstream; remote lacme-accountd server for instance. * Add test suite against Let's Encrypt's staging environment 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. + 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/config/lacme.conf b/config/lacme.conf index 3faed2b..0deba7b 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -116,7 +116,7 @@ # Path to the lacme-accountd(1) configuration file. # -#config = @@sysconfdir@@/lacme/lacme-accountd.conf +#config = # The (private) account key to use for signing requests. See # lacme-accountd(1) for details. diff --git a/lacme b/lacme index 2f239e2..7800429 100755 --- a/lacme +++ b/lacme @@ -114,7 +114,7 @@ do { user => '', group => '', command => '@@bindir@@/lacme-accountd', - config => '@@sysconfdir@@/lacme/lacme-accountd.conf', + config => '', privkey => undef, quiet => 'Yes', } @@ -511,7 +511,7 @@ sub acme_client($@) { open STDOUT, '>&', $s or die "dup: $!"; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; push @args, '--stdio'; - push @args, '--config='.$accountd->{config} if defined $accountd->{config}; + push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; push @args, '--debug' if $OPTS{debug}; diff --git a/lacme.8.md b/lacme.8.md index c354c1a..c32469d 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -315,7 +315,6 @@ UNIX-domain socket. *config* : Path to the [`lacme-accountd`(1)] configuration file. - Default: `@@sysconfdir@@/lacme/lacme-accountd.conf`. *privkey* -- cgit v1.2.3 From ad1856777bf108826008b60a1e70c1e3fbb94ec7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 01:14:23 +0100 Subject: 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. --- Changelog | 3 +++ config/lacme-accountd.conf | 2 +- config/lacme.conf | 5 ----- lacme | 6 ++++-- lacme.8.md | 5 ----- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Changelog b/Changelog index a700625..abdb3ef 100644 --- a/Changelog +++ b/Changelog @@ -32,6 +32,9 @@ lacme (0.7.1) upstream; 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. + * 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. + 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/config/lacme-accountd.conf b/config/lacme-accountd.conf index 7248eb5..10f332e 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -4,8 +4,8 @@ # - file:FILE, for a private key in PEM format (optionally encrypted) # - gpg:FILE, for a gpg-encrypted private key # -#privkey = gpg:/path/to/encrypted/account.key.gpg #privkey = file:/path/to/account.key +#privkey = gpg:/path/to/encrypted/account.key.gpg # For a gpg-encrypted private account key, "gpg" specifies the binary # gpg(1) to use, as well as some default options. Default: "gpg diff --git a/config/lacme.conf b/config/lacme.conf index 0deba7b..a6cb9c7 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -118,11 +118,6 @@ # #config = -# The (private) account key to use for signing requests. See -# lacme-accountd(1) for details. -# -#privkey = file:/path/to/account.key - # Be quiet. # #quiet = Yes diff --git a/lacme b/lacme index 7800429..87a44be 100755 --- a/lacme +++ b/lacme @@ -115,7 +115,7 @@ do { group => '', command => '@@bindir@@/lacme-accountd', config => '', - privkey => undef, + privkey => '', quiet => 'Yes', } ); @@ -501,6 +501,8 @@ sub acme_client($@) { my ($client, $cleanup); my $conf = $CONFIG->{client}; if (defined (my $accountd = $CONFIG->{accountd})) { + warn "Setting 'privkey' in lacme.conf's [accountd] section is deprecated and will become an error in a future release! " + ."Set it in lacme-accountd.conf instead.\n" if $accountd->{privkey} ne ''; socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; my $pid = fork() // "fork: $!"; unless ($pid) { @@ -512,7 +514,7 @@ sub acme_client($@) { my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; push @args, '--stdio'; push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; - push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; + 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}; exec { $cmd } $cmd, @args or die; diff --git a/lacme.8.md b/lacme.8.md index c32469d..3852b13 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -316,11 +316,6 @@ UNIX-domain socket. : Path to the [`lacme-accountd`(1)] configuration file. -*privkey* - -: The (private) account key to use for signing requests. See - [`lacme-accountd`(1)] for details. - *quiet* : Be quiet. Possible values: `Yes`/`No`. -- cgit v1.2.3 From 5cf25633d48f79f39ab8c35883e1e437b3a058e4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 02:05:48 +0100 Subject: lacme: Preserve $GPG_TTY when spawning the accountd. This is needed for gpg-encrypted privkeys. --- lacme | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lacme b/lacme index 87a44be..d141b62 100755 --- a/lacme +++ b/lacme @@ -503,14 +503,16 @@ sub acme_client($@) { if (defined (my $accountd = $CONFIG->{accountd})) { warn "Setting 'privkey' in lacme.conf's [accountd] section is deprecated and will become an error in a future release! " ."Set it in lacme-accountd.conf instead.\n" if $accountd->{privkey} ne ''; + my $GPG_TTY = $ENV{GPG_TTY}; socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; my $pid = fork() // "fork: $!"; unless ($pid) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); - set_FD_CLOEXEC($s, 1); $client->close() or die "close: $!"; open STDIN, '<&', $s or die "dup: $!"; open STDOUT, '>&', $s or die "dup: $!"; + 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"; push @args, '--stdio'; push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; -- 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 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 3e49ef22ba3fbbe4e73bc4ad151770603ffa5ef1 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 18:52:33 +0100 Subject: lacme-accountd: Don't error out when the default configuration file is missing. Instead, treat it as an empty file. This makes it possible to use lacme-accountd(1) without configuration file under ~/.config/lacme. --- lacme-accountd | 23 +++++++++++++---------- lacme-accountd.1.md | 9 ++++++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lacme-accountd b/lacme-accountd index 1dc5f03..b9a6e33 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -69,16 +69,19 @@ do { ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" , "@@sysconfdir@@/lacme/$NAME.conf" ); - die "Error: Can't find configuration file\n" unless defined $conffile; - print STDERR "Using configuration file: $conffile\n" if $OPTS{debug}; - - my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n"; - 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; - $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; - $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; + + 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 $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; + $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; + $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; + } else { + print STDERR "Ignoring missing configuration file at default location $conffile\n" if $OPTS{debug}; + } $OPTS{quiet} = 0 if $OPTS{debug}; die "Error: 'privkey' is not specified\n" unless defined $OPTS{privkey}; diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index e628476..5303418 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -41,9 +41,12 @@ Options `--config=`*filename* -: Use *filename* as configuration file. See the **[configuration - file](#configuration-file)** section below for the configuration - options. +: 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. + + See the **[configuration file](#configuration-file)** section below + for the configuration options. `--privkey=`*value* -- 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 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 e3a3f59865290ea70de66ffa3b017916aac3ffef Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 16:05:31 +0100 Subject: Makefile wibble --- Makefile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 3ad440c..0fdd9df 100644 --- a/Makefile +++ b/Makefile @@ -79,14 +79,14 @@ release: git tag -sm "Release version $$VERS" "v$$VERS" install: all - install -m0644 -vDt $(sysconfdir)/lacme $(BUILDDIR)/config/*.conf $(BUILDDIR)/snippets/*.conf - install -vd $(sysconfdir)/lacme/lacme-certs.conf.d - install -m0644 -vDt $(datadir)/lacme $(BUILDDIR)/certs/* - install -m0755 -vDt $(libexecdir)/lacme $(BUILDDIR)/client $(BUILDDIR)/webserver - install -m0644 -vDt $(man1dir) $(BUILDDIR)/lacme-accountd.1 - install -m0644 -vDt $(man8dir) $(BUILDDIR)/lacme.8 - install -m0755 -vDt $(bindir) $(BUILDDIR)/lacme-accountd - install -m0755 -vDt $(sbindir) $(BUILDDIR)/lacme + install -m0644 -vDt $(sysconfdir)/lacme -- $(BUILDDIR)/config/*.conf $(BUILDDIR)/snippets/*.conf + install -m0755 -vd -- $(sysconfdir)/lacme/lacme-certs.conf.d + install -m0644 -vDt $(datadir)/lacme -- $(BUILDDIR)/certs/* + install -m0755 -vDt $(libexecdir)/lacme -- $(BUILDDIR)/client $(BUILDDIR)/webserver + install -m0644 -vDt $(man1dir) -- $(BUILDDIR)/lacme-accountd.1 + install -m0644 -vDt $(man8dir) -- $(BUILDDIR)/lacme.8 + install -m0755 -vDt $(sbindir) -- $(BUILDDIR)/lacme + install -m0755 -vDt $(bindir) -- $(BUILDDIR)/lacme-accountd uninstall: rm -vf -- $(bindir)/lacme-accountd $(sbindir)/lacme -- 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: =?UTF-8?q?Symlink=20$(sysconfdir)/apache2/conf-available/lacme.co?= =?UTF-8?q?nf=20=E2=86=92=20../../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(-) 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 becac5d1fad959a0ffb0d67afed0d4d7069c3114 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 23:57:36 +0100 Subject: Use real UID not effective UID in environment sanitation. Not that it make a difference since we don't run suid. --- lacme | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lacme b/lacme index d141b62..9f46b47 100755 --- a/lacme +++ b/lacme @@ -237,9 +237,9 @@ sub drop_privileges($$$) { # sanitize environment my $term = $ENV{TERM}; - my @ent = getpwuid($>) or die "getpwuid($>): $!"; + my @ent = getpwuid($<) or die "getpwuid($<): $!"; %ENV = ( USER => $ent[0], LOGNAME => $ent[0], HOME => $ent[7], SHELL => $ent[8] ); - $ENV{PATH} = $> == 0 ? "/usr/sbin:/usr/bin:/sbin:/bin" : "/usr/bin:/bin"; + $ENV{PATH} = $< == 0 ? "/usr/sbin:/usr/bin:/sbin:/bin" : "/usr/bin:/bin"; $ENV{TERM} = $term if defined $term; # preserve $TERM chdir $dir or die "chdir($dir): $!"; -- cgit v1.2.3 From 8de74ffb4a2008a61c05e9a24c8fa9b14858d2be Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 18:31:20 +0100 Subject: Remove dependency on List::Util (core module). --- Changelog | 1 + INSTALL | 1 - lacme | 7 +++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Changelog b/Changelog index abdb3ef..8952ba6 100644 --- a/Changelog +++ b/Changelog @@ -50,6 +50,7 @@ lacme (0.7.1) upstream; + accountd: replace internal option --conn-fd=FD with flag --stdio. Using stdin/stdout makes it possible to tunnel the accountd connection through ssh. + + Remove dependency on List::Util (core module). - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/INSTALL b/INSTALL index cb8d57f..85bd0c2 100644 --- a/INSTALL +++ b/INSTALL @@ -28,7 +28,6 @@ lacme depends on OpenSSL ≥1.1.0 and the following Perl modules: - File::Temp (core module) - Getopt::Long (core module) - JSON (optionally C/XS-accelerated with JSON::XS) - - List::Util (core module) - LWP::UserAgent - LWP::Protocol::https (for https:// ACME directory URIs) - MIME::Base64 (core module) diff --git a/lacme b/lacme index 9f46b47..cb399f9 100755 --- a/lacme +++ b/lacme @@ -30,7 +30,6 @@ use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC O_CREAT O_EXCL O_WRONLY SEEK_SET/; use File::Basename 'dirname'; use File::Temp (); use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; -use List::Util 'first'; use POSIX (); use Socket 1.95 qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC INADDR_ANY IN6ADDR_ANY IPPROTO_IPV6 @@ -744,15 +743,15 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); } - my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; - unless (defined $certtype) { + my $cert = $conf->{'certificate-chain'} // $conf->{'certificate'}; + unless (defined $cert) { print STDERR "[$s] Warning: Missing 'certificate' and 'certificate-chain', skipping\n"; $rv = 1; next; } # skip certificates that expire at least $conf->{'min-days'} days in the future - if (-f $conf->{$certtype} and defined (my $t = x509_enddate($conf->{$certtype}))) { + if (-f $cert and defined (my $t = x509_enddate($cert))) { my $d = $OPTS{'min-days'} // $conf->{'min-days'} // 21; if ($d >= 0 and $t - time > $d*86400) { my $d = POSIX::strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($t)); -- cgit v1.2.3 From 2114bd775df342f3491cdd839031254041b655ae Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 00:48:40 +0100 Subject: typofix --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3a8e461..2ac524b 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ libexecdir ?= $(exec_prefix)/libexec datarootdir ?= $(prefix)/share datadir ?= $(datarootdir) sysconfdir ?= $(prefix)/etc -localstatedir =? $(prefix)/var +localstatedir ?= $(prefix)/var runstatedir ?= $(localstatedir)/run mandir ?= $(datarootdir)/man man1dir ?= $(mandir)/man1 -- cgit v1.2.3 From c342643613a940e147d9b598666823d6baa19a0d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 01:36:48 +0100 Subject: wibble --- lacme | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lacme b/lacme index cb399f9..9a62cbb 100755 --- a/lacme +++ b/lacme @@ -295,7 +295,7 @@ sub spawn_webserver() { die "Error: Refusing to use non-empty challenge directory $dir\n" unless $_ eq '.' or $_ eq '..'; } - closedir $dh or die "close: $!"; + closedir $dh or die "closedir: $!"; undef $dh; # use a "lock file" (NFS-friendly) to avoid concurrent usages @@ -309,7 +309,7 @@ sub spawn_webserver() { push @CLEANUP, sub() { if (opendir(my $dh, $dir)) { my @files = grep { $_ ne '.' and $_ ne '..' and $_ ne $lockfile } readdir $dh; - closedir $dh or warn "close: $!"; + closedir $dh or warn "closedir: $!"; push @files, $lockfile; # unlink $lockfile last foreach (@files) { die unless /\A(.+)\z/; # untaint @@ -712,7 +712,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { warn "$conffile/$_ has unknown suffix, skipping\n"; } } - closedir $dh; + closedir $dh or die "closedir: $!"; } foreach my $filename (sort @filenames) { print STDERR "Reading $filename\n" if $OPTS{debug}; -- cgit v1.2.3 From 4a502836164821b9faa56d363c8fb116ce032321 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 18:11:09 +0100 Subject: Wording: s/option/setting/. --- config/lacme-certs.conf | 6 +++--- config/lacme.conf | 2 +- lacme-accountd.1.md | 4 ++-- lacme.8.md | 26 +++++++++++++------------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf index 91c2b3d..5259690 100644 --- a/config/lacme-certs.conf +++ b/config/lacme-certs.conf @@ -1,5 +1,5 @@ # Each non-default section refer to separate certificate issuance -# requests. Options in the default section apply to each sections. +# requests. Settings in the default section apply to each sections. # Message digest to sign the Certificate Signing Request with, # overriding the req(1ssl) default. @@ -27,7 +27,7 @@ #[www] -# Path the service's private key. This option is required. +# Path the service's private key. This setting is required. # #certificate-key = /etc/nginx/ssl/srv.key @@ -40,7 +40,7 @@ # #certificate-chain = /etc/nginx/ssl/srv.chain.crt -# Subject field of the Certificate Signing Request. This option is +# Subject field of the Certificate Signing Request. This setting is # required. # #subject = /CN=example.org diff --git a/config/lacme.conf b/config/lacme.conf index a6cb9c7..98ecacb 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -12,7 +12,7 @@ # 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 option is ignored when lacme-accountd(1) is spawned by lacme(8), +# 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. # diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 9377e23..cd6352c 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -94,11 +94,11 @@ environment variable is not set), and When given on the command line, the `--privkey=`, `--socket=` and `--quiet` options take precedence over their counterpart (without -leading `--`) in the configuration file. Valid options are: +leading `--`) in the configuration file. Valid settings are: *privkey* -: See `--privkey=`. This option is required when `--privkey=` is not +: See `--privkey=`. This setting is required when `--privkey=` is not specified on the command line. *gpg* diff --git a/lacme.8.md b/lacme.8.md index 6218d36..4dfc67e 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -37,9 +37,9 @@ with its own executable: For certificate issuances (`newOrder` command), it also generates Certificate Signing Requests, then verifies the validity of the issued certificate, and optionally reloads or restarts services when - the *notify* option is set. + the *notify* setting is set. - 3. An actual [ACME] client (specified with the *command* option of the + 3. An actual [ACME] client (specified with the *command* setting of the [`[client]` section](#client-section) of the configuration file), which builds [ACME] commands and dialogues with the remote [ACME] server. @@ -49,7 +49,7 @@ with its own executable: requested by writing the data to be signed to the socket. 4. For certificate issuances (`newOrder` command), an optional - webserver (specified with the *command* option of the [`[webserver]` + webserver (specified with the *command* setting of the [`[webserver]` section](#webserver-section) of the configuration file), which is spawned by the “master” `lacme`. (The only challenge type currently supported by `lacme` is `http-01`, which requires a webserver to @@ -95,8 +95,8 @@ Commands account key or the server's private key. Command alias: `revoke-cert`. -Generic options -=============== +Generic settings +================ `--config=`*filename* @@ -110,7 +110,7 @@ Generic options 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. - This command-line option overrides the *socket* option of the + 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 ignored. @@ -134,7 +134,7 @@ 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 options are: +Valid settings are: Default section --------------- @@ -247,7 +247,7 @@ served during certificate issuance. lacme client user (by default `@@lacme_client_user@@`) needs to be able to create files under it. - This option is required when *listen* is empty. + This setting is required when *listen* is empty. *user* @@ -270,7 +270,7 @@ served during certificate issuance. argument etc. (Note that `lacme` might append more arguments when executing the command internally.) A separate process is spawned for each address to *listen* on. (In - particular no webserver process is forked when the *listen* option + particular no webserver process is forked when the *listen* setting is empty.) Default: `@@libexecdir@@/lacme/webserver`. @@ -279,7 +279,7 @@ served during certificate issuance. : Whether to automatically install temporary [`iptables`(8)] rules to open the `ADDRESS[:PORT]` specified with *listen*. The rules are automatically removed once `lacme` exits. - This option is ignored when *challenge-directory* is set. + This setting is ignored when *challenge-directory* is set. Default: `No`. `[accountd]` section @@ -327,7 +327,7 @@ For certificate issuances (`newOrder` command), a separate file is used to configure paths to the certificate and key, as well as the subject, subjectAltName, etc. to generate Certificate Signing Requests. Each section denotes a separate certificate issuance. -Valid options are: +Valid settings are: *certificate* @@ -342,7 +342,7 @@ Valid options are: *certificate-key* -: Path to the service's private key. This option is required. The +: Path to the service's private key. This setting is required. The [`genpkey`(1ssl)] command can be used to generate a new service RSA key: @@ -377,7 +377,7 @@ Valid options are: *subject* : Subject field of the Certificate Signing Request, in the form - `/type0=value0/type1=value1/type2=…`. This option is required. + `/type0=value0/type1=value1/type2=…`. This setting is required. *subjectAltName* -- 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 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(-) 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 f5a8ada638d9fcef9939b00f941588288f6287cc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 20 Feb 2021 20:38:48 +0100 Subject: =?UTF-8?q?Documentation:=20Wrap=20commands=20in=20`=E2=80=A6`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lacme.8.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lacme.8.md b/lacme.8.md index 9a14d75..ca47470 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -316,8 +316,9 @@ UNIX-domain socket. 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. + Use for instance `` `ssh -T lacme@account.example.net + lacme-accountd` `` in order to spawn a remote [`lacme-accountd`(1)] + server. *config* @@ -433,7 +434,7 @@ Valid settings are: *notify* -: Command to pass the the system's command shell (`/bin/sh -c`) +: Command to pass the the system's command shell (`` `/bin/sh -c` ``) after successful installation of the *certificate* and/or *certificate-chain*. -- cgit v1.2.3 From cf3d42c066d2f54d4a57aa38907a7c6c7d06aeb6 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 16 Feb 2021 00:00:40 +0100 Subject: lacme-accountd(1): base64url-decode incoming signature requests. Before printing them to the standard error. --- Changelog | 2 ++ lacme-accountd | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index baf67b9..c69d0d0 100644 --- a/Changelog +++ b/Changelog @@ -48,6 +48,8 @@ lacme (0.7.1) upstream; drop). * lacme-accountd(1): add %-specifiers support for --config=, --socket= and --privkey= (and 'socket'/'privkey' configuration options). + * lacme-accountd(1): base64url-decode incoming signature requests shown + in messages to the standard error. + 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-accountd b/lacme-accountd index e170637..0adfe38 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -30,7 +30,7 @@ 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 MIME::Base64 'encode_base64url'; +use MIME::Base64 qw/decode_base64url encode_base64url/; use Socket qw/PF_UNIX SOCK_STREAM SHUT_RDWR/; use Config::Tiny (); @@ -195,7 +195,23 @@ sub conn($$;$) { # sign whatever comes in while (defined (my $data = $in->getline())) { $data =~ s/\r\n\z// or die; - print STDERR "[$id] >>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; + + 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"; + 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"; + last; + } + + print STDERR "[$id] >>> Incoming signature request for ", + "base64url(", decode_base64url($protected), ") . ", + "base64url(", decode_base64url($payload), ")" + unless $OPTS{quiet}; + my $sig = $SIGN->($data); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; } -- 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(-) 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 626c0418b3d8c3747a7be8e2620d7c85a8c2c613 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 02:55:46 +0100 Subject: Make the ACME API server URL configurable at build time. --- Makefile | 7 +++++-- client | 2 +- config/lacme.conf | 2 +- lacme.8.md | 2 +- test | 6 ++---- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index a4caff0..16ac04e 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,8 @@ lacme_www_group ?= www-data lacme_client_user ?= nobody lacme_client_group ?= nogroup +acmeapi_server ?= https://acme-v02.api.letsencrypt.org/directory + $(BUILDDIR)/%: % mkdir -pv -- $(dir $@) cp --no-dereference --preserve=mode,links,xattr -vfT -- "$<" "$@" @@ -62,8 +64,9 @@ $(BUILDDIR)/%: % s#@@lacme_www_user@@#$(lacme_www_user)#g; \ s#@@lacme_www_group@@#$(lacme_www_group)#g; \ s#@@lacme_client_user@@#$(lacme_client_user)#g; \ - s#@@lacme_client_group@@#$(lacme_client_group)#g;" \ - -- "$@" + s#@@lacme_client_group@@#$(lacme_client_group)#g; \ + s#@@acmeapi_server@@#$(acmeapi_server)#g; \ + " -- "$@" release: @if ! git diff HEAD --quiet -- ./Changelog ./lacme ./lacme-accountd ./client; then \ diff --git a/client b/client index a5490f8..e62541c 100755 --- a/client +++ b/client @@ -210,7 +210,7 @@ sub acme($;$) { }); } -my $SERVER_URI = $CONFIG->{server} // 'https://acme-v02.api.letsencrypt.org/directory'; +my $SERVER_URI = $CONFIG->{server} // '@@acmeapi_server@@'; my %RES; # Get the resource URI from the directory diff --git a/config/lacme.conf b/config/lacme.conf index 198729d..0392be5 100644 --- a/config/lacme.conf +++ b/config/lacme.conf @@ -35,7 +35,7 @@ # for testing # as it has relaxed rate-limiting. # -#server = https://acme-v02.api.letsencrypt.org/directory +#server = @@acmeapi_server@@ # Timeout in seconds after which the client stops polling the ACME # server and considers the request failed. diff --git a/lacme.8.md b/lacme.8.md index 7d66e79..30de221 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -194,7 +194,7 @@ of [ACME] commands and dialogues with the remote [ACME] server). *server* : Root URI of the [ACME] server. - Default: `https://acme-v02.api.letsencrypt.org/directory`. + Default: `@@acmeapi_server@@`. *timeout* diff --git a/test b/test index 50b7382..2be9303 100755 --- a/test +++ b/test @@ -85,7 +85,8 @@ elif [ "$MODE" = "dev" ]; then lacme_www_user=_lacme-www \ lacme_www_group=nogroup \ lacme_client_user=_lacme-client \ - lacme_client_group=nogroup + lacme_client_group=nogroup \ + acmeapi_server="https://acme-staging-v02.api.letsencrypt.org/directory" fi ACCOUNT_KEY="$BUILDDIR/account.key" @@ -162,9 +163,6 @@ 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 -- perl -pi -e \ - 's|\b\Qhttps://acme-v02.api.letsencrypt.org/\E\b|https://acme-staging-v02.api.letsencrypt.org/|' \ - "/usr/libexec/lacme/client" "/etc/lacme/lacme.conf" # install account key and configure lacme accordingly sudo install -oroot -groot -m0600 -vT -- "$BUILDDIR/account.key" \ -- cgit v1.2.3 From 543578b20187836a83d84c7cd669cb2448e3a3d7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 12:01:28 +0100 Subject: test suite: Indicate which tests have passed. --- test | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/test b/test index 2be9303..c9aaef8 100755 --- a/test +++ b/test @@ -222,15 +222,31 @@ run() { PATH="/usr/sbin:/usr/bin:/sbin:/bin" \ DOMAINNAME="$DOMAINNAME" \ sh -ue "$testdir/run" || return $? - - # clean up - schroot -c "$CHROOT" -e - CHROOT="" } +RV=0 +declare -a PASSED=() FAILED=() for t in "${TESTS[@]}"; do - if ! run "$t"; then - echo "FAILED: $t" >&2 - exit 1 + run "$t" && rv=0 || rv=$? + if [ -n "$CHROOT" ]; then + # clean up + schroot -c "$CHROOT" -e + CHROOT="" + fi + if [ $rv -eq 0 ]; then + PASSED+=( "$t" ) + else + FAILED+=( "$t" ) + RV=$rv + break # stop at the first failure fi done + +echo >&2 +echo "================================================================================" >&2 + +echo "PASSED: ${PASSED[*]:-"(none)"}" >&2 +if [ ${#FAILED[@]} -gt 0 ]; then + echo "FAILED: ${FAILED[*]}" >&2 +fi +exit $RV -- cgit v1.2.3 From 7545130ac74c5a6f3dfa62087ac0287686248eec Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 12:22:06 +0100 Subject: test suite: Don't try to show stderr if it's empty. --- test | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test b/test index c9aaef8..4fdfdbc 100755 --- a/test +++ b/test @@ -197,9 +197,11 @@ run() { set +x local rv=\$? i if [ \$rv -eq 0 ]; then rv=1; fi - echo "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" - cat <"\$STDERR" >&2 - echo "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + if [ -s "\$STDERR" ]; then + echo "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" + cat <"\$STDERR" >&2 + echo "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + fi [ \$# -eq 0 ] || echo "Error: \$*" >&2 exit \$rv } -- cgit v1.2.3 From 9bc3b5756ef3f36aaa0a1b28db71767c87e9446e Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 13:00:31 +0100 Subject: accountd: Fix prototype. --- lacme-accountd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lacme-accountd b/lacme-accountd index c8c6d5e..27e45cd 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -217,7 +217,7 @@ unless (defined $OPTS{stdio}) { # For each new connection, send the protocol version and the account key's # public parameters, then sign whatever comes in # -sub conn($$;$) { +sub conn($$$) { my ($in, $out, $id) = @_; $out->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ) or warn "print: $!"; -- cgit v1.2.3 From 594c32ac58167396980a404261047e94155a83d3 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 13:01:14 +0100 Subject: wording --- lacme-accountd | 2 +- lacme-accountd.1.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lacme-accountd b/lacme-accountd index 27e45cd..6a6e90d 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -231,7 +231,7 @@ sub conn($$$) { last; } unless (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) { - # payload can be empty, for instance for POST-as-GET + # POST-as-GET yields an empty payload info("[$id] >>> Error: Malformed payload data, refusing to sign!"); last; } diff --git a/lacme-accountd.1.md b/lacme-accountd.1.md index 66ef222..d0b2c6b 100644 --- a/lacme-accountd.1.md +++ b/lacme-accountd.1.md @@ -136,8 +136,9 @@ leading `--`) in the configuration file. Valid settings are: ============ The value the `--config=`, `--privkey=` and `--socket=` CLI options (and -*privkey*, *socket* and *logfile* settings in the configuration file) -are subject to %-expansion for the following specifiers. +also the *privkey*, *socket* and *logfile* settings from the +configuration file) are subject to %-expansion for the following +specifiers. ---- ------------------------------------------------------------------ `%C` `@@localstatedir@@/cache` for the root user, and `$XDG_CACHE_HOME` -- 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(-) 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 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 ba6addf54cef0b1536dc87c42a41b4dc207ac884 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 14:27:50 +0100 Subject: accountd: Pass JWA and JWK thumbprint via extended greeting data. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passing the JWA to the ACME client is required if we want to support account keys other than RSA. As of 0.7 both lacme-accountd(1) and lacme(8) hardcode “RS256” (SHA256withRSA per RFC 7518 sec. A.1). Passing the JWK thumbprint is handy as it gives more flexibility if RFC 8555 sec. 8.1 were to be updated with another digest algorithm (it's currently hardcoded to SHA-256). A single lacme-account(1) instance might be used to sign requests from many clients, and it's easier to upgrade a single ‘lacme-accountd’ than many ‘lacme’. Moreover, in some restricted environments lacme-accountd might hide the JWK from the client to prevent ‘newAccount’ requests (such as contact updates); passing its thumbprint is enough for ‘newOrder’ requests. --- Changelog | 2 ++ client | 33 ++++++++++++++++++++++----------- lacme-accountd | 16 ++++++++++++---- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Changelog b/Changelog index e6becda..ffd9536 100644 --- a/Changelog +++ b/Changelog @@ -69,6 +69,8 @@ lacme (0.7.1) upstream; connection through ssh. The new flag is documented to allow safe usage is authorized_keys(5) restrictions. + Remove dependency on List::Util (core module). + + accountd: Pass JWA and JWK thumbprint via extended greeting data. + This gives better forward flexibility. - lacme: delay webserver socket shutdown to after the process has terminated. - documentation: suggest to generate private key material with diff --git a/client b/client index e62541c..7a63259 100755 --- a/client +++ b/client @@ -49,7 +49,7 @@ my $NAME = 'lacme-client'; use Errno 'EEXIST'; use Fcntl qw/O_CREAT O_EXCL O_WRONLY/; -use Digest::SHA qw/sha256 sha256_hex/; +use Digest::SHA 'sha256'; use MIME::Base64 qw/encode_base64 encode_base64url/; use Date::Parse (); @@ -70,24 +70,34 @@ open (my $CONFFILE, '<&=', $1+0) or die "fdopen $1: $!"; (shift @ARGV // die) =~ /\A(\d+)\z/ or die; open (my $S, '+<&=', $1+0) or die "fdopen $1: $!"; +# JSON keys need to be sorted lexicographically (for instance in the thumbprint) +sub json() { JSON::->new->utf8->canonical(); } + ############################################################################# # Read the protocol version and JSON Web Key (RFC 7517) from the # lacme-accountd socket # + +my ($JWK, $JWK_thumbprint, $ALG, $KID); do { my $greeting = $S->getline(); die "Error: Invalid client version\n" unless defined $greeting and - $greeting =~ /\A(\d+) OK(?:.*)\r\n\z/ and $1 == $PROTOCOL_VERSION; + $greeting =~ /\A(\d+) OK(?: (.*))?\r\n\z/ and $1 == $PROTOCOL_VERSION; + if (defined (my $extra = $2)) { + my $h = eval { JSON::->new->decode($extra) }; + if ($@ or !defined $h) { + 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/}; + } + } + my $jwk_str = $S->getline() // die "ERROR: No JWK from lacme-accountd\n"; + $JWK = JSON::->new->decode($jwk_str); + $JWK_thumbprint //= encode_base64url(sha256(json()->encode($JWK))); # SHA-256 is hardcoded, see RFC 8555 sec. 8.1 + $ALG //= "RS256"; }; -my $JWK = JSON::->new->decode($S->getline()); -my $KID; - -# JSON keys need to be sorted lexicographically (for instance in the thumbprint) -sub json() { JSON::->new->utf8->canonical(); } - -my $JWK_thumbprint = encode_base64url(sha256(json()->encode($JWK))); -my $NONCE; ############################################################################# @@ -111,6 +121,7 @@ my $UA = do { LWP::UserAgent::->new( agent => "$NAME/$VERSION", ssl_opts => \%ssl_opts ); } // die "Can't create LWP::UserAgent object"; $UA->default_header( 'Accept-Language' => 'en' ); +my $NONCE; ############################################################################# @@ -192,7 +203,7 @@ sub acme($;$) { die "Missing nonce\n" unless defined $NONCE; # Produce the JSON Web Signature: RFC 7515 section 5 - my %header = ( alg => 'RS256', nonce => $NONCE, url => $uri ); + 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)); diff --git a/lacme-accountd b/lacme-accountd index 0f0b0d9..d4521f9 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -27,6 +27,7 @@ our $VERSION = '0.3'; my $PROTOCOL_VERSION = 1; my $NAME = 'lacme-accountd'; +use Digest::SHA 'sha256'; use Errno 'EINTR'; use File::Basename 'dirname'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; @@ -141,7 +142,7 @@ do { # Build the JSON Web Key (RFC 7517) from the account key's public parameters, # and determine the signing method $SIGN. # -my ($JWK, $SIGN); +my ($EXTRA_GREETING_STR, $JWK_STR, $SIGN); if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { my ($method, $filename) = ($1, spec_expand($2)); my ($fh, @command); @@ -174,13 +175,19 @@ if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { my ($n, $e) = $rsa->get_key_parameters(); # don't include private params! $_ = encode_base64url($_->to_bin()) foreach ($n, $e); - $JWK = { kty => 'RSA', n => $n, e => $e }; + my %extra_greeting; + my %jwk = ( kty => 'RSA', n => $n, e => $e ); + $extra_greeting{alg} = 'RS256'; # SHA256withRSA (RFC 7518 sec. A.1) $SIGN = sub($) { $rsa->sign($_[0]) }; + + # 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)); + $EXTRA_GREETING_STR = JSON::->new->encode(\%extra_greeting); } else { error("Unsupported method: $OPTS{privkey}"); } -my $JWK_STR = JSON::->new->encode($JWK); ############################################################################# @@ -219,7 +226,8 @@ unless (defined $OPTS{stdio}) { # sub conn($$$) { my ($in, $out, $id) = @_; - $out->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK_STR, "\r\n" ) or warn "print: $!"; + $out->printflush( "$PROTOCOL_VERSION OK ", $EXTRA_GREETING_STR, "\r\n", + $JWK_STR, "\r\n" ) or warn "print: $!"; # sign whatever comes in while (defined (my $data = $in->getline())) { -- 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(-) 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 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 3d7cbb119cae717e0a3e5546d9a2b6ae982c514b Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 00:19:14 +0100 Subject: Add 'logfile' to lacme-account.conf. --- config/lacme-accountd.conf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/lacme-accountd.conf b/config/lacme-accountd.conf index d31c6c8..5c769cf 100644 --- a/config/lacme-accountd.conf +++ b/config/lacme-accountd.conf @@ -20,6 +20,10 @@ # #socket = %t/S.lacme +# An optional file where to log to. +# +#logfile = + # 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. -- cgit v1.2.3 From fef888045bfd03c26822782411ff835c03440d58 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 00:28:02 +0100 Subject: logfile: treat empty values as unset. --- lacme-accountd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lacme-accountd b/lacme-accountd index a842bce..9ed41c4 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -119,10 +119,10 @@ do { print STDERR "Using configuration file: $conffile\n" if $OPTS{debug}; my $h = Config::Tiny::->read($conffile) or error(Config::Tiny::->errstr()); my $h2 = delete $h->{_} // {}; - if (defined (my $logfile = $h2->{logfile})) { + if ((my $logfile = $h2->{logfile} // "") ne "") { $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: $!"; + open $LOG, ">>", $1 or die "Can't open $1: $!"; # open ASAP (before config validation) } error("Invalid section(s): ".join(', ', keys %$h)) if %$h; my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket logfile keyid quiet/; -- cgit v1.2.3 From ed85b6a6740028ce9ce821975a534f696eabd8ed Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 01:04:58 +0100 Subject: client: Print Terms of Service URL for 'account' command. --- Changelog | 1 + client | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Changelog b/Changelog index 9c606fe..a402207 100644 --- a/Changelog +++ b/Changelog @@ -96,6 +96,7 @@ lacme (0.7.1) upstream; - Set the DEBUG environment variable to 0/1 instead of ""/1. - Use File::Basename::dirname() to correctly extract the parent directory of the socket path. + - client: Print Terms of Service URL for 'account' command. -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 diff --git a/client b/client index d7874b7..0bcf8cd 100755 --- a/client +++ b/client @@ -289,6 +289,9 @@ if ($COMMAND eq 'account') { my $r = acme_resource('newAccount', %h); # TODO: list account orders: https://github.com/letsencrypt/boulder/issues/3335 + print STDERR "Terms of Service: $RES{meta}->{termsOfService}\n" + if defined $RES{meta} and defined $RES{meta}->{termsOfService}; + if ($r->is_success()) { $KID = $r->header('Location'); print STDERR "Key ID: $KID\n"; -- cgit v1.2.3 From d9c91dc92f4d6af2d9f537a5f30a2cb40715da71 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 01:49:27 +0100 Subject: space damage --- test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test b/test index 4fdfdbc..dc80751 100755 --- a/test +++ b/test @@ -213,7 +213,7 @@ run() { } set -x EOF - sudo tee -a "$rootdir$testdir/run" >/dev/null <"$t" + sudo tee -a "$rootdir$testdir/run" >/dev/null <"$t" sudo schroot -d"/" -c "$CHROOT" -r -- env -i \ USER="root" \ -- cgit v1.2.3 From f55a2782db2e86c88ed4780d7ed54b09792d07d6 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 03:13:26 +0100 Subject: Print error messages only once. --- lacme-accountd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lacme-accountd b/lacme-accountd index 9ed41c4..9909cb8 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -79,13 +79,13 @@ sub info(@) { logmsg(all => @_); } sub error(@) { my @msg = ("Error: ", @_); info(@msg); - die(@msg, "\n"); + exit 255; } sub panic(@) { my @loc = caller; my @msg = (@_, " at line $loc[2] in $loc[1]"); info(@msg); - die(@msg, "\n"); + exit 255; } sub env_fallback($$) { -- 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(-) 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(-) 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 From 3eba02ef820a393bd5781be9f8fcda1611ae7c3d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 03:19:57 +0100 Subject: Prepare new release v0.8.0. --- Changelog | 4 ++-- client | 2 +- lacme | 2 +- lacme-accountd | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Changelog b/Changelog index a402207..9f12237 100644 --- a/Changelog +++ b/Changelog @@ -1,4 +1,4 @@ -lacme (0.7.1) upstream; +lacme (0.8.0) upstream; * Breaking change: 'challenge-directory' now needs to be set to an *existing* directory (writable by the lacme client user). Since @@ -98,7 +98,7 @@ lacme (0.7.1) upstream; directory of the socket path. - client: Print Terms of Service URL for 'account' command. - -- Guilhem Moulin Wed, 09 Dec 2020 18:23:22 +0100 + -- Guilhem Moulin Mon, 22 Feb 2021 03:19:57 +0100 lacme (0.7) upstream; diff --git a/client b/client index 0bcf8cd..fdef865 100755 --- a/client +++ b/client @@ -43,7 +43,7 @@ use warnings; # instance own by another user and created with umask 0177) is not a # problem since SOCKET_FD can be bound as root prior to the execve(2). -our $VERSION = '0.3'; +our $VERSION = '0.8.0'; my $PROTOCOL_VERSION = 1; my $NAME = 'lacme-client'; diff --git a/lacme b/lacme index 1e51c6c..731535f 100755 --- a/lacme +++ b/lacme @@ -22,7 +22,7 @@ use v5.14.2; use strict; use warnings; -our $VERSION = '0.3'; +our $VERSION = '0.8.0'; my $NAME = 'lacme'; use Errno 'EINTR'; diff --git a/lacme-accountd b/lacme-accountd index 9909cb8..0f5deb2 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -23,7 +23,7 @@ use v5.14.2; use strict; use warnings; -our $VERSION = '0.3'; +our $VERSION = '0.8.0'; my $PROTOCOL_VERSION = 1; my $NAME = 'lacme-accountd'; -- cgit v1.2.3