From fbcd17c52091cb51a86f0ab2acb5348a12b613e0 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 12:06:09 +0100 Subject: In lacme's the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config'. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This matches the arguably expected behavior that ‘config = %h/foo’ is passed as ‘--config=%h/foo’ and resolved by lacme-accountd(1) (possibly remote and with another passwd database). --- Changelog | 7 +++++++ lacme | 2 +- lacme.8.md | 5 ++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index 9f12237..3765bf9 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,10 @@ +lacme (0.8.1) upstream; + + - lacme: in the [accountd] config, let lacme-accountd(1) do the + %-expansion for 'config', not lacme(8) when building the command. + + -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 + lacme (0.8.0) upstream; * Breaking change: 'challenge-directory' now needs to be set to an diff --git a/lacme b/lacme index 731535f..9691888 100755 --- a/lacme +++ b/lacme @@ -536,7 +536,7 @@ sub acme_client($@) { 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='.spec_expand($accountd->{config}) if $accountd->{config} ne ''; + push @args, '--config='.$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}; diff --git a/lacme.8.md b/lacme.8.md index ad6dab6..c39f51c 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -322,9 +322,8 @@ UNIX-domain socket. *config* -: Path to the [`lacme-accountd`(1)] configuration file. The value is - subject to [%-specifier expansion](#percent-specifiers) _after_ - privilege drop. +: Path to the [`lacme-accountd`(1)] configuration file. Note that the + value might be subject to %-expansion by [`lacme-accountd`(1)]. *quiet* -- cgit v1.2.3 From 6f375631548a3562635af555bd453e4de40bf135 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 14:33:34 +0100 Subject: accountd::conn(): Minor refactoring. --- lacme-accountd | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lacme-accountd b/lacme-accountd index 0f5deb2..5794ec1 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -239,19 +239,22 @@ sub conn($$$) { $data =~ s/\r\n\z// or panic(); my ($header, $payload) = split(/\./, $data, 2); - unless (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) { + if (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) { + $header = decode_base64url($header); + } else { 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 + if (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) { + # empty payloads are valid, cf. POST-as-GET + $payload = decode_base64url($payload); + } else { info("[$id] >>> Error: Refusing to sign request: Malformed payload"); last; } - logmsg(noquiet => "[$id] >>> OK signing request: ", - "header=base64url(", decode_base64url($header), "); ", - "playload=base64url(", decode_base64url($payload), ")"); + my $req = "header=base64url($header); playload=base64url($payload)"; + logmsg(noquiet => "[$id] >>> OK signing request: ", $req); my $sig = $SIGN->($data); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; -- cgit v1.2.3 From 87fa9468a26c1902423839473049cd3325098c1a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 14:49:00 +0100 Subject: lacme-account: Improve log messages. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Again… --- Changelog | 1 + lacme-accountd | 23 ++++++++++++----------- tests/accountd | 6 +++--- tests/accountd-kid | 8 ++++---- tests/accountd-remote | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Changelog b/Changelog index 3765bf9..faf32a8 100644 --- a/Changelog +++ b/Changelog @@ -1,5 +1,6 @@ lacme (0.8.1) upstream; + + lacme-accountd: improve log messages. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. diff --git a/lacme-accountd b/lacme-accountd index 5794ec1..68d0f39 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -83,7 +83,8 @@ sub error(@) { } sub panic(@) { my @loc = caller; - my @msg = (@_, " at line $loc[2] in $loc[1]"); + my @msg = ("PANIC at line $loc[2] in $loc[1]"); + push @msg, ": ", @_ if @_; info(@msg); exit 255; } @@ -234,29 +235,29 @@ sub conn($$$) { $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())) { $data =~ s/\r\n\z// or panic(); + # validate JWS Signing Input from RFC 7515: + # ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload)) my ($header, $payload) = split(/\./, $data, 2); if (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) { $header = decode_base64url($header); } else { - info("[$id] >>> Error: Refusing to sign request: Malformed protected header"); + info("[$id] NOSIGN [malformed JWS Protected Header]"); last; } if (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) { - # empty payloads are valid, cf. POST-as-GET + # empty payloads are valid, and used for POST-as-GET (RFC 8555 sec. 6.3) $payload = decode_base64url($payload); } else { - info("[$id] >>> Error: Refusing to sign request: Malformed payload"); + info("[$id] NOSIGN [malformed JWS Payload]"); last; } - my $req = "header=base64url($header); playload=base64url($payload)"; - logmsg(noquiet => "[$id] >>> OK signing request: ", $req); - - my $sig = $SIGN->($data); + my $req = "header=base64url($header) playload=base64url($payload)"; + my $sig = $SIGN->($data) // panic(); + logmsg(noquiet => "[$id] SIGNED ", $req); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; } } @@ -270,9 +271,9 @@ if (defined $OPTS{stdio}) { next if $! == EINTR; # try again if accept(2) was interrupted by a signal panic("accept: $!"); }; - logmsg(noquiet => "[$count] >>> Accepted new connection"); + logmsg(noquiet => "[$count] Accepted new connection"); conn($conn, $conn, $count); - logmsg(noquiet => "[$count] >>> Connection terminated"); + logmsg(noquiet => "[$count] Connection terminated"); $conn->close() or warn "close: $!"; } } diff --git a/tests/accountd b/tests/accountd index a603c16..7e8fd4c 100644 --- a/tests/accountd +++ b/tests/accountd @@ -79,9 +79,9 @@ wait # 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 "[0] Accepted new connection" ~lacme-account/.local/share/lacme/accountd.log +grep -Fq "[1] Accepted new connection" ~lacme-account/.local/share/lacme/accountd.log grep -Fq "Shutting down and closing lacme Account Key Manager" ~lacme-account/.local/share/lacme/accountd.log -grep -F ">>> OK signing request:" ~lacme-account/.local/share/lacme/accountd.log +grep -F "] SIGNED header=base64url({" ~lacme-account/.local/share/lacme/accountd.log # vim: set filetype=sh : diff --git a/tests/accountd-kid b/tests/accountd-kid index e1bd63d..f55facf 100644 --- a/tests/accountd-kid +++ b/tests/accountd-kid @@ -28,8 +28,8 @@ runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" --quiet & PID=$! ! 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 +grep -F "] SIGNED header=base64url({" ~lacme-account/.local/share/lacme/accountd.log >/tmp/signed +! grep -vF "] SIGNED header=base64url({\"alg\":\"RS256\",\"jwk\":{}," >> OK signing request: header=" ~lacme-account/.local/share/lacme/accountd.log | \ - grep -vF ">>> OK signing request: header=base64url({\"alg\":\"RS256\",\"kid\":\"$keyid\"," || exit 1 +grep -F "] SIGNED header=base64url({" ~lacme-account/.local/share/lacme/accountd.log >/tmp/signed +! grep -vF "] SIGNED header=base64url({\"alg\":\"RS256\",\"kid\":\"$keyid\"," >> OK signing request:" ~lacme-account/.local/share/lacme/accountd.log +grep -F "] SIGNED header=base64url({" ~lacme-account/.local/share/lacme/accountd.log # vim: set filetype=sh : -- cgit v1.2.3 From 045d169339c5b973f0924269e6ca485e48de3668 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 20:32:33 +0100 Subject: lacme-accountd: Refuse to sign JWS with an invalid Protected Header. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit “The JWS Protected Header is a JSON object” — RFC 7515 sec. 2. “The JWS Protected Header MUST include the following fields: - "alg" - "nonce" - "url" - either "jwk" or "kid"” — RFC 8555 sec. 6.2. --- Changelog | 1 + lacme-accountd | 13 +++++++++++++ tests/accountd-validate | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 tests/accountd-validate diff --git a/Changelog b/Changelog index faf32a8..da34ddc 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,7 @@ lacme (0.8.1) upstream; + lacme-accountd: improve log messages. + + lacme-accountd: refuse to sign JWS with an invalid Protected Header. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. diff --git a/lacme-accountd b/lacme-accountd index 68d0f39..5478cc2 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -256,6 +256,19 @@ sub conn($$$) { } my $req = "header=base64url($header) playload=base64url($payload)"; + + eval { $header = JSON::->new->decode($header); }; + if ($@ or # couldn't decode (parse error) + # RFC 7515: not a JSON object + !defined($header) or ref($header) ne "HASH" or + # RFC 8555 sec. 6.2: the protected Header MUST include all these fields + grep !defined, @$header{qw/alg nonce url/} or + # RFC 8555 sec. 6.2: the protected header MUST include any of these fields + !grep defined, @$header{qw/jwk kid/}) { + info("[$id] NOSIGN [invalid JWS Protected Header] ", $req); + last; + } + my $sig = $SIGN->($data) // panic(); logmsg(noquiet => "[$id] SIGNED ", $req); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; diff --git a/tests/accountd-validate b/tests/accountd-validate new file mode 100644 index 0000000..d4be5ee --- /dev/null +++ b/tests/accountd-validate @@ -0,0 +1,36 @@ +# JWS Signing Input (RFC 7515) validation + +# missing or empty protected header +printf "\\r\\n" | lacme-accountd --stdio 2>"$STDERR" +grepstderr -Fq "] NOSIGN [malformed JWS Protected Header]" +printf ".foo\\r\\n" | lacme-accountd --stdio 2>"$STDERR" +grepstderr -Fq "] NOSIGN [malformed JWS Protected Header]" + +# invalid base64url-encoded protected header +printf "foo/bar.baz\\r\\n" | lacme-accountd --stdio 2>"$STDERR" +grepstderr -Fq "] NOSIGN [malformed JWS Protected Header]" + +# missing payload +printf "foo\\r\\n" | lacme-accountd --stdio 2>"$STDERR" +grepstderr -Fq "] NOSIGN [malformed JWS Payload]" + +# invalid base64url-encoded payload +printf "foo.bar/baz\\r\\n" | lacme-accountd --stdio 2>"$STDERR" +grepstderr -Fq "] NOSIGN [malformed JWS Payload]" + +# invalid JWS Protected Header: not a JSON object; missing fields "alg", +# "nonce", "url", or either "jwk" or "kid" +for s in "null" "\"str\"" "{}" "{\"alg\":\"\",\"nonce\":\"\",\"url\":\"\"}" "{\"jwk\":{}}"; do + s="$(printf "%s" "$s" | base64 -w0 | sed "s/=*$//" | tr "+/" "-_")" + printf "%s.\\r\\n" "$s" | lacme-accountd --stdio 2>"$STDERR" + grepstderr -F "] NOSIGN [invalid JWS Protected Header]" +done + +# valid JWS Protected Header and Payload +h="{\"alg\":\"\",\"nonce\":\"\",\"url\":\"\",\"jwk\":{}}" +s="$(printf "%s" "$h" | base64 -w0 | sed "s/=*$//" | tr "+/" "-_")" +p="$(printf "%s" "JWS Payload" | base64 -w0 | sed "s/=*$//" | tr "+/" "-_")" +printf "%s.%s\\r\\n" "$s" "$p" | lacme-accountd --stdio 2>"$STDERR" +grepstderr -F "] SIGNED header=base64url($h) playload=base64url(JWS Payload)" + +# vim: set filetype=sh : -- cgit v1.2.3 From 7bdf10e68ba849c9314b3c139a434e27efd1e192 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 21:38:56 +0100 Subject: test: Allow prefixing test names with 'tests/'. It's handy to be able to run `./test tests/accountd*` or similar. --- test | 1 + 1 file changed, 1 insertion(+) diff --git a/test b/test index 81d910c..c1341e9 100755 --- a/test +++ b/test @@ -62,6 +62,7 @@ if [ $# -eq 0 ]; then done else for t in "$@"; do + t="${t#tests/}" if [ -f "tests/$t" ]; then TESTS+=( "$t" ) else -- cgit v1.2.3 From a3a7dd6780e77da26c25b026a4453f7f5b2c00ae Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 22:09:11 +0100 Subject: test suite: Avoid setting twice the ACME API server URL. --- test | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test b/test index c1341e9..5200974 100755 --- a/test +++ b/test @@ -34,6 +34,7 @@ usage() { # 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" +ACMEAPI_SERVER="https://acme-staging-v02.api.letsencrypt.org/directory" MODE="dev" DISTRIBUTION="sid" @@ -117,7 +118,7 @@ run() { lacme_www_group=nogroup \ lacme_client_user=_lacme-client \ lacme_client_group=nogroup \ - acmeapi_server="https://acme-staging-v02.api.letsencrypt.org/directory" + acmeapi_server="$ACMEAPI_SERVER" CHROOT="$(schroot -c "$DISTRIBUTION-$ARCH-sbuild" -b)" rootdir="/run/schroot/mount/$CHROOT" @@ -168,8 +169,7 @@ run() { 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 + sed -ri "0,/^#?server\\s*=.*/ {s||server = $ACMEAPI_SERVER|}" /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 0fb2ebb14c538d736d9260fc6fae51d02375a999 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 23:20:15 +0100 Subject: lacme-accountd: panic() upon internal error of the signing routine. It might croak and we want to log that error also. --- lacme-accountd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lacme-accountd b/lacme-accountd index 5478cc2..5109888 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -269,7 +269,8 @@ sub conn($$$) { last; } - my $sig = $SIGN->($data) // panic(); + my $sig = eval { $SIGN->($data) }; + panic($@) if $@ or !defined $sig; logmsg(noquiet => "[$id] SIGNED ", $req); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; } -- cgit v1.2.3 From af5e3d794fc2f83f6cc3b5ddff386dad5463707d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 23 Feb 2021 00:20:32 +0100 Subject: Consolidate error messages. --- client | 10 +++++----- tests/accountd-kid | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client b/client index fdef865..6438f6a 100755 --- a/client +++ b/client @@ -87,13 +87,13 @@ do { 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"; + print STDERR "Warning: 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, $KID) = @$h{qw/jwk-thumbprint alg kid/}; } } - my $jwk_str = $S->getline() // die "ERROR: No JWK from lacme-accountd\n"; + 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"; @@ -210,7 +210,7 @@ sub acme2($$;$) { $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"; + my $sig = $S->getline() // die "Error: lost connection with lacme-accountd\n"; $sig =~ s/\r\n\z// or die; undef $NONCE; # consume the nonce @@ -249,7 +249,7 @@ sub acme_resource($%) { 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' ", + 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); @@ -342,7 +342,7 @@ elsif ($COMMAND eq 'newOrder') { $fh->print($keyAuthorization); $fh->close() or die "close: $!"; } elsif ($! == EEXIST) { - print STDERR "WARNING: File exists: $challenge->{token}\n"; + print STDERR "Warning: File exists: $challenge->{token}\n"; } else { die "open($challenge->{token}): $!"; } diff --git a/tests/accountd-kid b/tests/accountd-kid index f55facf..1f282fd 100644 --- a/tests/accountd-kid +++ b/tests/accountd-kid @@ -26,7 +26,7 @@ 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 "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 "] SIGNED header=base64url({" ~lacme-account/.local/share/lacme/accountd.log >/tmp/signed ! grep -vF "] SIGNED header=base64url({\"alg\":\"RS256\",\"jwk\":{}," Date: Tue, 23 Feb 2021 00:28:56 +0100 Subject: lacme-accountd: don't log debug messages unless --debug is set. --- Changelog | 1 + lacme-accountd | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index da34ddc..c4dd0fc 100644 --- a/Changelog +++ b/Changelog @@ -4,6 +4,7 @@ lacme (0.8.1) upstream; + lacme-accountd: refuse to sign JWS with an invalid Protected Header. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. + - lacme-accountd: don't log debug messages unless --debug is set. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/lacme-accountd b/lacme-accountd index 5109888..47a4c32 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -67,7 +67,7 @@ usage(0) if $OPTS{help}; my $LOG; sub logmsg($@) { my $lvl = shift // "all"; - if (defined $LOG) { + if (defined $LOG and ($lvl ne "debug" or $OPTS{debug})) { my $now = localtime; $LOG->printflush("[", $now, "] ", @_, "\n") or warn "print: $!"; } -- cgit v1.2.3 From 3a527c2159cdd23f489970f935edbccc37da1901 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 23 Feb 2021 00:58:46 +0100 Subject: lacme-accountd: Refactor logging logic. --- Changelog | 2 +- lacme-accountd | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Changelog b/Changelog index c4dd0fc..b7459fd 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,6 @@ lacme (0.8.1) upstream; - + lacme-accountd: improve log messages. + + lacme-accountd: improve log messages and refactor logging logic. + lacme-accountd: refuse to sign JWS with an invalid Protected Header. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. diff --git a/lacme-accountd b/lacme-accountd index 47a4c32..a35ac88 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -64,18 +64,21 @@ 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; +my ($LOG, $LOGLEVEL); +my ($LOG_INFO, $LOG_VERBOSE, $LOG_DEBUG) = (0,1,2); sub logmsg($@) { - my $lvl = shift // "all"; - if (defined $LOG and ($lvl ne "debug" or $OPTS{debug})) { + my $lvl = shift; + if (defined $LOG and ($lvl <= $LOGLEVEL or $lvl <= $LOG_VERBOSE)) { + # --quiet flag hides verbose-level messages from the standard + # error but we add them to the logfile nonetheless my $now = localtime; $LOG->printflush("[", $now, "] ", @_, "\n") or warn "print: $!"; } - unless (($lvl eq "debug" and !$OPTS{debug}) or ($lvl eq "noquiet" and $OPTS{quiet})) { + if ($lvl <= $LOGLEVEL) { print STDERR @_, "\n" or warn "print: $!"; } } -sub info(@) { logmsg(all => @_); } +sub info(@) { logmsg($LOG_INFO => @_); } sub error(@) { my @msg = ("Error: ", @_); info(@msg); @@ -134,7 +137,7 @@ do { print STDERR "Ignoring missing configuration file at default location $conffile\n" if $OPTS{debug}; } - $OPTS{quiet} = 0 if $OPTS{debug}; + $LOGLEVEL = $OPTS{debug} ? $LOG_DEBUG : $OPTS{quiet} ? $LOG_INFO : $LOG_VERBOSE; error("'privkey' is not specified") unless defined $OPTS{privkey}; }; @@ -214,7 +217,7 @@ unless (defined $OPTS{stdio}) { my $umask = umask(0177) // panic("umask: $!"); - logmsg(noquiet => "Starting lacme Account Key Manager at $sockname"); + logmsg($LOG_VERBOSE => "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: $!"); @@ -271,7 +274,7 @@ sub conn($$$) { my $sig = eval { $SIGN->($data) }; panic($@) if $@ or !defined $sig; - logmsg(noquiet => "[$id] SIGNED ", $req); + logmsg($LOG_VERBOSE => "[$id] SIGNED ", $req); $out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!"; } } @@ -285,9 +288,9 @@ if (defined $OPTS{stdio}) { next if $! == EINTR; # try again if accept(2) was interrupted by a signal panic("accept: $!"); }; - logmsg(noquiet => "[$count] Accepted new connection"); + logmsg($LOG_VERBOSE => "[$count] Accepted new connection"); conn($conn, $conn, $count); - logmsg(noquiet => "[$count] Connection terminated"); + logmsg($LOG_VERBOSE => "[$count] Connection terminated"); $conn->close() or warn "close: $!"; } } @@ -297,11 +300,11 @@ if (defined $OPTS{stdio}) { # END { if (defined $SOCKNAME and -S $SOCKNAME) { - logmsg(debug => "Unlinking $SOCKNAME"); + logmsg($LOG_DEBUG => "Unlinking $SOCKNAME"); unlink $SOCKNAME or info("Error: unlink($SOCKNAME): $!"); } if (defined $S) { - logmsg(noquiet => "Shutting down and closing lacme Account Key Manager"); + logmsg($LOG_VERBOSE => "Shutting down and closing lacme Account Key Manager"); shutdown($S, SHUT_RDWR) or info("Error: shutdown: $!"); close $S or info("Error: close: $!"); } -- cgit v1.2.3 From 016c9611970c0667ad02cb1cf31834f2325b1575 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 12:56:28 +0100 Subject: lacme: When getpwnam()/getgrnam()'s errno is 0, exclude it from error messages. --- Changelog | 2 ++ lacme | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Changelog b/Changelog index b7459fd..8b90177 100644 --- a/Changelog +++ b/Changelog @@ -5,6 +5,8 @@ lacme (0.8.1) upstream; - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. - lacme-accountd: don't log debug messages unless --debug is set. + - lacme: when getpwnam()/getgrnam()'s errno is 0, exclude it from error + messages. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/lacme b/lacme index 9691888..a1e6b10 100755 --- a/lacme +++ b/lacme @@ -240,7 +240,7 @@ 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 "getgrnam($group): $!"; + my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n"); $) = "$gid $gid"; die "setgroups: $!" if $@; POSIX::setgid($gid) or die "setgid: $!"; @@ -249,7 +249,7 @@ sub drop_privileges($$$) { # set effective and real uid if ($user ne '') { - my $uid = getpwnam($user) // die "getpwnam($user): $!"; + my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n"); POSIX::setuid($uid) or die "setuid: $!"; die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check } @@ -351,7 +351,7 @@ sub spawn_webserver() { 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): $!"; + my $uid = getpwnam($username) // die "getpwnam($username)", ($! ? ": $!" : "\n"); chown($uid, -1, $tmpdir) or die "chown: $!"; } @@ -849,10 +849,10 @@ 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 "getpwnam($user): $!"; - my $gid = defined $group ? (getgrnam($group) // die "getgrnam($group): $!") : -1; + my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n"); + my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n") if defined $group; foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chown($uid, $gid, $_) or die "chown: $!"; + chown($uid, $gid // -1, $_) or die "chown: $!"; } } if (defined $conf->{chmod}) { -- cgit v1.2.3 From 83bcf394a15c4c2797353c040f1814c6b03b5db3 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 13:00:32 +0100 Subject: tests/drop-privileges: Ensure failure to drop privileges yields an error. And doesn't retain root privileges. --- Changelog | 2 ++ tests/drop-privileges | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 8b90177..ae42df7 100644 --- a/Changelog +++ b/Changelog @@ -7,6 +7,8 @@ lacme (0.8.1) upstream; - lacme-accountd: don't log debug messages unless --debug is set. - lacme: when getpwnam()/getgrnam()'s errno is 0, exclude it from error messages. + - tests/drop-privileges: ensure failure to drop privileges yields an + error instead of retaining root priviliges. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/tests/drop-privileges b/tests/drop-privileges index 0596e31..fd432d9 100644 --- a/tests/drop-privileges +++ b/tests/drop-privileges @@ -1,6 +1,17 @@ # Check privilige drop: UID/GID changes, chdir, environment, and file # descriptors +# ensure failure to drop privileges doesn't retain root privileges +sed -ri 's/^#(user|group)\s*=\s*$/\1 = nonexistent-\1/' /etc/lacme/lacme.conf +! lacme account 2>"$STDERR" || fail +grepstderr -Fxq "getgrnam(nonexistent-group)" +grepstderr -Fxq "Error: Invalid client version" + +sed -ri 's/^group\s*=\s*nonexistent.*/#&/' /etc/lacme/lacme.conf +! lacme account 2>"$STDERR" || fail +grepstderr -Fxq "getpwnam(nonexistent-user)" +grepstderr -Fxq "Error: Invalid client version" + # create wrapper to inspect processes STATUSDIR="/dev/shm/lacme-wrap" install -oroot -groot -m0755 /dev/stdin /run/lacme-wrap <<-EOF @@ -24,8 +35,7 @@ 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 +sed -ri 's/^#?(user|group)\s*=\s*nonexistent.*/\1 = lacme-account/' /etc/lacme/lacme.conf chown lacme-account: /etc/lacme/account.key install -oroot -groot -dm0755 -- "$STATUSDIR" -- cgit v1.2.3 From bb3ef24a8d97dd9b0299cf23e4815c57c5ad7fb7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 13:17:43 +0100 Subject: typofix --- tests/cert-install | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cert-install b/tests/cert-install index f2147d2..347570c 100644 --- a/tests/cert-install +++ b/tests/cert-install @@ -103,7 +103,7 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test3.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test3.crt)" [ "$st" = "root:root 0644" ] -# chmod user +# chown user openssl genpkey -algorithm RSA -out /etc/lacme/test4.key cat >"/etc/lacme/lacme-certs.conf.d/test4.conf" <<- EOF [test4] @@ -120,7 +120,7 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test4.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test4.crt)" [ "$st" = "nobody:root 0644" ] -# chmod user:group +# chown user:group openssl genpkey -algorithm RSA -out /etc/lacme/test5.key cat >"/etc/lacme/lacme-certs.conf.d/test5.conf" <<- EOF [test5] @@ -137,7 +137,7 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)" [ "$st" = "nobody:nogroup 0644" ] -# chown +# chmod openssl genpkey -algorithm RSA -out /etc/lacme/test6.key cat >"/etc/lacme/lacme-certs.conf.d/test6.conf" <<- EOF [test6] -- cgit v1.2.3 From c96f887e5d8a1625f7dfb76d7f646499aead8eed Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 13:18:00 +0100 Subject: tab damage --- tests/cert-install | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cert-install b/tests/cert-install index 347570c..afc86c3 100644 --- a/tests/cert-install +++ b/tests/cert-install @@ -110,7 +110,7 @@ cat >"/etc/lacme/lacme-certs.conf.d/test4.conf" <<- EOF certificate-key = /etc/lacme/test4.key certificate = /etc/lacme/test4.pem certificate-chain = /etc/lacme/test4.crt - chown = nobody + chown = nobody subject = $subject EOF @@ -127,7 +127,7 @@ cat >"/etc/lacme/lacme-certs.conf.d/test5.conf" <<- EOF certificate-key = /etc/lacme/test5.key certificate = /etc/lacme/test5.pem certificate-chain = /etc/lacme/test5.crt - chown = nobody:nogroup + chown = nobody:nogroup subject = $subject EOF @@ -144,7 +144,7 @@ cat >"/etc/lacme/lacme-certs.conf.d/test6.conf" <<- EOF certificate-key = /etc/lacme/test6.key certificate = /etc/lacme/test6.pem certificate-chain = /etc/lacme/test6.crt - chmod = 0400 + chmod = 0400 subject = $subject EOF -- cgit v1.2.3 From 539e3a8b8a2baf6746716125e99231da14a153a9 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 13:19:21 +0100 Subject: tests/cert-install: Include tests for failing chown(2). Due to unknown user/group name. --- Changelog | 2 ++ tests/cert-install | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Changelog b/Changelog index ae42df7..ee90be3 100644 --- a/Changelog +++ b/Changelog @@ -9,6 +9,8 @@ lacme (0.8.1) upstream; messages. - tests/drop-privileges: ensure failure to drop privileges yields an error instead of retaining root priviliges. + - tests/cert-install: include tests for failing chown(2) due to unknown + user/group name. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/tests/cert-install b/tests/cert-install index afc86c3..39110f4 100644 --- a/tests/cert-install +++ b/tests/cert-install @@ -120,6 +120,15 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test4.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test4.crt)" [ "$st" = "nobody:root 0644" ] +rm -f /etc/lacme/test4.pem /etc/lacme/test4.crt +sed -ri "s/^chown\\s*=.*/chown = nonexistent-user/" /etc/lacme/lacme-certs.conf.d/test4.conf +! lacme newOrder test4 2>"$STDERR" || fail newOrder test4 +grepstderr -Fxq "getpwnam(nonexistent-user)" +st="$(stat -c "%U:%G %#a" /etc/lacme/test4.pem)" +[ "$st" = "root:root 0644" ] +st="$(stat -c "%U:%G %#a" /etc/lacme/test4.crt)" +[ "$st" = "root:root 0644" ] + # chown user:group openssl genpkey -algorithm RSA -out /etc/lacme/test5.key cat >"/etc/lacme/lacme-certs.conf.d/test5.conf" <<- EOF @@ -137,6 +146,15 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)" [ "$st" = "nobody:nogroup 0644" ] +rm -f /etc/lacme/test5.pem /etc/lacme/test5.crt +sed -ri "s/^chown\\s*=.*/chown = nobody:nonexistent-group/" /etc/lacme/lacme-certs.conf.d/test5.conf +! lacme newOrder test5 2>"$STDERR" || fail newOrder test5 +grepstderr -Fxq "getgrnam(nonexistent-group)" +st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)" +[ "$st" = "root:root 0644" ] +st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)" +[ "$st" = "root:root 0644" ] + # chmod openssl genpkey -algorithm RSA -out /etc/lacme/test6.key cat >"/etc/lacme/lacme-certs.conf.d/test6.conf" <<- EOF -- cgit v1.2.3 From d1a862d9cb98a54e12c9fdbc405b896f3f0efcfe Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 13:25:38 +0100 Subject: lacme: Ignore empty values in 'chown'/'chmod'/'certificate'/'certificate-chain'. --- Changelog | 2 ++ lacme | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Changelog b/Changelog index ee90be3..1682847 100644 --- a/Changelog +++ b/Changelog @@ -11,6 +11,8 @@ lacme (0.8.1) upstream; error instead of retaining root priviliges. - tests/cert-install: include tests for failing chown(2) due to unknown user/group name. + - lacme: ignore empty values in settings 'chown', 'chmod', 'certificate' + and 'certificate-chain'. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/lacme b/lacme index a1e6b10..66dd6f6 100755 --- a/lacme +++ b/lacme @@ -766,15 +766,15 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); } - my $cert = $conf->{'certificate-chain'} // $conf->{'certificate'}; - unless (defined $cert) { + my @certs = grep {defined $_ and $_ ne ""} @$conf{qw/certificate-chain certificate/}; + unless (@certs) { 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 $cert and defined (my $t = x509_enddate($cert))) { + if (-f $certs[0] and defined (my $t = x509_enddate($certs[0]))) { 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)); @@ -838,26 +838,26 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } # install certificate - if (defined $conf->{'certificate'}) { - print STDERR "Installing X.509 certificate $conf->{'certificate'}\n"; - install_cert($conf->{'certificate'}, $x509, 1); + if ((my $path = $conf->{'certificate'} // "") ne "") { + print STDERR "Installing X.509 certificate $path\n"; + install_cert($path, $x509, 1); } - if (defined $conf->{'certificate-chain'}) { - print STDERR "Installing X.509 certificate chain $conf->{'certificate-chain'}\n"; - install_cert($conf->{'certificate-chain'}, $x509); + if ((my $path = $conf->{'certificate-chain'} // "") ne "") { + print STDERR "Installing X.509 certificate chain $path\n"; + install_cert($path, $x509); } - if (defined $conf->{chown}) { - my ($user, $group) = split /:/, $conf->{chown}, 2; + if ((my $own = $conf->{chown} // "") ne "") { + my ($user, $group) = split /:/, $own, 2; my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n"); my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n") if defined $group; - foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { + foreach (@certs) { chown($uid, $gid // -1, $_) or die "chown: $!"; } } - if (defined $conf->{chmod}) { - my $mode = oct($conf->{chmod}) // die; - foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { + if ((my $mode = $conf->{chmod} // "") ne "") { + my $mode = oct($mode) // die; + foreach (@certs) { chmod($mode, $_) or die "chown: $!"; } } -- cgit v1.2.3 From faab30461b0f2b920e3dd19489ce458c0b38e6d9 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 21:06:48 +0100 Subject: If restricting access via umask() fails, don't include errno in the error message. errno is not set on umask failure, see https://perldoc.perl.org/functions/umask. --- Changelog | 2 ++ lacme | 4 ++-- lacme-accountd | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index 1682847..e047ac5 100644 --- a/Changelog +++ b/Changelog @@ -236,6 +236,8 @@ lacme (0.2) upstream; directories. New default "lacme-certs.conf lacme-certs.conf.d/". - Minor manpage fixes - More useful message upon Validation Challenge failure. + - If restricting access via umask() fails, don't include errno in the + error message as it's not set on failure. -- Guilhem Moulin Sat, 03 Dec 2016 16:40:56 +0100 diff --git a/lacme b/lacme index 66dd6f6..fb19646 100755 --- a/lacme +++ b/lacme @@ -376,14 +376,14 @@ sub spawn_webserver() { if ($domain == AF_UNIX) { # bind(2) with a loose umask(2) to allow anyone to connect - my $umask = umask(0111) // die "umask: $!"; + my $umask = umask(0111) // die; my $path = Socket::unpack_sockaddr_un($sockaddr); 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: Couldn't unlink $path: $!"; }; - umask($umask) // die "umask: $!"; + umask($umask) // die; } else { bind($sock, $sockaddr) or die "Couldn't bind to $p: $!"; diff --git a/lacme-accountd b/lacme-accountd index a35ac88..98c11ad 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -215,7 +215,7 @@ unless (defined $OPTS{stdio}) { my @stat = stat($dirname) or error("stat($dirname): $!"); error("Insecure permissions on $dirname") if ($stat[2] & 0022) != 0; - my $umask = umask(0177) // panic("umask: $!"); + my $umask = umask(0177) // panic(); logmsg($LOG_VERBOSE => "Starting lacme Account Key Manager at $sockname"); socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or panic("socket: $!"); @@ -225,7 +225,7 @@ unless (defined $OPTS{stdio}) { ($SOCKNAME, $S) = ($sockname, $sock); listen($S, 1) or panic("listen: $!"); - umask($umask) // panic("umask: $!"); + umask($umask) // panic(); }; -- cgit v1.2.3 From cdd025133a306cd8d3e81aa832ac056119d65f3a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 20:03:44 +0100 Subject: lacme: Don't write certificate(-chain) file on chown/chmod failure. Otherwise we end up with files with mode 0644 owned by root:root, and subsequent lacme(8) invocations will likely not renew them for a while. This change also saves a chown(2) call. And the new logic (chown resp. chmod from root:root resp. 0600) is safe if we ever include private key material in there too. --- Changelog | 1 + lacme | 54 +++++++++++++++++++++++++++++------------------------- tests/cert-install | 34 ++++++++++++++-------------------- 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/Changelog b/Changelog index e047ac5..2a027f1 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,7 @@ lacme (0.8.1) upstream; + lacme-accountd: improve log messages and refactor logging logic. + lacme-accountd: refuse to sign JWS with an invalid Protected Header. + + lacme: don't write certificate(-chain) file on chown/chmod failure. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. - lacme-accountd: don't log debug messages unless --debug is set. diff --git a/lacme b/lacme index fb19646..2366830 100755 --- a/lacme +++ b/lacme @@ -657,8 +657,9 @@ sub spawn($@) { ############################################################################# # Install the certificate (optionally excluding the chain of trust) # -sub install_cert($$;$) { - my ($filename, $chain, $leafonly) = @_; +sub install_cert(%) { + my %args = @_; + my $filename = $args{path} // die; my ($dirname, $basename) = $filename =~ /\A(.*)\/([^\/]+)\z/ ? ($1, $2) : ('.', $filename); @@ -666,10 +667,8 @@ sub install_cert($$;$) { TEMPLATE => "$basename.XXXXXX") // die; eval { - my $umask = umask() // die "umask: $!"; - chmod(0644 &~ $umask, $fh) or die "chmod: $!"; - if ($leafonly) { - # keep only the leaf certificate + if ($args{nochain}) { + # keep only the first certificate in the file pipe my $rd, my $wd or die "pipe: $!"; my $pid = fork // die "fork: $!"; unless ($pid) { @@ -678,16 +677,34 @@ sub install_cert($$;$) { exec qw/openssl x509 -outform PEM/ or die; } $rd->close() or die "close: $!"; - $wd->print($chain); + $wd->print($args{content}); $wd->close() or die "close: $!"; waitpid $pid => 0; die $? if $? > 0; } else { - $fh->print($chain) or die "print: $!"; + $fh->print($args{content}) or die "print: $!"; } + + my $mode; + if ((my $m = $args{mode}) ne "") { + $mode = oct($m) // die; + } else { + my $umask = umask() // die; + $mode = 0644 &~ $umask; + } + chmod($mode, $fh) or die "chown: $!"; + + if ((my $owner = $args{owner}) ne "") { + my ($user, $group) = split /:/, $owner, 2; + my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n"); + my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n") if defined $group; + chown($uid, $gid // -1, $fh) or die "chown: $!"; + } + $fh->close() or die "close: $!"; }; + my $path = $fh->filename(); if ($@) { print STDERR "Unlinking $path\n" if $OPTS{debug}; @@ -837,29 +854,16 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } } + my %install = ( content => $x509, mode => $conf->{chmod} // "", owner => $conf->{chown} // "" ); + # install certificate if ((my $path = $conf->{'certificate'} // "") ne "") { print STDERR "Installing X.509 certificate $path\n"; - install_cert($path, $x509, 1); + install_cert(%install, path => $path, nochain => 1); } if ((my $path = $conf->{'certificate-chain'} // "") ne "") { print STDERR "Installing X.509 certificate chain $path\n"; - install_cert($path, $x509); - } - - if ((my $own = $conf->{chown} // "") ne "") { - my ($user, $group) = split /:/, $own, 2; - my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n"); - my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n") if defined $group; - foreach (@certs) { - chown($uid, $gid // -1, $_) or die "chown: $!"; - } - } - if ((my $mode = $conf->{chmod} // "") ne "") { - my $mode = oct($mode) // die; - foreach (@certs) { - chmod($mode, $_) or die "chown: $!"; - } + install_cert(%install, path => $path); } my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump/; diff --git a/tests/cert-install b/tests/cert-install index 39110f4..5d8a239 100644 --- a/tests/cert-install +++ b/tests/cert-install @@ -110,25 +110,22 @@ cat >"/etc/lacme/lacme-certs.conf.d/test4.conf" <<- EOF certificate-key = /etc/lacme/test4.key certificate = /etc/lacme/test4.pem certificate-chain = /etc/lacme/test4.crt - chown = nobody + chown = nonexistent-user subject = $subject EOF +! lacme newOrder test4 2>"$STDERR" || fail newOrder test4 +grepstderr -Fxq "getpwnam(nonexistent-user)" +! test -e /etc/lacme/test4.pem +! test -e /etc/lacme/test4.crt + +sed -ri "s/^chown\\s*=.*/chown = nobody/" /etc/lacme/lacme-certs.conf.d/test4.conf 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" ] -rm -f /etc/lacme/test4.pem /etc/lacme/test4.crt -sed -ri "s/^chown\\s*=.*/chown = nonexistent-user/" /etc/lacme/lacme-certs.conf.d/test4.conf -! lacme newOrder test4 2>"$STDERR" || fail newOrder test4 -grepstderr -Fxq "getpwnam(nonexistent-user)" -st="$(stat -c "%U:%G %#a" /etc/lacme/test4.pem)" -[ "$st" = "root:root 0644" ] -st="$(stat -c "%U:%G %#a" /etc/lacme/test4.crt)" -[ "$st" = "root:root 0644" ] - # chown user:group openssl genpkey -algorithm RSA -out /etc/lacme/test5.key cat >"/etc/lacme/lacme-certs.conf.d/test5.conf" <<- EOF @@ -136,25 +133,22 @@ cat >"/etc/lacme/lacme-certs.conf.d/test5.conf" <<- EOF certificate-key = /etc/lacme/test5.key certificate = /etc/lacme/test5.pem certificate-chain = /etc/lacme/test5.crt - chown = nobody:nogroup + chown = nobody:nonexistent-group subject = $subject EOF +! lacme newOrder test5 2>"$STDERR" || fail newOrder test5 +grepstderr -Fxq "getgrnam(nonexistent-group)" +! test -e /etc/lacme/test5.pem +! test -e /etc/lacme/test5.crt + +sed -ri "s/^chown\\s*=.*/chown = nobody:nogroup/" /etc/lacme/lacme-certs.conf.d/test5.conf 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" ] -rm -f /etc/lacme/test5.pem /etc/lacme/test5.crt -sed -ri "s/^chown\\s*=.*/chown = nobody:nonexistent-group/" /etc/lacme/lacme-certs.conf.d/test5.conf -! lacme newOrder test5 2>"$STDERR" || fail newOrder test5 -grepstderr -Fxq "getgrnam(nonexistent-group)" -st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)" -[ "$st" = "root:root 0644" ] -st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)" -[ "$st" = "root:root 0644" ] - # chmod openssl genpkey -algorithm RSA -out /etc/lacme/test6.key cat >"/etc/lacme/lacme-certs.conf.d/test6.conf" <<- EOF -- cgit v1.2.3 From c612a7ff44995f4f9c39fa0fb68470d90c88decf Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 21:01:12 +0100 Subject: lacme: Default mode for certificate(-chain) creation is 0644 minus umask restrictions. Also, always spawn the client with umask 0022 so a starting lacme(8) with a restrictive umask doesn't impede serving challenge response files. --- Changelog | 4 ++++ client | 3 ++- lacme | 1 + lacme.8.md | 3 ++- tests/cert-install | 45 +++++++++++++++++++++++++++++++-------------- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/Changelog b/Changelog index 2a027f1..f7f11f6 100644 --- a/Changelog +++ b/Changelog @@ -3,6 +3,10 @@ lacme (0.8.1) upstream; + lacme-accountd: improve log messages and refactor logging logic. + lacme-accountd: refuse to sign JWS with an invalid Protected Header. + lacme: don't write certificate(-chain) file on chown/chmod failure. + + lacme: default mode for certificate(-chain) creation is 0644 minus + umask restrictions. Also, always spawn the client with umask 0022 so + a starting lacme(8) with a restrictive umask doesn't impede serving + challenge files. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. - lacme-accountd: don't log debug messages unless --debug is set. diff --git a/client b/client index 6438f6a..33189d3 100755 --- a/client +++ b/client @@ -338,7 +338,8 @@ elsif ($COMMAND eq 'newOrder') { my $keyAuthorization = $challenge->{token}.'.'.$JWK_thumbprint; # serve $keyAuthorization at http://$domain/.well-known/acme-challenge/$challenge->{token} - if (sysopen(my $fh, $challenge->{token}, O_CREAT|O_EXCL|O_WRONLY, 0644)) { + if (sysopen(my $fh, $challenge->{token}, O_CREAT|O_EXCL|O_WRONLY)) { + # note: the file is created mode 0666 minus umask restrictions $fh->print($keyAuthorization); $fh->close() or die "close: $!"; } elsif ($! == EEXIST) { diff --git a/lacme b/lacme index 2366830..9012890 100755 --- a/lacme +++ b/lacme @@ -581,6 +581,7 @@ sub acme_client($@) { set_FD_CLOEXEC($client, 1); my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); + umask(0022) // die; set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; $ENV{DEBUG} = $OPTS{debug} // 0; diff --git a/lacme.8.md b/lacme.8.md index c39f51c..7f6558e 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -429,7 +429,8 @@ Valid settings are: *chmod* : An optional octal mode to chmod the issued *certificate* and - *certificate-chain* to. + *certificate-chain* to. By default the files are created with mode + 0644 minus umask restrictions. *notify* diff --git a/tests/cert-install b/tests/cert-install index 5d8a239..c49a294 100644 --- a/tests/cert-install +++ b/tests/cert-install @@ -149,40 +149,57 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)" [ "$st" = "nobody:nogroup 0644" ] -# chmod +# umask restrictions (also test empty values) 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 + certificate = + chmod = + chown = 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" ] +( umask 0077 && lacme newOrder test6 2>"$STDERR" || fail newOrder test6 ) +! test -e /etc/lacme/test6.pem st="$(stat -c "%U:%G %#a" /etc/lacme/test6.crt)" -[ "$st" = "root:root 0400" ] +[ "$st" = "root:root 0600" ] -# post-issuance notification +# chmod 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 = /etc/lacme/test7.pem certificate-chain = /etc/lacme/test7.crt + chmod = 0400 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 +st="$(stat -c "%U:%G %#a" /etc/lacme/test7.pem)" +[ "$st" = "root:root 0400" ] +st="$(stat -c "%U:%G %#a" /etc/lacme/test7.crt)" +[ "$st" = "root:root 0400" ] -rm -f /tmp/test7.notify -lacme newOrder test7 2>"$STDERR" || fail newOrder test7 +# post-issuance notification +openssl genpkey -algorithm RSA -out /etc/lacme/test8.key +cat >"/etc/lacme/lacme-certs.conf.d/test8.conf" <<- EOF + [test8] + certificate-key = /etc/lacme/test8.key + certificate-chain = /etc/lacme/test8.crt + subject = $subject + notify = touch /tmp/test8.notify +EOF + +lacme newOrder test8 2>"$STDERR" || fail newOrder test8 +grepstderr -Fxq "Running notification command \`touch /tmp/test8.notify\`" +test -e /tmp/test8.notify + +rm -f /tmp/test8.notify +lacme newOrder test8 2>"$STDERR" || fail newOrder test8 ngrepstderr -Fq "Running notification command" -! test -e /tmp/test7.notify +! test -e /tmp/test8.notify # vim: set filetype=sh : -- cgit v1.2.3 From c6a4aaa6128d55ba5f7f3cd2bd75f789f69ae407 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 21:24:13 +0100 Subject: lacme: Add 'owner' resp. 'mode' as (prefered) alias for 'chown' resp. 'chmod'. --- Changelog | 2 ++ config/lacme-certs.conf | 4 ++-- lacme | 8 ++++++-- lacme.8.md | 4 ++-- tests/cert-install | 20 ++++++++++---------- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Changelog b/Changelog index f7f11f6..14add81 100644 --- a/Changelog +++ b/Changelog @@ -7,6 +7,8 @@ lacme (0.8.1) upstream; umask restrictions. Also, always spawn the client with umask 0022 so a starting lacme(8) with a restrictive umask doesn't impede serving challenge files. + + lacme: add 'owner' resp. 'mode' as (prefered) alias for 'chown' resp. + 'chmod'. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. - lacme-accountd: don't log debug messages unless --debug is set. diff --git a/config/lacme-certs.conf b/config/lacme-certs.conf index 5259690..4af5652 100644 --- a/config/lacme-certs.conf +++ b/config/lacme-certs.conf @@ -52,11 +52,11 @@ # username[:groupname] to chown the issued certificate and # certificate-chain with. # -#chown = root:root +#owner = root:root # Octal mode to chmod the issued certificate and certificate-chain with. # -#chmod = 0644 +#mode = 0644 # Command to pass the the system's command shell ("/bin/sh -c") after # successful installation of the certificate and/or certificate-chain. diff --git a/lacme b/lacme index 9012890..2d9202d 100755 --- a/lacme +++ b/lacme @@ -761,7 +761,8 @@ 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 tlsfeature chown chmod notify/; + hash keyUsage subject subjectAltName tlsfeature + owner chown mode 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}}; @@ -855,7 +856,10 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } } - my %install = ( content => $x509, mode => $conf->{chmod} // "", owner => $conf->{chown} // "" ); + my %install = ( content => $x509, + mode => $conf->{mode} // $conf->{chmod} // "", + owner => $conf->{owner} // $conf->{chown} // "" + ); # install certificate if ((my $path = $conf->{'certificate'} // "") ne "") { diff --git a/lacme.8.md b/lacme.8.md index 7f6558e..65f1c36 100644 --- a/lacme.8.md +++ b/lacme.8.md @@ -421,12 +421,12 @@ Valid settings are: See [`x509v3_config`(5ssl)] for a list of possible values. Note that the ACME server might override the value provided here. -*chown* +*owner*, *chown* : An optional `username[:groupname]` to chown the issued *certificate* and *certificate-chain* to. -*chmod* +*mode*, *chmod* : An optional octal mode to chmod the issued *certificate* and *certificate-chain* to. By default the files are created with mode diff --git a/tests/cert-install b/tests/cert-install index c49a294..4b3e820 100644 --- a/tests/cert-install +++ b/tests/cert-install @@ -103,14 +103,14 @@ st="$(stat -c "%U:%G %#a" /etc/lacme/test3.pem)" st="$(stat -c "%U:%G %#a" /etc/lacme/test3.crt)" [ "$st" = "root:root 0644" ] -# chown user +# owner 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 = nonexistent-user + owner = nonexistent-user subject = $subject EOF @@ -119,21 +119,21 @@ grepstderr -Fxq "getpwnam(nonexistent-user)" ! test -e /etc/lacme/test4.pem ! test -e /etc/lacme/test4.crt -sed -ri "s/^chown\\s*=.*/chown = nobody/" /etc/lacme/lacme-certs.conf.d/test4.conf +sed -ri "s/^owner\\s*=.*/owner = nobody/" /etc/lacme/lacme-certs.conf.d/test4.conf 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" ] -# chown user:group +# owner 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:nonexistent-group + owner = nobody:nonexistent-group subject = $subject EOF @@ -142,7 +142,7 @@ grepstderr -Fxq "getgrnam(nonexistent-group)" ! test -e /etc/lacme/test5.pem ! test -e /etc/lacme/test5.crt -sed -ri "s/^chown\\s*=.*/chown = nobody:nogroup/" /etc/lacme/lacme-certs.conf.d/test5.conf +sed -ri "s/^owner\\s*=.*/owner = nobody:nogroup/" /etc/lacme/lacme-certs.conf.d/test5.conf lacme newOrder test5 2>"$STDERR" || fail newOrder test5 st="$(stat -c "%U:%G %#a" /etc/lacme/test5.pem)" [ "$st" = "nobody:nogroup 0644" ] @@ -156,8 +156,8 @@ cat >"/etc/lacme/lacme-certs.conf.d/test6.conf" <<- EOF certificate-key = /etc/lacme/test6.key certificate-chain = /etc/lacme/test6.crt certificate = - chmod = - chown = + mode = + owner = subject = $subject EOF @@ -166,14 +166,14 @@ EOF st="$(stat -c "%U:%G %#a" /etc/lacme/test6.crt)" [ "$st" = "root:root 0600" ] -# chmod +# mode 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 = /etc/lacme/test7.pem certificate-chain = /etc/lacme/test7.crt - chmod = 0400 + mode = 0400 subject = $subject EOF -- cgit v1.2.3 From ea5a51ecaa72c8277b4f878cf3635025d757fa37 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 21:28:31 +0100 Subject: lacme: Return an error when the 'mode'/'chown' isn't a number. oct("foobar") is 0, definitely not what we want. --- Changelog | 1 + lacme | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog b/Changelog index 14add81..92b8a4d 100644 --- a/Changelog +++ b/Changelog @@ -20,6 +20,7 @@ lacme (0.8.1) upstream; user/group name. - lacme: ignore empty values in settings 'chown', 'chmod', 'certificate' and 'certificate-chain'. + - lacme: return an error when the 'mode'/'chown' isn't a number. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/lacme b/lacme index 2d9202d..b52cddd 100755 --- a/lacme +++ b/lacme @@ -689,7 +689,8 @@ sub install_cert(%) { my $mode; if ((my $m = $args{mode}) ne "") { - $mode = oct($m) // die; + die "Not an octal string: $m\n" unless $m =~ /^[0-9]+$/; + $mode = oct($m); } else { my $umask = umask() // die; $mode = 0644 &~ $umask; -- cgit v1.2.3 From 491998131f18d136ca37f15898d07062ad7a1fae Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 24 Feb 2021 21:50:11 +0100 Subject: lacme: improve install_cert()'s handling of temporary files. --- lacme | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/lacme b/lacme index b52cddd..102deb6 100755 --- a/lacme +++ b/lacme @@ -660,12 +660,10 @@ sub spawn($@) { # sub install_cert(%) { my %args = @_; - my $filename = $args{path} // die; + my $path = $args{path} // die; - my ($dirname, $basename) = - $filename =~ /\A(.*)\/([^\/]+)\z/ ? ($1, $2) : ('.', $filename); - my $fh = File::Temp::->new(UNLINK => 0, DIR => $dirname, - TEMPLATE => "$basename.XXXXXX") // die; + my $fh = File::Temp::->new(TEMPLATE => "$path.XXXXXXXXXX", UNLINK => 0) // die; + my $path_tmp = $fh->filename(); eval { if ($args{nochain}) { @@ -707,13 +705,14 @@ sub install_cert(%) { $fh->close() or die "close: $!"; }; - my $path = $fh->filename(); if ($@) { - print STDERR "Unlinking $path\n" if $OPTS{debug}; - unlink $path or warn "unlink($path): $!"; + print STDERR "Unlinking $path_tmp\n" if $OPTS{debug}; + unlink $path_tmp or warn "unlink($path_tmp): $!"; die $@; + } else { + # atomically replace $path if it exists + rename($path_tmp, $path) or die "rename($path_tmp, $path): $!"; } - rename($path, $filename) or die "rename($path, $filename): $!"; } -- cgit v1.2.3 From f09c95ea97c9bdee92f7c7622689aed540373a73 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 25 Feb 2021 00:30:37 +0100 Subject: lacme: split certificates using Net::SSLeay::PEM_* instead of calling openssl. --- Changelog | 2 ++ lacme | 72 +++++++++++++++++++++++++++++++-------------------------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/Changelog b/Changelog index 92b8a4d..d63c754 100644 --- a/Changelog +++ b/Changelog @@ -9,6 +9,8 @@ lacme (0.8.1) upstream; challenge files. + lacme: add 'owner' resp. 'mode' as (prefered) alias for 'chown' resp. 'chmod'. + + lacme: split certificates using Net::SSLeay::PEM_* instead of calling + openssl. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. - lacme-accountd: don't log debug messages unless --debug is set. diff --git a/lacme b/lacme index 102deb6..13c2ef5 100755 --- a/lacme +++ b/lacme @@ -37,7 +37,7 @@ use Socket 1.95 qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC use Config::Tiny (); use Date::Parse (); -use Net::SSLeay (); +use Net::SSLeay 1.46 (); # Clean up PATH $ENV{PATH} = join ':', qw{/usr/bin /bin}; @@ -658,32 +658,14 @@ sub spawn($@) { ############################################################################# # Install the certificate (optionally excluding the chain of trust) # -sub install_cert(%) { - my %args = @_; - my $path = $args{path} // die; +sub install_cert($$%) { + my ($path, $content, %args) = @_; my $fh = File::Temp::->new(TEMPLATE => "$path.XXXXXXXXXX", UNLINK => 0) // die; my $path_tmp = $fh->filename(); eval { - if ($args{nochain}) { - # keep only the first certificate in the file - pipe my $rd, my $wd or die "pipe: $!"; - my $pid = fork // die "fork: $!"; - unless ($pid) { - open STDIN, '<&', $rd or die "dup: $!"; - open STDOUT, '>&', $fh or die "dup: $!"; - exec qw/openssl x509 -outform PEM/ or die; - } - $rd->close() or die "close: $!"; - $wd->print($args{content}); - $wd->close() or die "close: $!"; - - waitpid $pid => 0; - die $? if $? > 0; - } else { - $fh->print($args{content}) or die "print: $!"; - } + $fh->print($content) or die "print: $!"; my $mode; if ((my $m = $args{mode}) ne "") { @@ -785,15 +767,15 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); } - my @certs = grep {defined $_ and $_ ne ""} @$conf{qw/certificate-chain certificate/}; - unless (@certs) { + my @certpaths = grep {defined $_ and $_ ne ""} @$conf{qw/certificate-chain certificate/}; + unless (@certpaths) { 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 $certs[0] and defined (my $t = x509_enddate($certs[0]))) { + if (-f $certpaths[0] and defined (my $t = x509_enddate($certpaths[0]))) { 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)); @@ -826,18 +808,37 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } } - my ($x509, $csr_pubkey, $x509_pubkey); + my $chain; print STDERR "[$s] Will request authorization for: ".join(", ", @authz), "\n" if $OPTS{debug}; - if (acme_client({chdir => $challenge_dir, in => $csr, out => \$x509}, @authz)) { + if (acme_client({chdir => $challenge_dir, in => $csr, out => \$chain}, @authz)) { print STDERR "[$s] Error: Couldn't issue X.509 certificate!\n"; $rv = 1; next; } + my $cert; + eval { + my $mem = Net::SSLeay::BIO_s_mem() or die; + my $bio = Net::SSLeay::BIO_new($mem) or die; + die "incomplete write" unless + Net::SSLeay::BIO_write($bio, $chain) == length($chain); + my $x509 = Net::SSLeay::PEM_read_bio_X509($bio); + $cert = Net::SSLeay::PEM_get_string_X509($x509); + Net::SSLeay::BIO_free($bio) or die; + }; + if ($@) { + print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n"; + $rv = 1; + next; + } + # extract pubkeys from CSR and cert, and ensure they match + # XXX would be nice to use X509_get_X509_PUBKEY and X509_REQ_get_X509_PUBKEY here, + # or EVP_PKEY_cmp(), but unfortunately Net::SSLeay 1.88 doesn't support these + my ($cert_pubkey, $csr_pubkey); + spawn({in => $cert, out => \$cert_pubkey}, qw/openssl x509 -inform PEM -noout -pubkey/); spawn({in => $csr, out => \$csr_pubkey }, qw/openssl req -inform DER -noout -pubkey/); - spawn({in => $x509, out => \$x509_pubkey}, qw/openssl x509 -inform PEM -noout -pubkey/); - unless (defined $x509_pubkey and defined $csr_pubkey and $x509_pubkey eq $csr_pubkey) { + unless (defined $cert_pubkey and defined $csr_pubkey and $cert_pubkey eq $csr_pubkey) { print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n"; $rv = 1; next; @@ -845,7 +846,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { # verify certificate validity against the CA bundle if ((my $CAfile = $conf->{CAfile} // '@@datadir@@/lacme/ca-certificates.crt') ne '') { - my %args = (in => $x509); + my %args = (in => $cert); $args{out} = \*STDERR if $OPTS{debug}; my @options = ('-trusted', $CAfile, '-purpose', 'sslserver', '-x509_strict'); push @options, '-show_chain' if $OPTS{debug}; @@ -856,25 +857,24 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } } - my %install = ( content => $x509, + # install certificate + my %install_opts = ( mode => $conf->{mode} // $conf->{chmod} // "", owner => $conf->{owner} // $conf->{chown} // "" ); - - # install certificate if ((my $path = $conf->{'certificate'} // "") ne "") { print STDERR "Installing X.509 certificate $path\n"; - install_cert(%install, path => $path, nochain => 1); + install_cert($path => $cert, %install_opts); } if ((my $path = $conf->{'certificate-chain'} // "") ne "") { print STDERR "Installing X.509 certificate chain $path\n"; - install_cert(%install, path => $path); + install_cert($path => $chain, %install_opts); } my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump/; open my $fh, '|-', qw/openssl x509 -noout -fingerprint -sha256 -text -certopt/, @certopts or die "fork: $!"; - print $fh $x509; + print $fh $cert; close $fh or die $! ? "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; -- cgit v1.2.3 From 9a8f705eddd18ccc9a24fe0e7efe6b5a87b2be09 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 25 Feb 2021 01:41:59 +0100 Subject: lacme: pass a temporary JSON file with the client configuration to the internal client. So it doesn't have to parse the INI file again. Also, while lacme.conf is world-readable by default, one might restrict permissions and add private information in there, not realizing that everything, including comments, will be readable by the client. --- Changelog | 2 ++ client | 8 +------- lacme | 26 ++++++++++++++++---------- tests/drop-privileges | 4 ++-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Changelog b/Changelog index d63c754..8bf0721 100644 --- a/Changelog +++ b/Changelog @@ -11,6 +11,8 @@ lacme (0.8.1) upstream; 'chmod'. + lacme: split certificates using Net::SSLeay::PEM_* instead of calling openssl. + + lacme: pass a temporary JSON file with the client configuration to + the internal client, so it doesn't have to parse the INI file again. - lacme: in the [accountd] config, let lacme-accountd(1) do the %-expansion for 'config', not lacme(8) when building the command. - lacme-accountd: don't log debug messages unless --debug is set. diff --git a/client b/client index 33189d3..8439ddb 100755 --- a/client +++ b/client @@ -56,8 +56,6 @@ use Date::Parse (); use LWP::UserAgent (); use JSON (); -use Config::Tiny (); - # Clean up PATH $ENV{PATH} = join ':', qw{/usr/bin /bin}; delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; @@ -107,11 +105,7 @@ do { my $CONFIG = do { my $conf = do { local $/ = undef; <$CONFFILE> }; - 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 - $h->{client}; + JSON::->new->decode($conf); }; my $UA = do { my %args = %$CONFIG; diff --git a/lacme b/lacme index 13c2ef5..d7dac54 100755 --- a/lacme +++ b/lacme @@ -37,13 +37,14 @@ use Socket 1.95 qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC use Config::Tiny (); use Date::Parse (); +use JSON (); use Net::SSLeay 1.46 (); # Clean up PATH $ENV{PATH} = join ':', qw{/usr/bin /bin}; delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; -my ($COMMAND, %OPTS, $CONFFILE, $CONFIG, @CLEANUP); +my ($COMMAND, %OPTS, $CONFIG, @CLEANUP); $SIG{$_} = sub() { exit 1 } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM @@ -99,14 +100,12 @@ sub spec_expand($) { return $str; } -sub set_FD_CLOEXEC($$); my $CONFFILENAME = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf"); do { 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> }; - # don't close $CONFFILE so we can pass it to the client - set_FD_CLOEXEC($CONFFILE, 1); + open my $fh, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n"; + my $conf = do { local $/ = undef; <$fh> }; + close $fh or die "close: $!"; my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; my $defaults = delete $h->{_} // {}; @@ -573,19 +572,26 @@ sub acme_client($@) { die "connect: $!"; } } + set_FD_CLOEXEC($client, 1); + + my $client_config; + do { + my $tmp = File::Temp::->new(TMPDIR => 1, TEMPLATE => "lacme-client.conf.json-XXXXXXXXXX", UNLINK => 1) // die; + print $tmp JSON::->new->encode($conf); + open $client_config, "<", $tmp->filename() or die "open: $!"; + }; # 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 @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($client_config, $client); # untaint fileno my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); umask(0022) // die; - set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); - seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; + set_FD_CLOEXEC($_, 0) for ($client_config, $client); $ENV{DEBUG} = $OPTS{debug} // 0; }}, $cmd, @args2, $COMMAND, @fileno, @args); + close $client_config or die "close: $!\n"; if (defined $cleanup) { @CLEANUP = grep { $_ ne $cleanup } @CLEANUP; diff --git a/tests/drop-privileges b/tests/drop-privileges index fd432d9..8deb8f1 100644 --- a/tests/drop-privileges +++ b/tests/drop-privileges @@ -123,8 +123,8 @@ check_client() { 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" + grep -Eq "^[0-9]+ 0500 $UID:$GID /tmp/lacme-client.conf\.json-" "$prefix/fd" || return 1 + sed -ri '0,\#^[0-9]+ .* /tmp/lacme-client.conf\.json-# {//d}' "$prefix/fd" ! test -s "$prefix/fd" || return 1 } check_webserver() { -- cgit v1.2.3 From 40a4c9b9be51f9c41edd8b421dd629e001659fb4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 25 Jan 2023 03:11:22 +0100 Subject: Replace '$(dir $@)' with '$(@D)' in Makefile. --- Changelog | 1 + Makefile | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Changelog b/Changelog index 8bf0721..d37d964 100644 --- a/Changelog +++ b/Changelog @@ -25,6 +25,7 @@ lacme (0.8.1) upstream; - lacme: ignore empty values in settings 'chown', 'chmod', 'certificate' and 'certificate-chain'. - lacme: return an error when the 'mode'/'chown' isn't a number. + - Makefile: replace '$(dir $@)' with '$(@D)'. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/Makefile b/Makefile index 16ac04e..10e55c5 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ $(BUILDDIR)/certs/ca-certificates.crt: \ certs/isrg-root-x2.pem \ certs/lets-encrypt-r[34].pem \ certs/lets-encrypt-e[12].pem - mkdir -pv -- $(dir $@) + mkdir -pv -- $(@D) cat -- $^ >$@ # Staging Environment for tests, see https://letsencrypt.org/docs/staging-environment/ @@ -27,7 +27,7 @@ $(BUILDDIR)/certs-staging/ca-certificates.crt: \ 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 $@) + mkdir -pv -- $(@D) cat -- $^ >$@ prefix ?= $(DESTDIR) @@ -52,7 +52,7 @@ lacme_client_group ?= nogroup acmeapi_server ?= https://acme-v02.api.letsencrypt.org/directory $(BUILDDIR)/%: % - mkdir -pv -- $(dir $@) + mkdir -pv -- $(@D) cp --no-dereference --preserve=mode,links,xattr -vfT -- "$<" "$@" sed -i "s#@@bindir@@#$(bindir)#g; \ s#@@sbindir@@#$(sbindir)#g; \ -- cgit v1.2.3 From cb0b301e7a62a71d9e4454f9f7af5358c857c48c Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 25 Jan 2023 03:12:13 +0100 Subject: Adjust test suite against current Let's Encrypt staging environment. --- Changelog | 1 + tests/accountd | 1 + tests/accountd-kid | 4 +++- tests/cert-revoke | 4 ++-- tests/cert-verify | 2 +- tests/old-accountd | 1 + tests/old-lacme | 1 + 7 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index d37d964..4aa9f4f 100644 --- a/Changelog +++ b/Changelog @@ -26,6 +26,7 @@ lacme (0.8.1) upstream; and 'certificate-chain'. - lacme: return an error when the 'mode'/'chown' isn't a number. - Makefile: replace '$(dir $@)' with '$(@D)'. + - Test suite: Adjust against current Let's Encrypt staging environment. -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 diff --git a/tests/accountd b/tests/accountd index 7e8fd4c..433f8ad 100644 --- a/tests/accountd +++ b/tests/accountd @@ -65,6 +65,7 @@ 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=$! +sleep 1 # run lacme(8) multiple times using that single lacme-accountd(1) instance lacme --socket="$SOCKET" --debug account 2>"$STDERR" || fail diff --git a/tests/accountd-kid b/tests/accountd-kid index 1f282fd..8a4b53c 100644 --- a/tests/accountd-kid +++ b/tests/accountd-kid @@ -23,6 +23,7 @@ EOF SOCKET=~lacme-account/S.lacme runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" --quiet & PID=$! +sleep 1 # newAccount resource fails as per RFC 8555 sec. 6.2 it requires a JWK ! lacme --socket="$SOCKET" account 2>"$STDERR" || fail @@ -37,6 +38,7 @@ wait rm ~lacme-account/.local/share/lacme/accountd.log runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" --quiet & PID=$! +sleep 1 # newOrder works fine without JWK lacme --socket="$SOCKET" newOrder @@ -46,7 +48,7 @@ test /etc/lacme/simpletest.rsa.crt -nt /etc/lacme/simpletest.rsa.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 -Fq "400 Bad Request (unable to revoke" grepstderr -Fxq "Warning: Couldn't revoke /etc/lacme/simpletest.rsa.crt" kill $PID diff --git a/tests/cert-revoke b/tests/cert-revoke index f3d585e..179ccba 100644 --- a/tests/cert-revoke +++ b/tests/cert-revoke @@ -18,7 +18,7 @@ test /etc/lacme/simpletest.ecdsa.crt -nt /etc/lacme/simpletest.ecdsa.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 -Fq "400 Bad Request (unable to revoke" grepstderr -Fxq "Warning: Couldn't revoke /etc/lacme/simpletest.ecdsa.crt" # and the RSA certificate using the service key @@ -26,7 +26,7 @@ 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 -Fq "400 Bad Request (unable to revoke" 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 index 49629f2..4d254c6 100644 --- a/tests/cert-verify +++ b/tests/cert-verify @@ -14,7 +14,7 @@ openssl verify -no-CApath -CAfile /etc/ssl/certs/ca-certificates.crt -show_chain 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 "Could not open file or uri for loading certs of trusted certificates from /usr/share/lacme/ca-certificates.crt" grepstderr -Fxq "[simpletest-rsa] Error: Received invalid X.509 certificate from ACME server!" # verification error for unrelated CA bundle diff --git a/tests/old-accountd b/tests/old-accountd index b44f7ec..abd330d 100644 --- a/tests/old-accountd +++ b/tests/old-accountd @@ -21,6 +21,7 @@ DEBIAN_FRONTEND="noninteractive" apt install -y --no-install-recommends \ SOCKET=~lacme-account/S.lacme runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" & PID=$! +sleep 1 lacme --socket="$SOCKET" account lacme --socket="$SOCKET" newOrder diff --git a/tests/old-lacme b/tests/old-lacme index fa7d827..b1c9f88 100644 --- a/tests/old-lacme +++ b/tests/old-lacme @@ -26,6 +26,7 @@ mv -f /usr/share/lacme/ca-certificates.crt.back /usr/share/lacme/ca-certificates SOCKET=~lacme-account/S.lacme runuser -u lacme-account -- lacme-accountd --socket="$SOCKET" & PID=$! +sleep 1 sed -ri "s/^\[accountd]$/#&/" /etc/lacme/lacme.conf # https://bugs.debian.org/955767 lacme --socket="$SOCKET" account lacme --socket="$SOCKET" newOrder -- cgit v1.2.3 From b3af3526b293f396da02a6276ea86ca17dcd2d03 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 25 Jan 2023 03:23:51 +0100 Subject: Prepare new release v0.8.1. --- Changelog | 2 +- client | 2 +- lacme | 2 +- lacme-accountd | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Changelog b/Changelog index 4aa9f4f..fc658bf 100644 --- a/Changelog +++ b/Changelog @@ -28,7 +28,7 @@ lacme (0.8.1) upstream; - Makefile: replace '$(dir $@)' with '$(@D)'. - Test suite: Adjust against current Let's Encrypt staging environment. - -- Guilhem Moulin Mon, 22 Feb 2021 12:04:28 +0100 + -- Guilhem Moulin Wed, 25 Jan 2023 03:23:51 +0100 lacme (0.8.0) upstream; diff --git a/client b/client index 8439ddb..3cda821 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.8.0'; +our $VERSION = '0.8.1'; my $PROTOCOL_VERSION = 1; my $NAME = 'lacme-client'; diff --git a/lacme b/lacme index d7dac54..21a184c 100755 --- a/lacme +++ b/lacme @@ -22,7 +22,7 @@ use v5.14.2; use strict; use warnings; -our $VERSION = '0.8.0'; +our $VERSION = '0.8.1'; my $NAME = 'lacme'; use Errno 'EINTR'; diff --git a/lacme-accountd b/lacme-accountd index 98c11ad..a9f5469 100755 --- a/lacme-accountd +++ b/lacme-accountd @@ -23,7 +23,7 @@ use v5.14.2; use strict; use warnings; -our $VERSION = '0.8.0'; +our $VERSION = '0.8.1'; my $PROTOCOL_VERSION = 1; my $NAME = 'lacme-accountd'; -- cgit v1.2.3