aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@debian.org>2023-01-25 03:32:04 +0100
committerGuilhem Moulin <guilhem@debian.org>2023-01-25 03:32:04 +0100
commit33687a2e3aea5ae69add7812315445ad23748fab (patch)
tree952a06618d7da373043debef8a8c28d4c8371385
parent2a981ac3829f27d3179eb6b6e682dc17cc9c4225 (diff)
parentb3af3526b293f396da02a6276ea86ca17dcd2d03 (diff)
Merge tag 'v0.8.1' into debian/latest
Release version 0.8.1
-rw-r--r--Changelog34
-rw-r--r--Makefile6
-rwxr-xr-xclient23
-rw-r--r--config/lacme-certs.conf4
-rwxr-xr-xlacme167
-rwxr-xr-xlacme-accountd71
-rw-r--r--lacme.8.md12
-rwxr-xr-xtest7
-rw-r--r--tests/accountd7
-rw-r--r--tests/accountd-kid14
-rw-r--r--tests/accountd-remote2
-rw-r--r--tests/accountd-validate36
-rw-r--r--tests/cert-install65
-rw-r--r--tests/cert-revoke4
-rw-r--r--tests/cert-verify2
-rw-r--r--tests/drop-privileges18
-rw-r--r--tests/old-accountd1
-rw-r--r--tests/old-lacme1
18 files changed, 310 insertions, 164 deletions
diff --git a/Changelog b/Changelog
index 9f12237..fc658bf 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,35 @@
+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: add 'owner' resp. 'mode' as (prefered) alias for 'chown' resp.
+ '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.
+ - 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.
+ - 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'.
+ - 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 <guilhem@fripost.org> Wed, 25 Jan 2023 03:23:51 +0100
+
lacme (0.8.0) upstream;
* Breaking change: 'challenge-directory' now needs to be set to an
@@ -218,6 +250,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 <guilhem@guilhem.org> Sat, 03 Dec 2016 16:40:56 +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; \
diff --git a/client b/client
index fdef865..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';
@@ -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/};
@@ -87,13 +85,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";
@@ -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;
@@ -210,7 +204,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 +243,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);
@@ -338,11 +332,12 @@ 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) {
- print STDERR "WARNING: File exists: $challenge->{token}\n";
+ print STDERR "Warning: File exists: $challenge->{token}\n";
} else {
die "open($challenge->{token}): $!";
}
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 731535f..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';
@@ -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 Net::SSLeay ();
+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->{_} // {};
@@ -240,7 +239,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 +248,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 +350,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: $!";
}
@@ -376,14 +375,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: $!";
@@ -536,7 +535,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};
@@ -573,18 +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} // '/');
- set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client);
- seek($CONFFILE, SEEK_SET, 0) or die "seek: $!";
+ umask(0022) // die;
+ 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;
@@ -657,44 +664,43 @@ sub spawn($@) {
#############################################################################
# Install the certificate (optionally excluding the chain of trust)
#
-sub install_cert($$;$) {
- my ($filename, $chain, $leafonly) = @_;
+sub install_cert($$%) {
+ my ($path, $content, %args) = @_;
- 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 {
- my $umask = umask() // die "umask: $!";
- chmod(0644 &~ $umask, $fh) or die "chmod: $!";
- if ($leafonly) {
- # keep only the leaf certificate
- 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($chain);
- $wd->close() or die "close: $!";
+ $fh->print($content) or die "print: $!";
- waitpid $pid => 0;
- die $? if $? > 0;
+ my $mode;
+ if ((my $m = $args{mode}) ne "") {
+ die "Not an octal string: $m\n" unless $m =~ /^[0-9]+$/;
+ $mode = oct($m);
} else {
- $fh->print($chain) or die "print: $!";
+ 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};
- 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): $!";
}
@@ -743,7 +749,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}};
@@ -766,15 +773,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 @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 $cert and defined (my $t = x509_enddate($cert))) {
+ 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));
@@ -807,18 +814,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;
@@ -826,7 +852,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};
@@ -838,34 +864,23 @@ 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);
+ my %install_opts = (
+ mode => $conf->{mode} // $conf->{chmod} // "",
+ owner => $conf->{owner} // $conf->{chown} // ""
+ );
+ if ((my $path = $conf->{'certificate'} // "") ne "") {
+ print STDERR "Installing X.509 certificate $path\n";
+ install_cert($path => $cert, %install_opts);
}
- if (defined $conf->{'certificate-chain'}) {
- print STDERR "Installing X.509 certificate chain $conf->{'certificate-chain'}\n";
- install_cert($conf->{'certificate-chain'}, $x509);
- }
-
- 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;
- foreach (grep defined, @$conf{qw/certificate certificate-chain/}) {
- 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 "chown: $!";
- }
+ if ((my $path = $conf->{'certificate-chain'} // "") ne "") {
+ print STDERR "Installing X.509 certificate chain $path\n";
+ 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";
diff --git a/lacme-accountd b/lacme-accountd
index 0f5deb2..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';
@@ -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) {
+ 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);
@@ -83,7 +86,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;
}
@@ -133,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};
};
@@ -211,9 +215,9 @@ 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(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: $!");
@@ -221,7 +225,7 @@ unless (defined $OPTS{stdio}) {
($SOCKNAME, $S) = ($sockname, $sock);
listen($S, 1) or panic("listen: $!");
- umask($umask) // panic("umask: $!");
+ umask($umask) // panic();
};
@@ -234,26 +238,43 @@ 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);
- unless (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) {
- info("[$id] >>> Error: Refusing to sign request: Malformed protected header");
+ if (defined $header and $header =~ /\A[A-Za-z0-9\-_]+\z/) {
+ $header = decode_base64url($header);
+ } else {
+ info("[$id] NOSIGN [malformed JWS Protected Header]");
last;
}
- unless (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) {
- # POST-as-GET yields an empty payload
- info("[$id] >>> Error: Refusing to sign request: Malformed payload");
+ if (defined $payload and $payload =~ /\A[A-Za-z0-9\-_]*\z/) {
+ # empty payloads are valid, and used for POST-as-GET (RFC 8555 sec. 6.3)
+ $payload = decode_base64url($payload);
+ } else {
+ info("[$id] NOSIGN [malformed JWS 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)";
+
+ 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);
+ my $sig = eval { $SIGN->($data) };
+ panic($@) if $@ or !defined $sig;
+ logmsg($LOG_VERBOSE => "[$id] SIGNED ", $req);
$out->printflush( encode_base64url($sig), "\r\n" ) or warn "print: $!";
}
}
@@ -267,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: $!";
}
}
@@ -279,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: $!");
}
diff --git a/lacme.8.md b/lacme.8.md
index ad6dab6..65f1c36 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*
@@ -422,15 +421,16 @@ 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.
+ *certificate-chain* to. By default the files are created with mode
+ 0644 minus umask restrictions.
*notify*
diff --git a/test b/test
index 81d910c..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"
@@ -62,6 +63,7 @@ if [ $# -eq 0 ]; then
done
else
for t in "$@"; do
+ t="${t#tests/}"
if [ -f "tests/$t" ]; then
TESTS+=( "$t" )
else
@@ -116,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"
@@ -167,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" \
diff --git a/tests/accountd b/tests/accountd
index a603c16..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
@@ -79,9 +80,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..8a4b53c 100644
--- a/tests/accountd-kid
+++ b/tests/accountd-kid
@@ -23,13 +23,14 @@ 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
-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 ">>> 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\":{}," </tmp/signed
# rotate log and restart accountd
kill $PID
@@ -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,14 +48,14 @@ 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
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
+grep -F "] SIGNED header=base64url({" ~lacme-account/.local/share/lacme/accountd.log >/tmp/signed
+! grep -vF "] SIGNED header=base64url({\"alg\":\"RS256\",\"kid\":\"$keyid\"," </tmp/signed
# vim: set filetype=sh :
diff --git a/tests/accountd-remote b/tests/accountd-remote
index 9e7f812..ce2b54e 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 ">>> 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-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 :
diff --git a/tests/cert-install b/tests/cert-install
index f2147d2..4b3e820 100644
--- a/tests/cert-install
+++ b/tests/cert-install
@@ -103,74 +103,103 @@ 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
+# 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 = nobody
+ owner = 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/^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" ]
-# chmod 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:nogroup
+ owner = 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/^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" ]
st="$(stat -c "%U:%G %#a" /etc/lacme/test5.crt)"
[ "$st" = "nobody:nogroup 0644" ]
-# chown
+# 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 =
+ mode =
+ owner =
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
+# 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
+ mode = 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 :
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/drop-privileges b/tests/drop-privileges
index 0596e31..8deb8f1 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"
@@ -113,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() {
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