diff options
| author | Guilhem Moulin <guilhem@debian.org> | 2023-01-25 03:32:04 +0100 | 
|---|---|---|
| committer | Guilhem Moulin <guilhem@debian.org> | 2023-01-25 03:32:04 +0100 | 
| commit | 33687a2e3aea5ae69add7812315445ad23748fab (patch) | |
| tree | 952a06618d7da373043debef8a8c28d4c8371385 | |
| parent | 2a981ac3829f27d3179eb6b6e682dc17cc9c4225 (diff) | |
| parent | b3af3526b293f396da02a6276ea86ca17dcd2d03 (diff) | |
Merge tag 'v0.8.1' into debian/latest
Release version 0.8.1
| -rw-r--r-- | Changelog | 34 | ||||
| -rw-r--r-- | Makefile | 6 | ||||
| -rwxr-xr-x | client | 23 | ||||
| -rw-r--r-- | config/lacme-certs.conf | 4 | ||||
| -rwxr-xr-x | lacme | 167 | ||||
| -rwxr-xr-x | lacme-accountd | 71 | ||||
| -rw-r--r-- | lacme.8.md | 12 | ||||
| -rwxr-xr-x | test | 7 | ||||
| -rw-r--r-- | tests/accountd | 7 | ||||
| -rw-r--r-- | tests/accountd-kid | 14 | ||||
| -rw-r--r-- | tests/accountd-remote | 2 | ||||
| -rw-r--r-- | tests/accountd-validate | 36 | ||||
| -rw-r--r-- | tests/cert-install | 65 | ||||
| -rw-r--r-- | tests/cert-revoke | 4 | ||||
| -rw-r--r-- | tests/cert-verify | 2 | ||||
| -rw-r--r-- | tests/drop-privileges | 18 | ||||
| -rw-r--r-- | tests/old-accountd | 1 | ||||
| -rw-r--r-- | tests/old-lacme | 1 | 
18 files changed, 310 insertions, 164 deletions
@@ -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 @@ -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; \ @@ -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. @@ -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: $!");      } @@ -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* @@ -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  | 
