diff options
Diffstat (limited to 'letsencrypt')
| -rwxr-xr-x | letsencrypt | 704 | 
1 files changed, 0 insertions, 704 deletions
| diff --git a/letsencrypt b/letsencrypt deleted file mode 100755 index d11b569..0000000 --- a/letsencrypt +++ /dev/null @@ -1,704 +0,0 @@ -#!/usr/bin/perl -T - -#---------------------------------------------------------------------- -# Let's Encrypt ACME client -# Copyright © 2016 Guilhem Moulin <guilhem@fripost.org> -# -# 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 -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program.  If not, see <http://www.gnu.org/licenses/>. -#---------------------------------------------------------------------- - -use strict; -use warnings; - -our $VERSION = '0.0.1'; -my $NAME = 'letsencrypt'; - -use Errno qw/EADDRINUSE EINTR/; -use Fcntl qw/F_GETFD F_SETFD FD_CLOEXEC SEEK_SET/; -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 qw/PF_INET PF_INET6 PF_UNIX INADDR_ANY IN6ADDR_ANY -              SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/; - -use Config::Tiny (); -use Net::SSLeay (); - -# Clean up PATH -$ENV{PATH} = join ':', qw{/usr/bin /bin}; -delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; - -my ($COMMAND, %OPTS, $CONFFILE, $CONFIG, @CLEANUP); -$SIG{$_} = sub() { exit 1 } foreach qw/INT TERM/; # run the END block upon SIGINT/SIGTERM - - -############################################################################# -# Parse and validate configuration -# -sub usage(;$$) { -    my $rv = shift // 0; -    if ($rv) { -        my $msg = shift; -        print STDERR $msg."\n" if defined $msg; -        print STDERR "Try '$NAME --help' or consult the manpage for more information.\n"; -    } -    else { -        print STDERR "Usage: $NAME [--config=FILE] [--socket=PATH] [OPTIONS] COMMAND [ARGUMENT ..]\n" -                    ."Consult the manpage for more information.\n"; -    } -    exit $rv; -} -usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s socket=s agreement-uri=s debug help|h/); -usage(0) if $OPTS{help}; - -$COMMAND = shift(@ARGV) // usage(1, "Missing command"); -$COMMAND = $COMMAND =~ /\A(new-reg|reg=\p{Print}*|new-cert|revoke-cert)\z/ ? $1 -         : usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND -@ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV - -do { -    my $conffile = $OPTS{config} // first { -f $_ } -        ( "./$NAME.conf" -        , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/letsencrypt-tiny/$NAME.conf" -        , "/etc/letsencrypt-tiny/$NAME.conf" -        ); -    die "Error: Can't find configuration file\n" unless defined $conffile; -    print STDERR "Using configuration file: $conffile\n" if $OPTS{debug}; -    open $CONFFILE, '<', $conffile or die "Can't open $conffile: $!\n"; -    my $conf = do { local $/ = undef, <$CONFFILE> }; -    # don't close $CONFFILE so we can pass it to the client - -    my $h = Config::Tiny::->read_string($conf) or die Config::Tiny::->errstr()."\n"; -    my $defaults = delete $h->{_} // {}; -    my %valid = ( -        client => { -            socket  => (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.letsencrypt" : undef), -            user    => 'nobody', -            group   => 'nogroup', -            command => '/usr/lib/letsencrypt-tiny/client', -            # the rest is for the ACME client -            map {$_ => undef} qw/server timeout SSL_verify SSL_version SSL_cipher_list/ -        }, -        webserver => { -            listen                => ':80', -            'challenge-directory' => '/var/www/acme-challenge', -            user                  => 'www-data', -            group                 => 'www-data', -            command               => '/usr/lib/letsencrypt-tiny/webserver', -            iptables              => 'Yes' - -        } -    ); -    foreach my $s (keys %valid) { -        my $h = delete $h->{$s} // {}; -        my %v = map { $_ => delete $h->{$_} // $valid{$s}->{$_} } keys %{$valid{$s}}; -        die "Unknown option(s) in [$s]: ".join(', ', keys %$h)."\n" if %$h; -        $h->{$_} //= $defaults->{$_} foreach keys %$defaults; -        $CONFIG->{$s} = \%v; -    } -    die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; -    $CONFIG->{_} = $defaults; -}; - -# Regular expressions for domain validation -my $RE_LABEL  = qr/[0-9a-z](?:[0-9a-z\x2D]{0,61}[0-9a-z])?/aai; -my $RE_DOMAIN = qr/$RE_LABEL(?:\.$RE_LABEL)+/; - - -############################################################################# -# Generate a Certificate Signing Request (in DER format) -# -sub gen_csr(%) { -    my %args = @_; -    return unless defined $args{'certificate-key'} and defined $args{subject}; -    return if defined $args{hash} and !grep { $args{hash} eq $_ } qw/md5 rmd160 sha1 sha224 sha256 sha384 sha512/; - -    my $config = File::Temp::->new(SUFFIX => '.conf', TMPDIR => 1) // die; -    $config->print( -        "[ req ]\n", -        "distinguished_name = req_distinguished_name\n", -        "req_extensions     = v3_req\n", - -        "[ req_distinguished_name ]\n", - -        "[ v3_req ]\n", -        # XXX Golang errors on extensions marked critical -        # https://github.com/letsencrypt/boulder/issues/565 -        #"basicConstraints     = critical, CA:FALSE\n", -        "basicConstraints     = CA:FALSE\n", -        "subjectKeyIdentifier = hash\n" -    ); -    #$config->print("keyUsage = critical, $args{keyUsage}\n")   if defined $args{keyUsage}; -    $config->print("keyUsage = $args{keyUsage}\n")             if defined $args{keyUsage}; -    $config->print("subjectAltName = $args{subjectAltName}\n") if defined $args{subjectAltName}; -    $config->close() or die "Can't close: $!"; - -    my @args = (qw/-new -batch -key/, $args{'certificate-key'}); -    push @args, "-$args{hash}" if defined $args{hash}; -    push @args, '-subj', $args{subject}, '-config', $config->filename(), qw/-reqexts v3_req/; - -    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; - -    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: $!"; -            exec qw/openssl req -noout -text -inform DER/ or die; -        } -        $rd->close() or die "Can't close: $!"; -        $wd->print($csr); -        $wd->close() or die "Can't close: $!"; - -        waitpid $pid => 0; -        die $? if $? > 0; -    } - -    return $csr; -} - - -############################################################################# -# Get the timestamp of the given cert's expiration date. -# Internally the expiration date is stored as a RFC3339 string (such as -# yyyy-mm-ddThh:mm:ssZ); we convert it to a timestamp manually. -# -sub x509_enddate($) { -    my $filename = shift; -    my ($bio, $x509, $time, $dt); - -    $bio  = Net::SSLeay::BIO_new_file($filename, 'r'); -    $x509 = Net::SSLeay::PEM_read_bio_X509($bio)        if defined $bio; -    $time = Net::SSLeay::X509_get_notAfter($x509)       if defined $x509; -    $dt   = Net::SSLeay::P_ASN1_TIME_get_isotime($time) if defined $time; - -    my $t; -    if (defined $dt and $dt =~ s/\A(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})//) { -        # RFC3339 datetime strings; assume epoch is on January 1 of $epoch_year -        my ($y, $m, $d, $h, $min, $s) = ($1, $2, $3, $4, $5, $6); -        my (undef,undef,undef,undef,undef,$epoch_year,undef,undef,undef) = gmtime(0); -        $t = 0; -        foreach (($epoch_year+1900) .. $y-1) { -            $t += 365*86400; -            $t += 86400 if ($_ % 4 == 0 and $_ % 100 != 0) or ($_ % 400 == 0); # leap -        } - -        if ($m > 1) { -            my @m = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); -            $m[1]++ if ($y % 4 == 0 and $y % 100 != 0) or ($y % 400 == 0); # leap -            $t += 86400*$m[$_] for (0 .. $m-2); -        } - -        $t += 86400*($d-1); -        $t += $s + 60*($min + 60*$h); - -        $dt =~ s/\A\.(\d{1,9})\d*//; # ignore nanosecs - -        if ($dt =~ /\A([+-])(\d{2}):(\d{2})\z/) { -            my $tz = 60*($3 + 60*$2); -            $t = $1 eq '-' ? ($t+$tz) : ($t-$tz); -        } -    } - -    Net::SSLeay::X509_free($x509) if defined $x509; -    Net::SSLeay::BIO_free($bio)   if defined $bio; -    return $t; -} - - -############################################################################# -# Drop privileges and chdir afterwards -# -sub drop_privileges($$$) { -    my ($user, $group, $dir) = @_; - -    # 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): $!"; -        $) = "$gid $gid"; -        die "Can't setgroups: $!" if $@; -        POSIX::setgid($gid) or die "Can't 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: $!"; -        die "Couldn't setuid/seteuid" unless $< == $uid and $> == $uid; # safety check -    } - -    chdir $dir or die "Can't chdir to $dir: $!"; -} - - -############################################################################# -# Ensure the FD_CLOEXEC bit is $set on $fd -# -sub set_FD_CLOEXEC($$) { -    my ($fd, $set) = @_; -    my $flags = fcntl($fd, F_GETFD, 0) or die "Can't 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: $!"; -} - - -############################################################################# -# Try to spawn a webserver to serve ACME challenges, and return the -# temporary challenge directory. -# If a webserver is already listening, symlink the 'challenge-directory' -# configuration option to the temporary challenge directory. -# Otherwise, bind(2) a socket, pass its fileno to the webserver -# component, and optionally install iptables rules. -# -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 getgrnam($username): $!"; -        chown($uid, -1, $tmpdir) or die "Can't chown: $!"; -    } - -    my $conf = $CONFIG->{webserver}; -    my ($fam, $addr, $port) = (PF_INET, $conf->{listen}, 80); -    $port = $1 if $addr =~ s/:(\d+)$//; -    $addr = Socket::inet_ntop(PF_INET, INADDR_ANY) if $addr eq ''; -    $fam = PF_INET6 if $addr =~ s/^\[(.+)\]$/$1/; - -    my $proto = getprotobyname("tcp") // die; -    socket(my $srv, $fam, SOCK_STREAM, $proto) or die "socket: $!"; -    setsockopt($srv, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) or die "setsockopt: $!"; -    $addr = Socket::inet_pton($fam, $addr) // die "Invalid address $conf->{listen}\n"; -    my $sockaddr = $fam == PF_INET  ? Socket::pack_sockaddr_in($port,  $addr) -                 : $fam == PF_INET6 ? Socket::pack_sockaddr_in6($port, $addr) -                 : die; - -    # try to bind aginst the specified address:port -    bind($srv, $sockaddr) or do { -        die "Can't bind to $conf->{listen}: $!" if $! != EADDRINUSE; -        print STDERR "[$$] Using existing webserver on $conf->{listen}\n" if $OPTS{debug}; -        my $dir = $conf->{'challenge-directory'}; -        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 $tmpdir; -    }; -    listen($srv, 5) or die "listen: $!"; - -    # spawn the webserver component -    my $pid = fork() // "fork: $!"; -    unless ($pid) { -        drop_privileges($conf->{user}, $conf->{group}, $tmpdir); -        set_FD_CLOEXEC($srv, 0); -        $ENV{DEBUG} = $OPTS{debug}; -        # 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($srv) or die; -    } - -    print STDERR "[$$] Forking ACME webserver, child PID $pid\n" if $OPTS{debug}; -    set_FD_CLOEXEC($srv, 1); -    push @CLEANUP, sub() { -        print STDERR "[$$] Shutting down ACME webserver\n" if $OPTS{debug}; -        shutdown($srv, SHUT_RDWR) or warn "shutdown: $!"; -        kill 15 => $pid; -        waitpid $pid => 0; -    }; - -    return $tmpdir if lc ($conf->{iptables} // 'Yes') eq 'no'; - -    # install iptables -    my $iptables_bin = $fam == PF_INET ? 'iptables' : $fam == PF_INET6 ? 'ip6tables' : die; -    my $iptables_tmp = File::Temp::->new(TMPDIR => 1) // die; -    set_FD_CLOEXEC($iptables_tmp, 1); - -    my $pid2 = fork() // die "fork: $!"; -    unless ($pid2) { -        open STDIN,  '<',  '/dev/null'   or die "Can't open /dev/null: $!"; -        open STDOUT, '>&', $iptables_tmp or die "Can't dup: $!"; -        $| = 1; # turn off buffering for STDOUT -        exec "/sbin/$iptables_bin-save", "-c" or die; -    } -    waitpid $pid2 => 0; -    die "Error: /sbin/$iptables_bin-save exited with value ".($? >> 8) if $? > 0; - -    # seek back to the begining, as we'll restore directly from the -    # 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: $!"; - -    push @CLEANUP, sub() { -        print STDERR "[$$] Restoring iptables\n" if $OPTS{debug}; -        my $pid2 = fork() // die "fork: $!"; -        unless ($pid2) { -            open STDIN, '<&', $iptables_tmp or die "Can't dup: $!"; -            open STDOUT, '>', '/dev/null'   or die "Can't open /dev/null: $!"; -            exec "/sbin/$iptables_bin-restore", "-c" or die; -        } -        waitpid $pid2 => 0; -        warn "Warning: /sbin/$iptables_bin-restore exited with value ".($? >> 8) if $? > 0; -    }; - -    # it's safe to install the new iptables to open $port now that the -    # restore hook is in place -    my $mask = $fam == PF_INET  ? ($addr eq INADDR_ANY  ? '0' : '32') -             : $fam == PF_INET6 ? ($addr eq IN6ADDR_ANY ? '0' : '128') -             : die; -    my $dest = Socket::inet_ntop($fam, $addr) .'/'. $mask; - -    system ("/sbin/$iptables_bin", qw/-I INPUT  -p tcp -m tcp -m state/, -            '-d', $dest, '--dport', $port, -            '--state', 'NEW,ESTABLISHED', '-j', 'ACCEPT') == 0 or die; -    system ("/sbin/$iptables_bin", qw/-I OUTPUT -p tcp -m tcp -m state/, -            '-s', $dest, '--sport', $port, -            '--state',     'ESTABLISHED', '-j', 'ACCEPT') == 0 or die; - -    return $tmpdir; -} - - -############################################################################# -# Spawn the client component, and wait for it to return. -# If $args->{in} is defined, the data is written to the client's STDIN. -# If $args->{out} is defined, its value is set to client's STDOUT data. -# -sub acme_client($@) { -    my $args = shift; -    my @args = @_; - -    my @stat; -    my $conf = $CONFIG->{client}; -    my $sockname = $OPTS{socket} // $conf->{socket} // die "Missing socket option\n"; -    $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 "Can't 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 letsencrypt-accountd running?)\n"; -    die "Error: insecure permissions on $sockname\n" if ($stat[2] & 0066) != 0; - -    # connect(2) to the socket -    socket(my $client, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; -    my $sockaddr = Socket::sockaddr_un($sockname) // die "Invalid address $sockname\n"; -    until (connect($client, $sockaddr)) { -        next if $! == EINTR; # try again if connect(2) was interrupted by a signal -        die "connect: $!"; -    } - -    # use execve(2) rather than a Perl pseudo-process to ensure that the -    # child doesn't have access to the parent's memory -    my @fileno = map { fileno($_) =~ /^(\d+)$/ ? $1 : die } ($CONFFILE, $client); # untaint fileno -    spawn({%$args{qw/in 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: $!"; -        $ENV{DEBUG} = $OPTS{debug}; -    }}, $conf->{command}, $COMMAND, @fileno, @args); -} - -sub spawn($@) { -    my $args = shift; -    my @exec = @_; - -    # create communication pipes if needed -    my ($in_rd, $in_wd, $out_rd, $out_wd); -    if (defined $args->{in}) { -        pipe $in_rd, $in_wd or die "pipe: $!"; -    } -    if (defined $args->{out}) { -        pipe $out_rd, $out_wd or die "pipe: $!"; -    } - -    my $pid = fork() // "fork: $!"; -    unless ($pid) { -        # 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: $!"; -        } else { -            open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!"; -        } -        if (defined $args->{out}) { -            close $out_rd or die "Can't close: $!"; -            open STDOUT, '>&', $out_wd or die "Can't dup: $!"; -        } else { -            open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!"; -        } -        exec { $exec[0] } @exec or die; -    } -    push @CLEANUP, sub() { -        kill 15 => $pid; -        waitpid $pid => 0; -    }; - -    # 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_wd->print($args->{in}); -        $in_wd->close() or die "Can't close: $!"; -    } -    if (defined $args->{out}) { -        $out_wd->close() or die "Can't close: $!"; -        ${$args->{out}} = do { local $/ = undef; $out_rd->getline() }; -        $out_rd->close() or die "Can't close: $!"; -    } -    waitpid $pid => 0; -    pop @CLEANUP; -    undef ${$args->{out}} if defined $args->{out} and $? > 0; -    return $? > 255 ? ($? >> 8) : $? > 0 ? 1 : 0; -} - - -############################################################################# -# Install the certificate -# -sub install_cert($$@) { -    my $filename = shift; -    my $x509 = shift; - -    open my $fh, '>', $filename or die "Can't open $filename: $!"; -    print $fh $x509; -    foreach (@_) { # append the chain -        open my $fh2, '<', $_ or die "Can't open $_: $!"; -        my $ca = do { local $/ = undef; $fh2->getline() }; -        print $fh $ca; -        close $fh2 or die "Can't close: $!"; -    } -    close $fh or die "Can't close: $!"; -} - - -############################################################################# -# new-reg [--agreement-uri=URI] [CONTACT ..] -# reg=URI [--agreement-uri=URI] [CONTACT ..] -# -if ($COMMAND eq 'new-reg' or $COMMAND =~ /^reg=/) { -    die "Invalid registration URI (use the 'new-reg' command to determine the URI)\n" -        if $COMMAND eq 'reg='; -    $OPTS{'agreement-uri'} = $OPTS{'agreement-uri'} =~ /\A(\p{Print}+)\z/ ? $1 -                           : die "Invalid value for --agreement-uri\n" -        if defined $OPTS{'agreement-uri'}; - -    unshift @ARGV, ($OPTS{'agreement-uri'} // ''); -    exit acme_client({}, @ARGV); -} - - -############################################################################# -# new-cert [SECTION ..] -#   TODO: renewal without the account key, see -#         https://github.com/letsencrypt/acme-spec/pull/168 -# -elsif ($COMMAND eq 'new-cert') { -    my $conf; -    do { -        my $conffile = $OPTS{'config-certs'} // $CONFIG->{_}->{'config-certs'} // first { -f $_ } -            ( "./$NAME-certs.conf" -            , ($ENV{XDG_CONFIG_HOME} // "$ENV{HOME}/.config")."/letsencrypt-tiny/$NAME-certs.conf" -            , "/etc/letsencrypt-tiny/$NAME-certs.conf" -            ); -        die "Error: Can't find certificate configuration file\n" unless defined $conffile; -        my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n"; -        my $defaults = delete $h->{_} // {}; -        my @valid = qw/certificate certificate-chain certificate-key min-days CAfile -                       hash keyUsage subject subjectAltName 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}}; -            $conf->{$s}->{$_} //= $defaults->{$_} foreach keys %$defaults; -        } -    }; - -    my $challenge_dir; -    my $rv = 0; -    foreach my $s (@ARGV ? @ARGV : keys %$conf) { -        my $conf = $conf->{$s} // do { -            print STDERR "Warning: No such section $s, skipping\n"; -            $rv = 1; -            next; -        }; - -        my $certtype = first { defined $conf->{$_} } qw/certificate certificate-chain/; -        unless (defined $certtype) { -            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}))) { -            my $d = $conf->{'min-days'} // 10; -            if ($d > 0 and $t - time > $d*86400) { -                my $d = POSIX::strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($t)); -                print STDERR "[$s] Valid until $d, skipping\n"; -                next; -            } -        } - -        # generate the CSR -        my $csr = gen_csr(%$conf{qw/certificate-key subject subjectAltName keyUsage hash/}) // do { -            print STDERR "[$s] Warning: Couldn't generate CSR, skipping\n"; -            $rv = 1; -            next; -        }; - -        # spawn the webserver if not done already -        $challenge_dir //= spawn_webserver(); - -        # list all authorization domains to request -        my @authz; -        push @authz, $1 if defined $conf->{subject} =~ /\A.*\/CN=($RE_DOMAIN)\z/o; -        if (defined $conf->{subjectAltName}) { -            foreach my $d (split /,/, $conf->{subjectAltName}) { -                next unless $d =~ s/\A\s*DNS://; -                if ($d =~ /\A$RE_DOMAIN\z/o) { -                    push @authz, $d unless grep {$_ eq $d} @authz; -                } else { -                    print STDERR "[$s] Warning: Ignoring invalid domain $d\n"; -                } -            } -        } - -        my ($x509, $csr_pubkey, $x509_pubkey); -        print STDERR "[$s] Will request authorization for: ".join(", ", @authz), "\n" if $OPTS{debug}; -        if (acme_client({chdir => $challenge_dir, in => $csr, out => \$x509}, @authz)) { -            print STDERR "[$s] Error: Couldn't issue X.509 certificate!\n"; -            $rv = 1; -            next; -        } - -        # extract pubkeys from CSR and cert, and ensure they match -        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) { -            print STDERR "[$s] Error: Received bogus X.509 certificate from ACME server!\n"; -            $rv = 1; -            next; -        }; - -        # verify certificate validity against the CA -        $conf->{CAfile} //= '/usr/share/letsencrypt-tiny/lets-encrypt-x3-cross-signed.pem'; -        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; -        } - -        # install certificate -        if (defined $conf->{'certificate'}) { -            print STDERR "Installing X.509 certificate $conf->{'certificate'}\n"; -            install_cert($conf->{'certificate'}, $x509); -        } -        if (defined $conf->{'certificate-chain'}) { -            print STDERR "Installing X.509 certificate chain $conf->{'certificate-chain'}\n"; -            install_cert($conf->{'certificate-chain'}, $x509, $conf->{CAfile}); -        } - -        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; -            foreach (grep defined, @$conf{qw/certificate certificate-chain/}) { -                chown($uid, $gid, $_) or die "Can't 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: $!"; -            } -        } - -        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; -        close $fh or die $! ? -            "Can't close: $!" : -            "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; - -        if (defined $conf->{notify}) { -            print STDERR "Running notification command `$conf->{notify}`\n"; -            if (system($conf->{notify}) != 0) { -                print STDERR "Warning: notification command exited with value ".($? >> 8)."\n"; -                $rv = 1; -            } -        } -    } -    undef $challenge_dir; -    exit $rv; -} - - -############################################################################# -# revoke-cert FILE [FILE ..] -# -elsif ($COMMAND eq 'revoke-cert') { -    die "Nothing to revoke\n" unless @ARGV; -    my $rv = 0; -    foreach my $filename (@ARGV) { -        print STDERR "Revoking $filename\n"; - -        # conversion PEM -> DER -        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: $!" : -            "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; - -        my @certopts = join ',', qw/no_header no_version no_pubkey no_sigdump no_extensions/; -        open my $fh2, '|-', qw/openssl x509 -inform DER -noout -fingerprint -sha256 -text -certopt/, @certopts -            or die "fork: $!"; -        print $fh2 $der; -        close $fh2 or die $! ? -            "Can't close: $!" : -            "Error: x509(1ssl) exited with value ".($? >> 8)."\n"; - -        if (acme_client({in => $der})) { -            print STDERR "Warning: Couldn't revoke $filename\n"; -            $rv = 1; -        } -    } -    exit $rv; -} - - -############################################################################# -# -else { -    die "Unknown command $COMMAND" -} - - -END { -    local $?; -    $_->() foreach reverse @CLEANUP; -} | 
