diff options
| author | Guilhem Moulin <guilhem@fripost.org> | 2016-03-02 18:19:04 +0100 | 
|---|---|---|
| committer | Guilhem Moulin <guilhem@fripost.org> | 2016-03-02 18:19:04 +0100 | 
| commit | 97369c2e6dce66881d673ba308acd069c08a8776 (patch) | |
| tree | 3946b0216071d0b2e47b805ac2a19bef685a6c38 /letsencrypt-accountd | |
| parent | c4db1e4a18a13f7db04dbbd10663f0edba7d206d (diff) | |
| parent | 63633fd91bcc97217f2ac45ba602d752e8fbaafd (diff) | |
Merge branch 'master' into debian
Diffstat (limited to 'letsencrypt-accountd')
| -rwxr-xr-x | letsencrypt-accountd | 202 | 
1 files changed, 202 insertions, 0 deletions
| diff --git a/letsencrypt-accountd b/letsencrypt-accountd new file mode 100755 index 0000000..ffc5619 --- /dev/null +++ b/letsencrypt-accountd @@ -0,0 +1,202 @@ +#!/usr/bin/perl -T + +#---------------------------------------------------------------------- +# Let's Encrypt ACME client (account key manager) +# 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 $PROTOCOL_VERSION = 1; +my $NAME = 'letsencrypt-accountd'; + +use Errno 'EINTR'; +use Getopt::Long qw/:config posix_default no_ignore_case gnu_getopt auto_version/; +use List::Util 'first'; +use MIME::Base64 'encode_base64url'; +use Socket qw/PF_UNIX SOCK_STREAM SHUT_RDWR/; + +use Config::Tiny (); +use JSON (); + +# Clean up PATH +$ENV{PATH} = join ':', qw{/usr/bin /bin}; +delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; + +my ($SOCKNAME, $S, %OPTS); +$SIG{$_} = sub() { exit } 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] [--privkey=ARG] [--socket=PATH] [--quiet]\n" +                    ."Consult the manpage for more information.\n"; +    } +    exit $rv; +} +usage(1) unless GetOptions(\%OPTS, qw/config=s privkey=s socket=s quiet|q debug help|h/); +usage(0) if $OPTS{help}; + +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}; + +    my $h = Config::Tiny::->read($conffile) or die Config::Tiny::->errstr()."\n"; +    my $h2 = delete $h->{_} // {}; +    die "Invalid section(s): ".join(', ', keys %$h)."\n" if %$h; +    my %h = map { $_ => delete $h2->{$_} } qw/privkey gpg socket quiet/; +    die "Unknown option(s): ".join(', ', keys %$h2)."\n" if %$h2; +    $h{quiet} = lc $h{quiet} eq 'yes' ? 1 : 0 if defined $h{quiet}; +    $OPTS{$_} //= $h{$_} foreach grep {defined $h{$_}} keys %h; + +    $OPTS{quiet} = 0 if $OPTS{debug}; +    die "Error: 'privkey' is not specified\n" unless defined $OPTS{privkey}; +}; + + +############################################################################# +# Build the JSON Web Key (RFC 7517) from the account key's public parameters, +# and determine the signing method $SIGN. +# +my ($JWK, $SIGN); +if ($OPTS{privkey} =~ /\A(file|gpg):(\p{Print}+)\z/) { +    my ($method, $filename) = ($1,$2); +    my ($fh, @command); +    if ($method eq 'file') { +        # generate with `openssl genrsa 4096 | install --mode=0600 /dev/stdin /tmp/privkey` +        open $fh, '<', $filename or die "Error: Can't open $filename: $!\n"; +    } +    elsif ($method eq 'gpg') { +        @command = split /\s+/, ($OPTS{gpg} // 'gpg --quiet'); +        open $fh, '-|', @command, qw/-o - --decrypt --/, $filename or die "fork: $!"; +    } +    else { +        die; # impossible +    } + +    my $str = do {local $/ = undef; <$fh>}; +    close $fh or die $! ? +        "Can't close: $!" : +        "Error: $command[0] exited with value ".($? >> 8)."\n"; + +    require 'Crypt/OpenSSL/RSA.pm'; +    my $rsa = Crypt::OpenSSL::RSA->new_private_key($str); +    undef $str; + +    die "Error: $filename: Not a private key\n" unless $rsa->is_private(); +    die "Error: $filename: Invalid key\n"       unless $rsa->check_key(); +    $rsa->use_sha256_hash(); + +    require 'Crypt/OpenSSL/Bignum.pm'; +    my ($n, $e) = $rsa->get_key_parameters(); # don't include private params! +    $_ = encode_base64url($_->to_bin()) foreach ($n, $e); + +    $JWK = { kty => 'RSA', n => $n, e => $e }; +    $SIGN = sub($) { $rsa->sign($_[0]) }; +} +else { +    die "Error: unsupported method: $OPTS{privkey}\n"; +} +$JWK = JSON::->new->encode($JWK); + + +############################################################################# +# Create the server UNIX socket and bind(2) against it. +# NOTE: We don't use the abstract namespace so we can rely on the file +# permissions to keep other users out.  (Also, OpenSSH 7.1 doesn't seem +# to support the abstract namespace.)  The downside is that we have to +# delete the file manually. +# +do { +    my $sockname = $OPTS{socket} // (defined $ENV{XDG_RUNTIME_DIR} ? "$ENV{XDG_RUNTIME_DIR}/S.letsencrypt" : undef); +    die "Missing socket option\n" unless defined $sockname; +    $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; +    my @stat = stat($dirname) or die "Can't stat $dirname: $!"; +    die "Error: insecure permissions on $dirname\n" if ($stat[2] & 0022) != 0; + +    my $umask = umask(0177) // die "umask: $!"; + +    print STDERR "Starting Let's Encrypt Account Key Manager at $sockname\n" unless $OPTS{quiet}; +    socket(my $sock, PF_UNIX, SOCK_STREAM, 0) or die "socket: $!"; +    my $sockaddr = Socket::sockaddr_un($sockname) // die; +    bind($sock, $sockaddr) or die "bind: $!"; + +    ($SOCKNAME, $S) = ($sockname, $sock); +    listen($S, 1) or die "listen: $!"; + +    umask($umask) // die "umask: $!"; +}; + + +############################################################################# +# For each new connection, send the protocol version and the account key's +# public parameters, then sign whatever comes in +# +$SIG{PIPE} = 'IGNORE'; # ignore broken pipes +for (my $count = 0;; $count++) { +    accept(my $conn, $S) or do { +        next if $! == EINTR; # try again if accept(2) was interrupted by a signal +        die "accept: $!"; +    }; +    print STDERR "[$count]>> Accepted new connection\n" unless $OPTS{quiet}; + +    $conn->printflush( "$PROTOCOL_VERSION OK", "\r\n", $JWK, "\r\n" ); + +    # sign whatever comes in +    while (defined (my $data = $conn->getline())) { +        $data =~ s/\r\n\z// or die; +        print STDERR "[$count]>> Issuing SHA-256 signature for: $data\n" unless $OPTS{quiet}; +        my $sig = $SIGN->($data); +        $conn->printflush( encode_base64url($sig), "\r\n" ); +    } + +    print STDERR "[$count]>> Connection terminated\n" unless $OPTS{quiet}; +    close $conn or warn "Can't close: $!"; +} + + +############################################################################# +# +END { +    if (defined $SOCKNAME and -S $SOCKNAME) { +        print STDERR "Unlinking $SOCKNAME\n" if $OPTS{debug}; +        unlink $SOCKNAME or print STDERR "Can't unlink $SOCKNAME: $!\n"; +    } +    if (defined $S) { +        print STDERR "Shutting down and closing Let's Encrypt Account Key Manager\n" unless $OPTS{quiet}; +        shutdown($S, SHUT_RDWR) or warn "shutdown: $!"; +        close $S or print STDERR "Can't close: $!\n"; +    } +} | 
