From 647d28bf9b8da2ce47a888aad71ab5264eea6c6d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 18:28:03 +0100 Subject: lacme: delay webserver socket shutdown. To after the process has terminated. This solves a race condition spewing accept: Invalid argument at /usr/libexec/lacme/webserver line 80. (harmless) errors. Closes: deb#970458 --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index 07ebb45..088e393 100755 --- a/lacme +++ b/lacme @@ -346,9 +346,9 @@ sub spawn_webserver() { set_FD_CLOEXEC($sock, 1); push @CLEANUP, sub() { print STDERR "[$$] Shutting down ACME webserver bound to $p\n" if $OPTS{debug}; - shutdown($sock, SHUT_RDWR) or warn "shutdown: $!"; kill 15 => $pid; waitpid $pid => 0; + shutdown($sock, SHUT_RDWR) or warn "shutdown: $!"; }; # on dual-stack ipv4/ipv6, we'll need to open the port for the -- cgit v1.2.3 From 61e4ad1347f51a84400cbf87633cc99f657f9ad7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 20:28:46 +0100 Subject: Make unprivileged user/group for the internal client resp. webserver configurable. --- lacme | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 088e393..e4b8e01 100755 --- a/lacme +++ b/lacme @@ -91,8 +91,8 @@ do { my %valid = ( client => { socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), - user => 'nobody', - group => 'nogroup', + user => '@@lacme_client_user@@', + group => '@@lacme_client_group@@', command => '@@libexecdir@@/lacme/client', # the rest is for the ACME client map {$_ => undef} qw/server timeout SSL_verify SSL_version SSL_cipher_list/ @@ -100,8 +100,8 @@ do { webserver => { listen => '@@runstatedir@@/lacme-www.socket', 'challenge-directory' => undef, - user => 'www-data', - group => 'www-data', + user => '@@lacme_www_user@@', + group => '@@lacme_www_group@@', command => '@@libexecdir@@/lacme/webserver', iptables => 'No' -- cgit v1.2.3 From 0f574f73182491fe793fcdfce6632372fab4d5c3 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 9 Dec 2020 21:47:54 +0100 Subject: lacme: new flag `--force`. Which aliases to `--min-days=-1`, i.e., forces renewal regardless of the expiration date of existing certificates. --- lacme | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index e4b8e01..7f3d65d 100755 --- a/lacme +++ b/lacme @@ -63,7 +63,11 @@ sub usage(;$$) { } exit $rv; } -usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s register tos-agreed deactivate min-days=i quiet|q debug help|h/); +usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s + register tos-agreed deactivate + min-days=i force + quiet|q + debug help|h/); usage(0) if $OPTS{help}; $COMMAND = shift(@ARGV) // usage(1, "Missing command"); @@ -643,6 +647,7 @@ if ($COMMAND eq 'account') { # newOrder [SECTION ..] # elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { + $OPTS{'min-days'} = -1 if $OPTS{force}; $COMMAND = 'newOrder'; my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'} : defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ] -- cgit v1.2.3 From 9dfb2cde7baf686113e49266c28940c8a564c1ca Mon Sep 17 00:00:00 2001 From: Benjamin Tietz Date: Wed, 23 Sep 2020 17:22:32 +0200 Subject: lacme: allow direct use challenge-directory .well-known/acme-challenge --- lacme | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 7f3d65d..d7ae8ce 100755 --- a/lacme +++ b/lacme @@ -28,6 +28,7 @@ my $NAME = 'lacme'; use Errno 'EINTR'; use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; use File::Temp (); +use File::Path 'remove_tree'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; use POSIX (); @@ -104,6 +105,7 @@ do { webserver => { listen => '@@runstatedir@@/lacme-www.socket', 'challenge-directory' => undef, + 'hard-copy-challenge-directory' => 'No', user => '@@lacme_www_user@@', group => '@@lacme_www_group@@', command => '@@libexecdir@@/lacme/webserver', @@ -289,10 +291,26 @@ sub spawn_webserver() { # serve ACME challenge reponses). if (defined (my $dir = $conf->{'challenge-directory'})) { print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; - symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; - push @CLEANUP, sub() { - print STDERR "Unlinking $dir\n" if $OPTS{debug}; - unlink $dir or warn "Warning: Can't unlink $dir: $!"; + if (lc ($conf->{'hard-copy-challenge-directory'} // 'No') eq 'yes') { + mkdir $dir or die "Can't create directory $dir: $!"; + $tmpdir = $dir; + push @CLEANUP, sub() { + my $error = undef; + remove_tree($dir, { safe => 1, error => \$error }); + if ($error && @$error) { + foreach my $e (@$error) { + my ($file, $message) = %$e; + my $msghead = $file?"Error removing $file in":"Error while removing"; + warn "$msghead challenge dir $dir: $message\n"; + } + } + } + } else { + symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; + push @CLEANUP, sub() { + print STDERR "Unlinking $dir\n" if $OPTS{debug}; + unlink $dir or warn "Warning: Can't unlink $dir: $!"; + } } } elsif (!@sockaddr) { -- cgit v1.2.3 From a903ea92dd736c560d21fe45063d4914765fa173 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 17:01:17 +0100 Subject: challenge-directory now needs to be set to an *existing* directory. Since lacme(8) spawns a builtin webserver by default the change doesn't affect default configurations. See https://bugs.debian.org/970800 for the rationale. --- lacme | 82 ++++++++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 36 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index d7ae8ce..7ad7aa8 100755 --- a/lacme +++ b/lacme @@ -26,9 +26,8 @@ our $VERSION = '0.3'; my $NAME = 'lacme'; use Errno 'EINTR'; -use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; +use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC O_CREAT O_EXCL O_WRONLY SEEK_SET/; use File::Temp (); -use File::Path 'remove_tree'; use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; use POSIX (); @@ -105,7 +104,6 @@ do { webserver => { listen => '@@runstatedir@@/lacme-www.socket', 'challenge-directory' => undef, - 'hard-copy-challenge-directory' => 'No', user => '@@lacme_www_user@@', group => '@@lacme_www_group@@', command => '@@libexecdir@@/lacme/webserver', @@ -258,15 +256,6 @@ sub set_FD_CLOEXEC($$) { # The temporary challenge directory is returned. # sub spawn_webserver() { - # create a temporary directory; give write access to the ACME client - # and read access to the webserver - my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; - chmod 0755, $tmpdir or die "Can't chmod: $!"; - if ((my $username = $CONFIG->{client}->{user}) ne '') { - my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; - chown($uid, -1, $tmpdir) or die "Can't chown: $!"; - } - my $conf = $CONFIG->{webserver}; # parse and pack addresses to listen to @@ -286,35 +275,56 @@ sub spawn_webserver() { push @sockaddr, $sockaddr; } - # symlink the 'challenge-directory' configuration option to the - # temporary challenge directory (so an existing httpd can directly - # serve ACME challenge reponses). + # Use existing HTTPd to serve challenge files using 'challenge-directory' + # as document root if (defined (my $dir = $conf->{'challenge-directory'})) { print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; - if (lc ($conf->{'hard-copy-challenge-directory'} // 'No') eq 'yes') { - mkdir $dir or die "Can't create directory $dir: $!"; - $tmpdir = $dir; - push @CLEANUP, sub() { - my $error = undef; - remove_tree($dir, { safe => 1, error => \$error }); - if ($error && @$error) { - foreach my $e (@$error) { - my ($file, $message) = %$e; - my $msghead = $file?"Error removing $file in":"Error while removing"; - warn "$msghead challenge dir $dir: $message\n"; - } + # lacme(8) doesn't have the list of challenge files to delete on + # cleanup -- instead, we unlink all files and fails at + # initialization stage when the challenge directory is not empty + + opendir my $dh, $dir or die "opendir($dir): $!\n"; + while (readdir $dh) { + die "Error: Refusing to use non-empty challenge directory $dir\n" + unless $_ eq '.' or $_ eq '..'; + } + closedir $dh or die "close: $!"; + undef $dh; + + # use a "lock file" (NFS-friendly) to avoid concurrent usages + my $lockfile = ".$NAME.lock"; + sysopen(my $fh, "$dir/$lockfile", O_CREAT|O_EXCL|O_WRONLY, 0600) + or die "Can't create lockfile in challenge directory: $!"; + print $fh $$, "\n"; + close $fh or die "close: $!"; + undef $fh; + + push @CLEANUP, sub() { + if (opendir(my $dh, $dir)) { + my @files = grep { $_ ne '.' and $_ ne '..' and $_ ne $lockfile } readdir $dh; + closedir $dh or warn "close: $!"; + push @files, $lockfile; # unlink $lockfile last + foreach (@files) { + die unless /\A(.+)\z/; # untaint + unlink "$dir/$1" or warn "unlink($dir/$1): $!"; } + } else { + warn "opendir($dir): $!\n"; } - } else { - symlink $tmpdir, $dir or die "Can't symlink $dir -> $tmpdir: $!"; - push @CLEANUP, sub() { - print STDERR "Unlinking $dir\n" if $OPTS{debug}; - unlink $dir or warn "Warning: Can't unlink $dir: $!"; - } - } + }; + return $dir; # ignore 'listen' and 'iptables' } - elsif (!@sockaddr) { - die "'challenge-directory' option of section [webserver] is required when 'listen' is empty\n"; + + die "'challenge-directory' option is required in section [webserver] when 'listen' is empty\n" + unless @sockaddr; + + # create a temporary directory; give write access to the ACME client + # and read access to the webserver + my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; + chmod 0755, $tmpdir or die "Can't chmod: $!"; + if ((my $username = $CONFIG->{client}->{user}) ne '') { + my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; + chown($uid, -1, $tmpdir) or die "Can't chown: $!"; } # create socket(s) and spawn webserver(s) -- cgit v1.2.3 From 2c1a396728a381685923f7b1c4dea53d225112fc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 22:59:11 +0100 Subject: Add (self-signed) ISRG Roots to the CA bundle. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows us to fully validate provided X.509 chains using that self-contained bundle, regardless of which CAs is marqued as trusted under /etc/ssl/certs. Also, remove cross-signed intermediate CAs from the bundle as they're useless in a self-contained bundle. Also, remove decomissioned intermediate CAs Authority X3 and X4 from the bundle. This change bumps the minimum OpenSSL version to 1.1.0 (for verify(1ssl)'s ‘-trusted’ and ‘-show_chain’ options). --- lacme | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 7ad7aa8..480778f 100755 --- a/lacme +++ b/lacme @@ -784,13 +784,17 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { next; }; - # verify certificate validity against the CA - $conf->{CAfile} //= '@@datadir@@/lacme/ca-certificates.crt'; - if ($conf->{CAfile} ne '' and spawn({in => $x509}, 'openssl', 'verify', '-CAfile', $conf->{CAfile}, - qw/-purpose sslserver -x509_strict/)) { - print STDERR "[$s] Error: Received invalid X.509 certificate from ACME server!\n"; - $rv = 1; - next; + # verify certificate validity against the CA bundle + if ((my $CAfile = $conf->{CAfile} // '@@datadir@@/lacme/ca-certificates.crt') ne '') { + my %args = (in => $x509); + $args{out} = \*STDERR if $OPTS{debug}; + my @options = ('-trusted', $CAfile, '-purpose', 'sslserver', '-x509_strict'); + push @options, '-show_chain' if $OPTS{debug}; + if (spawn(\%args, 'openssl', 'verify', @options)) { + print STDERR "[$s] Error: Received invalid X.509 certificate from ACME server!\n"; + $rv = 1; + next; + } } # install certificate -- cgit v1.2.3 From f3e28985165e9ff30907d5da45a4a0bc8c0ccf31 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 17:02:31 +0100 Subject: Bump copyright years. --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index 480778f..bd4bd73 100755 --- a/lacme +++ b/lacme @@ -2,7 +2,7 @@ #---------------------------------------------------------------------- # ACME client written with process isolation and minimal privileges in mind -# Copyright © 2016-2017 Guilhem Moulin +# Copyright © 2015-2021 Guilhem Moulin # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by -- cgit v1.2.3 From f62a66c6ce82d9a1af241dc3952250362e601d45 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 14 Feb 2021 23:46:40 +0100 Subject: Add support for TLS Feature extension from RFC 7633. This is mostly useful for OCSP Must-Staple. --- lacme | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index bd4bd73..045c5b4 100755 --- a/lacme +++ b/lacme @@ -159,6 +159,7 @@ sub gen_csr(%) { ); $config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; + $config->print("tlsfeature = $args{tlsfeature}\n") if defined $args{tlsfeature}; $config->close() or die "Can't close: $!"; my @args = (qw/-new -batch -key/, $args{'certificate-key'}); @@ -703,7 +704,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { my $def = delete $h->{_} // {}; $defaults{$_} = $def->{$_} foreach keys %$def; my @valid = qw/certificate certificate-chain certificate-key min-days CAfile - hash keyUsage subject subjectAltName chown chmod notify/; + hash keyUsage subject subjectAltName tlsfeature chown chmod notify/; foreach my $s (keys %$h) { $conf->{$s} = { map { $_ => delete $h->{$s}->{$_} } @valid }; die "Unknown option(s) in [$s]: ".join(', ', keys %{$h->{$s}})."\n" if %{$h->{$s}}; @@ -744,7 +745,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { } # generate the CSR - my $csr = gen_csr(map {$_ => $conf->{$_}} qw/certificate-key subject subjectAltName keyUsage hash/) // do { + my $csr = gen_csr(map {$_ => $conf->{$_}} qw/certificate-key keyUsage subject subjectAltName tlsfeature hash/) // do { print STDERR "[$s] Warning: Couldn't generate CSR, skipping\n"; $rv = 1; next; -- cgit v1.2.3 From 2efd4458f4db7f489ecc81f4039b8e8103edf9d9 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Tue, 16 Feb 2021 17:24:31 +0100 Subject: Don't load configuration files from ./ by default. This is a breaking change: lacme(8) resp. lacme-accountd(1) no longer consider ./lacme.conf resp. ./lacme-accountd.conf as default location for the configuration file. Doing so has security implications when running these program from insecure directories. --- lacme | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 045c5b4..33f947c 100755 --- a/lacme +++ b/lacme @@ -77,8 +77,7 @@ $COMMAND = $COMMAND =~ /\A(account|newOrder|new-cert|revokeCert|revoke-cert)\z/ sub set_FD_CLOEXEC($$); my $CONFFILENAME = $OPTS{config} // first { -f $_ } - ( "./$NAME.conf" - , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/lacme/$NAME.conf" + ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" , "@@sysconfdir@@/lacme/$NAME.conf" ); do { -- cgit v1.2.3 From 3a5c3f0596398d64bb34498f40becbcd32ffa5de Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 11:42:18 +0100 Subject: Consolidate error messages for consistency. --- lacme | 116 +++++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 58 insertions(+), 58 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 33f947c..f0beac1 100755 --- a/lacme +++ b/lacme @@ -159,7 +159,7 @@ sub gen_csr(%) { $config->print("keyUsage = critical, $args{keyUsage}\n") if defined $args{keyUsage}; $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; $config->print("tlsfeature = $args{tlsfeature}\n") if defined $args{tlsfeature}; - $config->close() or die "Can't close: $!"; + $config->close() or die "close: $!"; my @args = (qw/-new -batch -key/, $args{'certificate-key'}); push @args, "-$args{hash}" if defined $args{hash}; @@ -167,20 +167,20 @@ sub gen_csr(%) { open my $fh, '-|', qw/openssl req -outform DER/, @args or die "fork: $!"; my $csr = do { local $/ = undef; <$fh> }; - close $fh or $! ? die "Can't close: $!" : return; + close $fh or $! ? die "close: $!" : return; if ($OPTS{debug}) { # print out the CSR in text form pipe my $rd, my $wd or die "pipe: $!"; my $pid = fork // die "fork: $!"; unless ($pid) { - open STDIN, '<&', $rd or die "Can't dup: $!"; - open STDOUT, '>&', \*STDERR or die "Can't dup: $!"; + open STDIN, '<&', $rd or die "dup: $!"; + open STDOUT, '>&', \*STDERR or die "dup: $!"; exec qw/openssl req -noout -text -inform DER/ or die; } - $rd->close() or die "Can't close: $!"; + $rd->close() or die "close: $!"; $wd->print($csr); - $wd->close() or die "Can't close: $!"; + $wd->close() or die "close: $!"; waitpid $pid => 0; die $? if $? > 0; @@ -220,21 +220,21 @@ sub drop_privileges($$$) { # set effective and real gid; also set the list of supplementary gids to that single gid if ($group ne '') { - my $gid = getgrnam($group) // die "Can't getgrnam($group): $!"; + my $gid = getgrnam($group) // die "getgrnam($group): $!"; $) = "$gid $gid"; - die "Can't setgroups: $!" if $@; - POSIX::setgid($gid) or die "Can't setgid: $!"; + die "setgroups: $!" if $@; + POSIX::setgid($gid) or die "setgid: $!"; die "Couldn't setgid/setguid" unless $( eq "$gid $gid" and $) eq "$gid $gid"; # safety check } # set effective and real uid if ($user ne '') { - my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; - POSIX::setuid($uid) or die "Can't setuid: $!"; + my $uid = getpwnam($user) // die "getpwnam($user): $!"; + POSIX::setuid($uid) or die "setuid: $!"; die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check } - chdir $dir or die "Can't chdir to $dir: $!"; + chdir $dir or die "chdir($dir): $!"; } @@ -243,10 +243,10 @@ sub drop_privileges($$$) { # sub set_FD_CLOEXEC($$) { my ($fd, $set) = @_; - my $flags = fcntl($fd, F_GETFD, 0) or die "Can't fcntl F_GETFD: $!"; + my $flags = fcntl($fd, F_GETFD, 0) or die "fcntl F_GETFD: $!"; my $flags2 = $set ? ($flags | FD_CLOEXEC) : ($flags & ~FD_CLOEXEC); return if $flags == $flags2; - fcntl($fd, F_SETFD, $flags2) or die "Can't fcntl F_SETFD: $!"; + fcntl($fd, F_SETFD, $flags2) or die "fcntl F_SETFD: $!"; } @@ -321,10 +321,10 @@ sub spawn_webserver() { # create a temporary directory; give write access to the ACME client # and read access to the webserver my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; - chmod 0755, $tmpdir or die "Can't chmod: $!"; + chmod 0755, $tmpdir or die "chmod: $!"; if ((my $username = $CONFIG->{client}->{user}) ne '') { - my $uid = getpwnam($username) // die "Can't getpwnam($username): $!"; - chown($uid, -1, $tmpdir) or die "Can't chown: $!"; + my $uid = getpwnam($username) // die "getpwnam($username): $!"; + chown($uid, -1, $tmpdir) or die "chown: $!"; } # create socket(s) and spawn webserver(s) @@ -353,7 +353,7 @@ sub spawn_webserver() { bind($sock, $sockaddr) or die "Couldn't bind to $p: $!"; push @CLEANUP, sub() { print STDERR "Unlinking $path\n" if $OPTS{debug}; - unlink $path or warn "Warning: Can't unlink $path: $!"; + unlink $path or warn "Warning: Couldn't unlink $path: $!"; }; umask($umask) // die "umask: $!"; } @@ -428,8 +428,8 @@ sub iptables_save($@) { my $pid = fork() // die "fork: $!"; unless ($pid) { - open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; - open STDOUT, '>&', $iptables_tmp or die "Can't dup: $!"; + open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; + open STDOUT, '>&', $iptables_tmp or die "dup: $!"; $| = 1; # turn off buffering for STDOUT exec "/usr/sbin/$iptables_bin-save", "-c" or die; } @@ -440,14 +440,14 @@ sub iptables_save($@) { # handle and not from the file. XXX if there was a way in Perl to # use open(2) with the O_TMPFILE flag we would use that to avoid # creating a file to start with - seek($iptables_tmp, SEEK_SET, 0) or die "Can't seek: $!"; + seek($iptables_tmp, SEEK_SET, 0) or die "seek: $!"; push @CLEANUP, sub() { print STDERR "[$$] Restoring $iptables_bin\n" if $OPTS{debug}; my $pid = fork() // die "fork: $!"; unless ($pid) { - open STDIN, '<&', $iptables_tmp or die "Can't dup: $!"; - open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; + open STDIN, '<&', $iptables_tmp or die "dup: $!"; + open STDOUT, '>', '/dev/null' or die "open(/dev/null): $!"; exec "/usr/sbin/$iptables_bin-restore", "-c" or die; } waitpid $pid => 0; @@ -496,7 +496,7 @@ sub acme_client($@) { unless ($pid) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); set_FD_CLOEXEC($s, 0); - $client->close() or die "Can't close: $!"; + $client->close() or die "close: $!"; my @cmd = ($accountd->{command}, '--conn-fd='.fileno($s)); push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; @@ -505,7 +505,7 @@ sub acme_client($@) { exec { $cmd[0] } @cmd or die; } print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; - $s->close() or die "Can't close: $!"; + $s->close() or die "close: $!"; $cleanup = sub() { print STDERR "[$$] Shutting down lacme-accountd\n" if $OPTS{debug}; shutdown($client, SHUT_RDWR) or warn "shutdown: $!"; @@ -520,11 +520,11 @@ sub acme_client($@) { # ensure we're the only user with write access to the parent dir my $dirname = $sockname =~ s/[^\/]+$//r; - @stat = stat($dirname) or die "Can't stat $dirname: $!"; + @stat = stat($dirname) or die "stat($dirname): $!"; die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; # ensure we're the only user with read/write access to the socket - @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n"; + @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n"; die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; # connect(2) to the socket @@ -543,7 +543,7 @@ sub acme_client($@) { my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); - seek($CONFFILE, SEEK_SET, 0) or die "Can't seek: $!"; + seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; $ENV{DEBUG} = $OPTS{debug}; }}, $conf->{command}, $COMMAND, @fileno, @args); @@ -572,18 +572,18 @@ sub spawn($@) { # child $args->{child}->() if defined $args->{child}; if (defined $args->{in}) { - close $in_wd or die "Can't close: $!"; - open STDIN, '<&', $in_rd or die "Can't dup: $!"; + close $in_wd or die "close: $!"; + open STDIN, '<&', $in_rd or die "dup: $!"; } else { - open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; + open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; } if (!defined $args->{out}) { - open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; + open STDOUT, '>', '/dev/null' or die "open(/dev/null): $!"; } elsif (ref $args->{out} ne 'GLOB') { - close $out_rd or die "Can't close: $!"; - open STDOUT, '>&', $out_wd or die "Can't dup: $!"; + close $out_rd or die "close: $!"; + open STDOUT, '>&', $out_wd or die "dup: $!"; } elsif (fileno(STDOUT) != fileno($args->{out})) { - open STDOUT, '>&', $args->{out} or die "Can't dup: $!"; + open STDOUT, '>&', $args->{out} or die "dup: $!"; } exec { $exec[0] } @exec or die; } @@ -595,18 +595,18 @@ sub spawn($@) { # parent print STDERR "[$$] Forking $exec[0], child PID $pid\n" if $OPTS{debug}; if (defined $args->{in}) { - $in_rd->close() or die "Can't close: $!"; + $in_rd->close() or die "close: $!"; $in_wd->print($args->{in}); - $in_wd->close() or die "Can't close: $!"; + $in_wd->close() or die "close: $!"; } if (defined $args->{out} and ref $args->{out} ne 'GLOB') { - $out_wd->close() or die "Can't close: $!"; + $out_wd->close() or die "close: $!"; if (ref $args->{out} eq 'CODE') { $args->{out}->($out_rd); } elsif (ref $args->{out} eq 'SCALAR') { ${$args->{out}} = do { local $/ = undef; $out_rd->getline() }; } - $out_rd->close() or die "Can't close: $!"; + $out_rd->close() or die "close: $!"; } waitpid $pid => 0; pop @CLEANUP; @@ -631,31 +631,31 @@ sub install_cert($$;$) { chmod(0644 &~ $umask, $fh) or die "chmod: $!"; if ($leafonly) { # keep only the leaf certificate - pipe my $rd, my $wd or die "Can't pipe: $!"; - my $pid = fork // die "Can't fork: $!"; + pipe my $rd, my $wd or die "pipe: $!"; + my $pid = fork // die "fork: $!"; unless ($pid) { - open STDIN, '<&', $rd or die "Can't dup: $!"; - open STDOUT, '>&', $fh or die "Can't dup: $!"; + open STDIN, '<&', $rd or die "dup: $!"; + open STDOUT, '>&', $fh or die "dup: $!"; exec qw/openssl x509 -outform PEM/ or die; } - $rd->close() or die "Can't close: $!"; + $rd->close() or die "close: $!"; $wd->print($chain); - $wd->close() or die "Can't close: $!"; + $wd->close() or die "close: $!"; waitpid $pid => 0; die $? if $? > 0; } else { - $fh->print($chain) or die "Can't print: $!"; + $fh->print($chain) or die "print: $!"; } - $fh->close() or die "Can't close: $!"; + $fh->close() or die "close: $!"; }; my $path = $fh->filename(); if ($@) { print STDERR "Unlinking $path\n" if $OPTS{debug}; - unlink $path or warn "Can't unlink $path: $!"; + unlink $path or warn "unlink($path): $!"; die $@; } - rename($path, $filename) or die "Can't rename $path to $filename: $!"; + rename($path, $filename) or die "rename($path, $filename): $!"; } @@ -687,7 +687,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { unless ($conffile =~ s#/\z## or -d $conffile) { @filenames = ($conffile); } else { - opendir my $dh, $conffile or die "Can't opendir $conffile: $!\n"; + opendir my $dh, $conffile or die "opendir($conffile): $!\n"; while (readdir $dh) { if (/\.conf\z/) { push @filenames, "$conffile/$_"; @@ -723,7 +723,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { if ($OPTS{debug}) { print STDERR "Configuration option for $s:\n"; - print " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); + print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); } my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; @@ -809,16 +809,16 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { if (defined $conf->{chown}) { my ($user, $group) = split /:/, $conf->{chown}, 2; - my $uid = getpwnam($user) // die "Can't getpwnam($user): $!"; - my $gid = defined $group ? (getgrnam($group) // die "Can't getgrnam($group): $!") : -1; + my $uid = getpwnam($user) // die "getpwnam($user): $!"; + my $gid = defined $group ? (getgrnam($group) // die "getgrnam($group): $!") : -1; foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chown($uid, $gid, $_) or die "Can't chown: $!"; + chown($uid, $gid, $_) or die "chown: $!"; } } if (defined $conf->{chmod}) { my $mode = oct($conf->{chmod}) // die; foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { - chmod($mode, $_) or die "Can't chown: $!"; + chmod($mode, $_) or die "chown: $!"; } } @@ -827,7 +827,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { or die "fork: $!"; print $fh $x509; close $fh or die $! ? - "Can't close: $!" : + "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; if (defined $conf->{notify}) { @@ -857,7 +857,7 @@ elsif ($COMMAND eq 'revokeCert' or $COMMAND eq 'revoke-cert') { open my $fh, '-|', qw/openssl x509 -outform DER -in/, $filename or die "fork: $!"; my $der = do { local $/ = undef; <$fh> }; close $fh or die $! ? - "Can't close: $!" : + "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump no_extensions/; @@ -865,7 +865,7 @@ elsif ($COMMAND eq 'revokeCert' or $COMMAND eq 'revoke-cert') { or die "fork: $!"; print $fh2 $der; close $fh2 or die $! ? - "Can't close: $!" : + "close: $!" : "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; if (acme_client({in => $der})) { -- cgit v1.2.3 From 4886d0dd6c77d029209cc09a9e15a89ffb23b9fc Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 19:03:00 +0100 Subject: Sanitize environment when spawning children. Set $HOME, $USER, $SHELL, $PATH, $LOGNAME to appropriate values (and perserve $TERM), which matches the login(1) behavior. --- lacme | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'lacme') diff --git a/lacme b/lacme index f0beac1..a5ba9f4 100755 --- a/lacme +++ b/lacme @@ -234,6 +234,13 @@ sub drop_privileges($$$) { die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check } + # sanitize environment + my $term = $ENV{TERM}; + my @ent = getpwuid($>) or die "getpwuid($>): $!"; + %ENV = ( USER => $ent[0], LOGNAME => $ent[0], HOME => $ent[7], SHELL => $ent[8] ); + $ENV{PATH} = $> == 0 ? "/usr/sbin:/usr/bin:/sbin:/bin" : "/usr/bin:/bin"; + $ENV{TERM} = $term if defined $term; # preserve $TERM + chdir $dir or die "chdir($dir): $!"; } -- cgit v1.2.3 From 044a4cb8b4ba06c6355c5e9978cd5dbfe9df94b2 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:15:03 +0100 Subject: webserver: reopen stdin from /dev/null. Having both lacme(8) and its webserver component reading from the same standard input could yield starvation. --- lacme | 1 + 1 file changed, 1 insertion(+) (limited to 'lacme') diff --git a/lacme b/lacme index a5ba9f4..e5f8715 100755 --- a/lacme +++ b/lacme @@ -374,6 +374,7 @@ sub spawn_webserver() { my $pid = fork() // "fork: $!"; unless ($pid) { drop_privileges($conf->{user}, $conf->{group}, $tmpdir); + open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; set_FD_CLOEXEC($sock, 0); $ENV{DEBUG} = $OPTS{debug}; # use execve(2) rather than a Perl pseudo-process to ensure that -- cgit v1.2.3 From 2e455335a9e8aa9aaace98bc4d61f53a2c93b930 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:19:51 +0100 Subject: Use 'acme-challenge.XXXXXXXXXX' as template for the temporary ACME challenge directory. --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index e5f8715..354a4a0 100755 --- a/lacme +++ b/lacme @@ -327,7 +327,7 @@ sub spawn_webserver() { # create a temporary directory; give write access to the ACME client # and read access to the webserver - my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1) // die; + my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1, TEMPLATE => "acme-challenge.XXXXXXXXXX") // die; chmod 0755, $tmpdir or die "chmod: $!"; if ((my $username = $CONFIG->{client}->{user}) ne '') { my $uid = getpwnam($username) // die "getpwnam($username): $!"; -- cgit v1.2.3 From 32c27cecbe7ab3bdf0cbc984c50b37fbe231e79d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:34:08 +0100 Subject: Set the DEBUG environment variable to 0/1 instead of ""/1. --- lacme | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 354a4a0..d2d8840 100755 --- a/lacme +++ b/lacme @@ -376,7 +376,7 @@ sub spawn_webserver() { drop_privileges($conf->{user}, $conf->{group}, $tmpdir); open STDIN, '<', '/dev/null' or die "open(/dev/null): $!"; set_FD_CLOEXEC($sock, 0); - $ENV{DEBUG} = $OPTS{debug}; + $ENV{DEBUG} = $OPTS{debug} // 0; # use execve(2) rather than a Perl pseudo-process to ensure that # the child doesn't have access to the parent's memory exec $conf->{command}, fileno($sock) or die; @@ -552,7 +552,7 @@ sub acme_client($@) { drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/'); set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; - $ENV{DEBUG} = $OPTS{debug}; + $ENV{DEBUG} = $OPTS{debug} // 0; }}, $conf->{command}, $COMMAND, @fileno, @args); if (defined $cleanup) { -- cgit v1.2.3 From d72df441f86f759bf143df745ff13fd9b90597bf Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Wed, 17 Feb 2021 23:53:31 +0100 Subject: Split client/webserver/accountd commands on whitespace. This doesn't change the default behavior. --- lacme | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index d2d8840..019a5e7 100755 --- a/lacme +++ b/lacme @@ -379,7 +379,8 @@ sub spawn_webserver() { $ENV{DEBUG} = $OPTS{debug} // 0; # use execve(2) rather than a Perl pseudo-process to ensure that # the child doesn't have access to the parent's memory - exec $conf->{command}, fileno($sock) or die; + my ($cmd, @args) = split(/\s+/, $conf->{command}) or die "Empty webserver command\n"; + exec { $cmd } $cmd, @args, fileno($sock) or die; } print STDERR "[$$] Forking ACME webserver bound to $p, child PID $pid\n" if $OPTS{debug}; @@ -505,12 +506,13 @@ sub acme_client($@) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); set_FD_CLOEXEC($s, 0); $client->close() or die "close: $!"; - my @cmd = ($accountd->{command}, '--conn-fd='.fileno($s)); - push @cmd, '--config='.$accountd->{config} if defined $accountd->{config}; - push @cmd, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; - push @cmd, '--quiet' unless lc $accountd->{quiet} eq 'no'; - push @cmd, '--debug' if $OPTS{debug}; - exec { $cmd[0] } @cmd or die; + my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; + push @args, '--conn-fd='.fileno($s); + push @args, '--config='.$accountd->{config} if defined $accountd->{config}; + push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; + push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; + push @args, '--debug' if $OPTS{debug}; + exec { $cmd } $cmd, @args or die; } print STDERR "[$$] Forking lacme-accountd, child PID $pid\n" if $OPTS{debug}; $s->close() or die "close: $!"; @@ -546,6 +548,7 @@ sub acme_client($@) { # use execve(2) rather than a Perl pseudo-process to ensure that the # child doesn't have access to the parent's memory + my ($cmd, @args2) = split(/\s+/, $conf->{command}) or die "Empty client command\n"; my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno set_FD_CLOEXEC($client, 1); my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() { @@ -553,7 +556,7 @@ sub acme_client($@) { set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client); seek($CONFFILE, SEEK_SET, 0) or die "seek: $!"; $ENV{DEBUG} = $OPTS{debug} // 0; - }}, $conf->{command}, $COMMAND, @fileno, @args); + }}, $cmd, @args2, $COMMAND, @fileno, @args); if (defined $cleanup) { @CLEANUP = grep { $_ ne $cleanup } @CLEANUP; @@ -564,7 +567,7 @@ sub acme_client($@) { sub spawn($@) { my $args = shift; - my @exec = @_; + my ($cmd, @args) = @_; # create communication pipes if needed my ($in_rd, $in_wd, $out_rd, $out_wd); @@ -593,7 +596,7 @@ sub spawn($@) { } elsif (fileno(STDOUT) != fileno($args->{out})) { open STDOUT, '>&', $args->{out} or die "dup: $!"; } - exec { $exec[0] } @exec or die; + exec { $cmd } $cmd, @args or die; } push @CLEANUP, sub() { kill 15 => $pid; @@ -601,7 +604,7 @@ sub spawn($@) { }; # parent - print STDERR "[$$] Forking $exec[0], child PID $pid\n" if $OPTS{debug}; + print STDERR "[$$] Forking $cmd, child PID $pid\n" if $OPTS{debug}; if (defined $args->{in}) { $in_rd->close() or die "close: $!"; $in_wd->print($args->{in}); -- cgit v1.2.3 From 42a8f9813716ed3495b6f49edea429b127eef0f0 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 00:49:46 +0100 Subject: accountd: replace internal option --conn-fd=FD with flag --stdio. Using stdin/stdout makes it possible to tunnel the accountd connection through ssh. --- lacme | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 019a5e7..3d3657f 100755 --- a/lacme +++ b/lacme @@ -504,10 +504,12 @@ sub acme_client($@) { my $pid = fork() // "fork: $!"; unless ($pid) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); - set_FD_CLOEXEC($s, 0); + set_FD_CLOEXEC($s, 1); $client->close() or die "close: $!"; + open STDIN, '<&', $s or die "dup: $!"; + open STDOUT, '>&', $s or die "dup: $!"; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; - push @args, '--conn-fd='.fileno($s); + push @args, '--stdio'; push @args, '--config='.$accountd->{config} if defined $accountd->{config}; push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; -- cgit v1.2.3 From baa7c25db322a9472c9155422057ec56aa93f439 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 00:06:49 +0100 Subject: Use File::Basename::dirname(). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To correctly extract the parent directory of the socket path. The previous returned an empty string when the socket path didn't contain ‘/’. --- lacme | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 3d3657f..2f239e2 100755 --- a/lacme +++ b/lacme @@ -27,6 +27,7 @@ my $NAME = 'lacme'; use Errno 'EINTR'; use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC O_CREAT O_EXCL O_WRONLY SEEK_SET/; +use File::Basename 'dirname'; use File::Temp (); use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; use List::Util 'first'; @@ -531,8 +532,8 @@ sub acme_client($@) { $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname # ensure we're the only user with write access to the parent dir - my $dirname = $sockname =~ s/[^\/]+$//r; - @stat = stat($dirname) or die "stat($dirname): $!"; + my $dirname = dirname($sockname); + @stat = stat($dirname) or die "stat($dirname): $!\n"; die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; # ensure we're the only user with read/write access to the socket @@ -695,7 +696,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { : [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ]; my ($conf, %defaults); foreach my $conffile (@$conffiles) { - $conffile = ($CONFFILENAME =~ s#[^/]+\z##r).$conffile unless $conffile =~ /\A\//; + $conffile = dirname($CONFFILENAME) .'/'. $conffile unless $conffile =~ /\A\//; my @filenames; unless ($conffile =~ s#/\z## or -d $conffile) { @filenames = ($conffile); -- cgit v1.2.3 From 1c4fc8c431e69780625600a4ee8526e1a3cbb3f4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 01:04:45 +0100 Subject: lacme(8)'s 'config' option in the [accountd] section no longer have a default value. The previous default, namely /etc/lacme/lacme-accountd.conf, is still honored when there is the user running lacme doesn't have a ~/.config/lacme/lacme-account.conf configuration file. --- lacme | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 2f239e2..7800429 100755 --- a/lacme +++ b/lacme @@ -114,7 +114,7 @@ do { user => '', group => '', command => '@@bindir@@/lacme-accountd', - config => '@@sysconfdir@@/lacme/lacme-accountd.conf', + config => '', privkey => undef, quiet => 'Yes', } @@ -511,7 +511,7 @@ sub acme_client($@) { open STDOUT, '>&', $s or die "dup: $!"; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; push @args, '--stdio'; - push @args, '--config='.$accountd->{config} if defined $accountd->{config}; + push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; push @args, '--debug' if $OPTS{debug}; -- cgit v1.2.3 From ad1856777bf108826008b60a1e70c1e3fbb94ec7 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 01:14:23 +0100 Subject: Deprecate setting 'privkey' in [accountd] section of the lacme(8) configuration file. One need to use the lacme-accountd(1) configuration file for that instead. --- lacme | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 7800429..87a44be 100755 --- a/lacme +++ b/lacme @@ -115,7 +115,7 @@ do { group => '', command => '@@bindir@@/lacme-accountd', config => '', - privkey => undef, + privkey => '', quiet => 'Yes', } ); @@ -501,6 +501,8 @@ sub acme_client($@) { my ($client, $cleanup); my $conf = $CONFIG->{client}; if (defined (my $accountd = $CONFIG->{accountd})) { + warn "Setting 'privkey' in lacme.conf's [accountd] section is deprecated and will become an error in a future release! " + ."Set it in lacme-accountd.conf instead.\n" if $accountd->{privkey} ne ''; socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; my $pid = fork() // "fork: $!"; unless ($pid) { @@ -512,7 +514,7 @@ sub acme_client($@) { my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; push @args, '--stdio'; push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; - push @args, '--privkey='.$accountd->{privkey} if defined $accountd->{privkey}; + push @args, '--privkey='.$accountd->{privkey} if $accountd->{privkey} ne ''; # XXX deprecated in 0.8.0 push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; push @args, '--debug' if $OPTS{debug}; exec { $cmd } $cmd, @args or die; -- cgit v1.2.3 From 5cf25633d48f79f39ab8c35883e1e437b3a058e4 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 02:05:48 +0100 Subject: lacme: Preserve $GPG_TTY when spawning the accountd. This is needed for gpg-encrypted privkeys. --- lacme | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index 87a44be..d141b62 100755 --- a/lacme +++ b/lacme @@ -503,14 +503,16 @@ sub acme_client($@) { if (defined (my $accountd = $CONFIG->{accountd})) { warn "Setting 'privkey' in lacme.conf's [accountd] section is deprecated and will become an error in a future release! " ."Set it in lacme-accountd.conf instead.\n" if $accountd->{privkey} ne ''; + my $GPG_TTY = $ENV{GPG_TTY}; socketpair($client, my $s, AF_UNIX, SOCK_STREAM, PF_UNSPEC) or die "socketpair: $!"; my $pid = fork() // "fork: $!"; unless ($pid) { drop_privileges($accountd->{user}, $accountd->{group}, '/'); - set_FD_CLOEXEC($s, 1); $client->close() or die "close: $!"; open STDIN, '<&', $s or die "dup: $!"; open STDOUT, '>&', $s or die "dup: $!"; + set_FD_CLOEXEC($s, 1); + $ENV{GPG_TTY} = $GPG_TTY if defined $GPG_TTY; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; push @args, '--stdio'; push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; -- cgit v1.2.3 From becac5d1fad959a0ffb0d67afed0d4d7069c3114 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 23:57:36 +0100 Subject: Use real UID not effective UID in environment sanitation. Not that it make a difference since we don't run suid. --- lacme | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index d141b62..9f46b47 100755 --- a/lacme +++ b/lacme @@ -237,9 +237,9 @@ sub drop_privileges($$$) { # sanitize environment my $term = $ENV{TERM}; - my @ent = getpwuid($>) or die "getpwuid($>): $!"; + my @ent = getpwuid($<) or die "getpwuid($<): $!"; %ENV = ( USER => $ent[0], LOGNAME => $ent[0], HOME => $ent[7], SHELL => $ent[8] ); - $ENV{PATH} = $> == 0 ? "/usr/sbin:/usr/bin:/sbin:/bin" : "/usr/bin:/bin"; + $ENV{PATH} = $< == 0 ? "/usr/sbin:/usr/bin:/sbin:/bin" : "/usr/bin:/bin"; $ENV{TERM} = $term if defined $term; # preserve $TERM chdir $dir or die "chdir($dir): $!"; -- cgit v1.2.3 From 8de74ffb4a2008a61c05e9a24c8fa9b14858d2be Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 18:31:20 +0100 Subject: Remove dependency on List::Util (core module). --- lacme | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 9f46b47..cb399f9 100755 --- a/lacme +++ b/lacme @@ -30,7 +30,6 @@ use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC O_CREAT O_EXCL O_WRONLY SEEK_SET/; use File::Basename 'dirname'; use File::Temp (); use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; -use List::Util 'first'; use POSIX (); use Socket 1.95 qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC INADDR_ANY IN6ADDR_ANY IPPROTO_IPV6 @@ -744,15 +743,15 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf); } - my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; - unless (defined $certtype) { + my $cert = $conf->{'certificate-chain'} // $conf->{'certificate'}; + unless (defined $cert) { print STDERR "[$s] Warning: Missing 'certificate' and 'certificate-chain', skipping\n"; $rv = 1; next; } # skip certificates that expire at least $conf->{'min-days'} days in the future - if (-f $conf->{$certtype} and defined (my $t = x509_enddate($conf->{$certtype}))) { + if (-f $cert and defined (my $t = x509_enddate($cert))) { my $d = $OPTS{'min-days'} // $conf->{'min-days'} // 21; if ($d >= 0 and $t - time > $d*86400) { my $d = POSIX::strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($t)); -- cgit v1.2.3 From c342643613a940e147d9b598666823d6baa19a0d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Fri, 19 Feb 2021 01:36:48 +0100 Subject: wibble --- lacme | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index cb399f9..9a62cbb 100755 --- a/lacme +++ b/lacme @@ -295,7 +295,7 @@ sub spawn_webserver() { die "Error: Refusing to use non-empty challenge directory $dir\n" unless $_ eq '.' or $_ eq '..'; } - closedir $dh or die "close: $!"; + closedir $dh or die "closedir: $!"; undef $dh; # use a "lock file" (NFS-friendly) to avoid concurrent usages @@ -309,7 +309,7 @@ sub spawn_webserver() { push @CLEANUP, sub() { if (opendir(my $dh, $dir)) { my @files = grep { $_ ne '.' and $_ ne '..' and $_ ne $lockfile } readdir $dh; - closedir $dh or warn "close: $!"; + closedir $dh or warn "closedir: $!"; push @files, $lockfile; # unlink $lockfile last foreach (@files) { die unless /\A(.+)\z/; # untaint @@ -712,7 +712,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { warn "$conffile/$_ has unknown suffix, skipping\n"; } } - closedir $dh; + closedir $dh or die "closedir: $!"; } foreach my $filename (sort @filenames) { print STDERR "Reading $filename\n" if $OPTS{debug}; -- cgit v1.2.3 From 0ef94d85e58497dcb2c4c954cadcac918032467a Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 18 Feb 2021 21:07:01 +0100 Subject: Add %-specifiers support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lacme(8): for --config=, --socket=, --config-certs= (and ‘socket’/ ‘config-certs’/‘challenge-directory’ configuration options *before* privilege drop; and for the [accountd] section ‘command’/‘config’ configuration options *after* privilege drop). lacme-accountd(1): for --config=, --socket= and --privkey= (and ‘socket’/‘privkey’ configuration options). This also changes the default configuration file location. lacme(8) and lacme-accountd(1) now respectively use /etc/lacme/lacme.conf resp. /etc/lacme/lacme-accountd.conf when running as root, and $XDG_CONFIG_HOME/lacme/lacme.conf resp. $XDG_CONFIG_HOME/lacme/lacme-accountd.conf when running as a normal user. There is no fallback to /etc anymore. --- lacme | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index 9a62cbb..ad7e1d8 100755 --- a/lacme +++ b/lacme @@ -75,13 +75,33 @@ $COMMAND = $COMMAND =~ /\A(account|newOrder|new-cert|revokeCert|revoke-cert)\z/ : usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND @ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV +sub env_fallback($$) { + my $v = $ENV{ shift() }; + return (defined $v and $v ne "") ? $v : shift; +} +sub spec_expand($) { + my $str = shift; + $str =~ s#%(.)# my $x = + $1 eq "C" ? ($< == 0 ? "@@localstatedir@@/cache" : env_fallback(XDG_CACHE_HOME => "$ENV{HOME}/.cache")) + : $1 eq "E" ? ($< == 0 ? "@@sysconfdir@@" : env_fallback(XDG_CONFIG_HOME => "$ENV{HOME}/.config")) + : $1 eq "g" ? (getgrgid((split /\s/,$()[0]))[0] + : $1 eq "G" ? $( =~ s/\s.*//r + : $1 eq "h" ? (getpwuid($<))[7] + : $1 eq "u" ? (getpwuid($<))[0] + : $1 eq "U" ? $< + : $1 eq "t" ? ($< == 0 ? "@@runstatedir@@" : $ENV{XDG_RUNTIME_DIR}) + : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") + : $1 eq "%" ? "%" + : die "Error: \"$str\" has unknown specifier %$1\n"; + die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x; + $x; + #ge; + return $str; +} + sub set_FD_CLOEXEC($$); -my $CONFFILENAME = $OPTS{config} // first { -f $_ } - ( ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config") . "/lacme/$NAME.conf" - , "@@sysconfdir@@/lacme/$NAME.conf" - ); +my $CONFFILENAME = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf"); do { - die "Error: Can't find configuration file\n" unless defined $CONFFILENAME; print STDERR "Using configuration file: $CONFFILENAME\n" if $OPTS{debug}; open $CONFFILE, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n"; my $conf = do { local $/ = undef; <$CONFFILE> }; @@ -93,7 +113,7 @@ do { my $accountd = defined $OPTS{socket} ? 0 : exists $h->{accountd} ? 1 : 0; my %valid = ( client => { - socket => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.lacme" : undef), + socket => '%t/S.lacme', user => '@@lacme_client_user@@', group => '@@lacme_client_group@@', command => '@@libexecdir@@/lacme/client', @@ -285,6 +305,7 @@ sub spawn_webserver() { # Use existing HTTPd to serve challenge files using 'challenge-directory' # as document root if (defined (my $dir = $conf->{'challenge-directory'})) { + $dir = spec_expand($dir); print STDERR "[$$] Using existing webserver on $dir\n" if $OPTS{debug}; # lacme(8) doesn't have the list of challenge files to delete on # cleanup -- instead, we unlink all files and fails at @@ -513,8 +534,9 @@ sub acme_client($@) { set_FD_CLOEXEC($s, 1); $ENV{GPG_TTY} = $GPG_TTY if defined $GPG_TTY; my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n"; + $_ = spec_expand($_) foreach ($cmd, @args); # expand %-specifiers after privilege drop and whitespace split push @args, '--stdio'; - push @args, '--config='.$accountd->{config} if $accountd->{config} ne ''; + push @args, '--config='.spec_expand($accountd->{config}) if $accountd->{config} ne ''; push @args, '--privkey='.$accountd->{privkey} if $accountd->{privkey} ne ''; # XXX deprecated in 0.8.0 push @args, '--quiet' unless lc $accountd->{quiet} eq 'no'; push @args, '--debug' if $OPTS{debug}; @@ -531,7 +553,7 @@ sub acme_client($@) { } else { my @stat; - my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; + my $sockname = spec_expand($OPTS{socket} // $conf->{socket}); $sockname = $sockname =~ /\A(\p{Print}+)\z/ ? $1 : die "Invalid socket name\n"; # untaint $sockname # ensure we're the only user with write access to the parent dir @@ -697,6 +719,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') { my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'} : defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ] : [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ]; + $_ = spec_expand($_) foreach @$conffiles; my ($conf, %defaults); foreach my $conffile (@$conffiles) { $conffile = dirname($CONFFILENAME) .'/'. $conffile unless $conffile =~ /\A\//; -- cgit v1.2.3 From 74c0a11722cf1e01b9a9834e89a07b55eaf01080 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 20 Feb 2021 22:05:18 +0100 Subject: lacme-accountd: new setting 'logfile' to log signature requests. Prefixed with a timestamp. --- lacme | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lacme') diff --git a/lacme b/lacme index ad7e1d8..88ab78d 100755 --- a/lacme +++ b/lacme @@ -558,12 +558,12 @@ sub acme_client($@) { # ensure we're the only user with write access to the parent dir my $dirname = dirname($sockname); - @stat = stat($dirname) or die "stat($dirname): $!\n"; - die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + @stat = stat($dirname) or die "Error: stat($dirname): $!\n"; + die "Error: Insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; # ensure we're the only user with read/write access to the socket @stat = stat($sockname) or die "Can't stat $sockname: $! (Is lacme-accountd running?)\n"; - die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; + die "Error: Insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; # connect(2) to the socket socket($client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; -- cgit v1.2.3 From d56b957dbae6c8214d50ce88d0ea04eb4654b843 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 17:34:51 +0100 Subject: wording --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index 88ab78d..1e51c6c 100755 --- a/lacme +++ b/lacme @@ -93,7 +93,7 @@ sub spec_expand($) { : $1 eq "T" ? env_fallback(TMPDIR => "/tmp") : $1 eq "%" ? "%" : die "Error: \"$str\" has unknown specifier %$1\n"; - die "Error: undefined expansion %$1 in \"$str\"\n" unless defined $x; + die "Error: Undefined expansion %$1 in \"$str\"\n" unless defined $x; $x; #ge; return $str; -- cgit v1.2.3 From 3eba02ef820a393bd5781be9f8fcda1611ae7c3d Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Mon, 22 Feb 2021 03:19:57 +0100 Subject: Prepare new release v0.8.0. --- lacme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lacme') diff --git a/lacme b/lacme index 1e51c6c..731535f 100755 --- a/lacme +++ b/lacme @@ -22,7 +22,7 @@ use v5.14.2; use strict; use warnings; -our $VERSION = '0.3'; +our $VERSION = '0.8.0'; my $NAME = 'lacme'; use Errno 'EINTR'; -- cgit v1.2.3