aboutsummaryrefslogtreecommitdiffstats
path: root/lacme
diff options
context:
space:
mode:
Diffstat (limited to 'lacme')
-rwxr-xr-xlacme167
1 files changed, 91 insertions, 76 deletions
diff --git a/lacme b/lacme
index 731535f..21a184c 100755
--- a/lacme
+++ b/lacme
@@ -22,7 +22,7 @@ use v5.14.2;
use strict;
use warnings;
-our $VERSION = '0.8.0';
+our $VERSION = '0.8.1';
my $NAME = 'lacme';
use Errno 'EINTR';
@@ -37,13 +37,14 @@ use Socket 1.95 qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC
use Config::Tiny ();
use Date::Parse ();
-use Net::SSLeay ();
+use JSON ();
+use Net::SSLeay 1.46 ();
# Clean up PATH
$ENV{PATH} = join ':', qw{/usr/bin /bin};
delete @ENV{qw/IFS CDPATH ENV BASH_ENV/};
-my ($COMMAND, %OPTS, $CONFFILE, $CONFIG, @CLEANUP);
+my ($COMMAND, %OPTS, $CONFIG, @CLEANUP);
$SIG{$_} = sub() { exit 1 } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM
@@ -99,14 +100,12 @@ sub spec_expand($) {
return $str;
}
-sub set_FD_CLOEXEC($$);
my $CONFFILENAME = spec_expand($OPTS{config} // "%E/lacme/$NAME.conf");
do {
print STDERR "Using configuration file: $CONFFILENAME\n" if $OPTS{debug};
- open $CONFFILE, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n";
- my $conf = do { local $/ = undef; <$CONFFILE> };
- # don't close $CONFFILE so we can pass it to the client
- set_FD_CLOEXEC($CONFFILE, 1);
+ open my $fh, '<', $CONFFILENAME or die "Can't open $CONFFILENAME: $!\n";
+ my $conf = do { local $/ = undef; <$fh> };
+ close $fh or die "close: $!";
my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n";
my $defaults = delete $h->{_} // {};
@@ -240,7 +239,7 @@ sub drop_privileges($$$) {
# set effective and real gid; also set the list of supplementary gids to that single gid
if ($group ne '') {
- my $gid = getgrnam($group) // die "getgrnam($group): $!";
+ my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n");
$) = "$gid $gid";
die "setgroups: $!" if $@;
POSIX::setgid($gid) or die "setgid: $!";
@@ -249,7 +248,7 @@ sub drop_privileges($$$) {
# set effective and real uid
if ($user ne '') {
- my $uid = getpwnam($user) // die "getpwnam($user): $!";
+ my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n");
POSIX::setuid($uid) or die "setuid: $!";
die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check
}
@@ -351,7 +350,7 @@ sub spawn_webserver() {
my $tmpdir = File::Temp::->newdir(CLEANUP => 1, TMPDIR => 1, TEMPLATE => "acme-challenge.XXXXXXXXXX") // die;
chmod 0755, $tmpdir or die "chmod: $!";
if ((my $username = $CONFIG->{client}->{user}) ne '') {
- my $uid = getpwnam($username) // die "getpwnam($username): $!";
+ my $uid = getpwnam($username) // die "getpwnam($username)", ($! ? ": $!" : "\n");
chown($uid, -1, $tmpdir) or die "chown: $!";
}
@@ -376,14 +375,14 @@ sub spawn_webserver() {
if ($domain == AF_UNIX) {
# bind(2) with a loose umask(2) to allow anyone to connect
- my $umask = umask(0111) // die "umask: $!";
+ my $umask = umask(0111) // die;
my $path = Socket::unpack_sockaddr_un($sockaddr);
bind($sock, $sockaddr) or die "Couldn't bind to $p: $!";
push @CLEANUP, sub() {
print STDERR "Unlinking $path\n" if $OPTS{debug};
unlink $path or warn "Warning: Couldn't unlink $path: $!";
};
- umask($umask) // die "umask: $!";
+ umask($umask) // die;
}
else {
bind($sock, $sockaddr) or die "Couldn't bind to $p: $!";
@@ -536,7 +535,7 @@ sub acme_client($@) {
my ($cmd, @args) = split(/\s+/, $accountd->{command}) or die "Empty accountd command\n";
$_ = spec_expand($_) foreach ($cmd, @args); # expand %-specifiers after privilege drop and whitespace split
push @args, '--stdio';
- push @args, '--config='.spec_expand($accountd->{config}) if $accountd->{config} ne '';
+ push @args, '--config='.$accountd->{config} if $accountd->{config} ne '';
push @args, '--privkey='.$accountd->{privkey} if $accountd->{privkey} ne ''; # XXX deprecated in 0.8.0
push @args, '--quiet' unless lc $accountd->{quiet} eq 'no';
push @args, '--debug' if $OPTS{debug};
@@ -573,18 +572,26 @@ sub acme_client($@) {
die "connect: $!";
}
}
+ set_FD_CLOEXEC($client, 1);
+
+ my $client_config;
+ do {
+ my $tmp = File::Temp::->new(TMPDIR => 1, TEMPLATE => "lacme-client.conf.json-XXXXXXXXXX", UNLINK => 1) // die;
+ print $tmp JSON::->new->encode($conf);
+ open $client_config, "<", $tmp->filename() or die "open: $!";
+ };
# use execve(2) rather than a Perl pseudo-process to ensure that the
# child doesn't have access to the parent's memory
my ($cmd, @args2) = split(/\s+/, $conf->{command}) or die "Empty client command\n";
- my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno
- set_FD_CLOEXEC($client, 1);
+ my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($client_config, $client); # untaint fileno
my $rv = spawn({in => $args->{in}, out => $args->{out}, child => sub() {
drop_privileges($conf->{user}, $conf->{group}, $args->{chdir} // '/');
- set_FD_CLOEXEC($_, 0) foreach ($CONFFILE, $client);
- seek($CONFFILE, SEEK_SET, 0) or die "seek: $!";
+ umask(0022) // die;
+ set_FD_CLOEXEC($_, 0) for ($client_config, $client);
$ENV{DEBUG} = $OPTS{debug} // 0;
}}, $cmd, @args2, $COMMAND, @fileno, @args);
+ close $client_config or die "close: $!\n";
if (defined $cleanup) {
@CLEANUP = grep { $_ ne $cleanup } @CLEANUP;
@@ -657,44 +664,43 @@ sub spawn($@) {
#############################################################################
# Install the certificate (optionally excluding the chain of trust)
#
-sub install_cert($$;$) {
- my ($filename, $chain, $leafonly) = @_;
+sub install_cert($$%) {
+ my ($path, $content, %args) = @_;
- my ($dirname, $basename) =
- $filename =~ /\A(.*)\/([^\/]+)\z/ ? ($1, $2) : ('.', $filename);
- my $fh = File::Temp::->new(UNLINK => 0, DIR => $dirname,
- TEMPLATE => "$basename.XXXXXX") // die;
+ my $fh = File::Temp::->new(TEMPLATE => "$path.XXXXXXXXXX", UNLINK => 0) // die;
+ my $path_tmp = $fh->filename();
eval {
- my $umask = umask() // die "umask: $!";
- chmod(0644 &~ $umask, $fh) or die "chmod: $!";
- if ($leafonly) {
- # keep only the leaf certificate
- pipe my $rd, my $wd or die "pipe: $!";
- my $pid = fork // die "fork: $!";
- unless ($pid) {
- open STDIN, '<&', $rd or die "dup: $!";
- open STDOUT, '>&', $fh or die "dup: $!";
- exec qw/openssl x509 -outform PEM/ or die;
- }
- $rd->close() or die "close: $!";
- $wd->print($chain);
- $wd->close() or die "close: $!";
+ $fh->print($content) or die "print: $!";
- waitpid $pid => 0;
- die $? if $? > 0;
+ my $mode;
+ if ((my $m = $args{mode}) ne "") {
+ die "Not an octal string: $m\n" unless $m =~ /^[0-9]+$/;
+ $mode = oct($m);
} else {
- $fh->print($chain) or die "print: $!";
+ my $umask = umask() // die;
+ $mode = 0644 &~ $umask;
}
+ chmod($mode, $fh) or die "chown: $!";
+
+ if ((my $owner = $args{owner}) ne "") {
+ my ($user, $group) = split /:/, $owner, 2;
+ my $uid = getpwnam($user) // die "getpwnam($user)", ($! ? ": $!" : "\n");
+ my $gid = getgrnam($group) // die "getgrnam($group)", ($! ? ": $!" : "\n") if defined $group;
+ chown($uid, $gid // -1, $fh) or die "chown: $!";
+ }
+
$fh->close() or die "close: $!";
};
- my $path = $fh->filename();
+
if ($@) {
- print STDERR "Unlinking $path\n" if $OPTS{debug};
- unlink $path or warn "unlink($path): $!";
+ print STDERR "Unlinking $path_tmp\n" if $OPTS{debug};
+ unlink $path_tmp or warn "unlink($path_tmp): $!";
die $@;
+ } else {
+ # atomically replace $path if it exists
+ rename($path_tmp, $path) or die "rename($path_tmp, $path): $!";
}
- rename($path, $filename) or die "rename($path, $filename): $!";
}
@@ -743,7 +749,8 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') {
my $def = delete $h->{_} // {};
$defaults{$_} = $def->{$_} foreach keys %$def;
my @valid = qw/certificate certificate-chain certificate-key min-days CAfile
- hash keyUsage subject subjectAltName tlsfeature chown chmod notify/;
+ hash keyUsage subject subjectAltName tlsfeature
+ owner chown mode chmod notify/;
foreach my $s (keys %$h) {
$conf->{$s} = { map { $_ => delete $h->{$s}->{$_} } @valid };
die "Unknown option(s) in [$s]: ".join(', ', keys %{$h->{$s}})."\n" if %{$h->{$s}};
@@ -766,15 +773,15 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') {
print STDERR " $_ = $conf->{$_}\n" foreach grep { defined $conf->{$_} } (sort keys %$conf);
}
- my $cert = $conf->{'certificate-chain'} // $conf->{'certificate'};
- unless (defined $cert) {
+ my @certpaths = grep {defined $_ and $_ ne ""} @$conf{qw/certificate-chain certificate/};
+ unless (@certpaths) {
print STDERR "[$s] Warning: Missing 'certificate' and 'certificate-chain', skipping\n";
$rv = 1;
next;
}
# skip certificates that expire at least $conf->{'min-days'} days in the future
- if (-f $cert and defined (my $t = x509_enddate($cert))) {
+ if (-f $certpaths[0] and defined (my $t = x509_enddate($certpaths[0]))) {
my $d = $OPTS{'min-days'} // $conf->{'min-days'} // 21;
if ($d >= 0 and $t - time > $d*86400) {
my $d = POSIX::strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($t));
@@ -807,18 +814,37 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') {
}
}
- my ($x509, $csr_pubkey, $x509_pubkey);
+ my $chain;
print STDERR "[$s] Will request authorization for: ".join(", ", @authz), "\n" if $OPTS{debug};
- if (acme_client({chdir => $challenge_dir, in => $csr, out => \$x509}, @authz)) {
+ if (acme_client({chdir => $challenge_dir, in => $csr, out => \$chain}, @authz)) {
print STDERR "[$s] Error: Couldn't issue X.509 certificate!\n";
$rv = 1;
next;
}
+ my $cert;
+ eval {
+ my $mem = Net::SSLeay::BIO_s_mem() or die;
+ my $bio = Net::SSLeay::BIO_new($mem) or die;
+ die "incomplete write" unless
+ Net::SSLeay::BIO_write($bio, $chain) == length($chain);
+ my $x509 = Net::SSLeay::PEM_read_bio_X509($bio);
+ $cert = Net::SSLeay::PEM_get_string_X509($x509);
+ Net::SSLeay::BIO_free($bio) or die;
+ };
+ if ($@) {
+ print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n";
+ $rv = 1;
+ next;
+ }
+
# extract pubkeys from CSR and cert, and ensure they match
+ # XXX would be nice to use X509_get_X509_PUBKEY and X509_REQ_get_X509_PUBKEY here,
+ # or EVP_PKEY_cmp(), but unfortunately Net::SSLeay 1.88 doesn't support these
+ my ($cert_pubkey, $csr_pubkey);
+ spawn({in => $cert, out => \$cert_pubkey}, qw/openssl x509 -inform PEM -noout -pubkey/);
spawn({in => $csr, out => \$csr_pubkey }, qw/openssl req -inform DER -noout -pubkey/);
- spawn({in => $x509, out => \$x509_pubkey}, qw/openssl x509 -inform PEM -noout -pubkey/);
- unless (defined $x509_pubkey and defined $csr_pubkey and $x509_pubkey eq $csr_pubkey) {
+ unless (defined $cert_pubkey and defined $csr_pubkey and $cert_pubkey eq $csr_pubkey) {
print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n";
$rv = 1;
next;
@@ -826,7 +852,7 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') {
# verify certificate validity against the CA bundle
if ((my $CAfile = $conf->{CAfile} // '@@datadir@@/lacme/ca-certificates.crt') ne '') {
- my %args = (in => $x509);
+ my %args = (in => $cert);
$args{out} = \*STDERR if $OPTS{debug};
my @options = ('-trusted', $CAfile, '-purpose', 'sslserver', '-x509_strict');
push @options, '-show_chain' if $OPTS{debug};
@@ -838,34 +864,23 @@ elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') {
}
# install certificate
- if (defined $conf->{'certificate'}) {
- print STDERR "Installing X.509 certificate $conf->{'certificate'}\n";
- install_cert($conf->{'certificate'}, $x509, 1);
+ my %install_opts = (
+ mode => $conf->{mode} // $conf->{chmod} // "",
+ owner => $conf->{owner} // $conf->{chown} // ""
+ );
+ if ((my $path = $conf->{'certificate'} // "") ne "") {
+ print STDERR "Installing X.509 certificate $path\n";
+ install_cert($path => $cert, %install_opts);
}
- if (defined $conf->{'certificate-chain'}) {
- print STDERR "Installing X.509 certificate chain $conf->{'certificate-chain'}\n";
- install_cert($conf->{'certificate-chain'}, $x509);
- }
-
- if (defined $conf->{chown}) {
- my ($user, $group) = split /:/, $conf->{chown}, 2;
- my $uid = getpwnam($user) // die "getpwnam($user): $!";
- my $gid = defined $group ? (getgrnam($group) // die "getgrnam($group): $!") : -1;
- foreach (grep defined, @$conf{qw/certificate certificate-chain/}) {
- chown($uid, $gid, $_) or die "chown: $!";
- }
- }
- if (defined $conf->{chmod}) {
- my $mode = oct($conf->{chmod}) // die;
- foreach (grep defined, @$conf{qw/certificate certificate-chain/}) {
- chmod($mode, $_) or die "chown: $!";
- }
+ if ((my $path = $conf->{'certificate-chain'} // "") ne "") {
+ print STDERR "Installing X.509 certificate chain $path\n";
+ install_cert($path => $chain, %install_opts);
}
my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump/;
open my $fh, '|-', qw/openssl x509 -noout -fingerprint -sha256 -text -certopt/, @certopts
or die "fork: $!";
- print $fh $x509;
+ print $fh $cert;
close $fh or die $! ?
"close: $!" :
"Error: x509(1ssl) exited with value ".($? >> 8)."\n";