diff options
| author | Guilhem Moulin <guilhem@fripost.org> | 2015-12-18 01:40:56 +0100 | 
|---|---|---|
| committer | Guilhem Moulin <guilhem@fripost.org> | 2015-12-18 01:41:58 +0100 | 
| commit | 29b96a13f83fcb95dac1d320cce071790fb98e0c (patch) | |
| tree | da338012d0b73854f00e54668d79b61b7fd61629 /acme-slave | |
| parent | 970ffbb595b6c07b3c03730c54de53189e3368b7 (diff) | |
acme-slave → client; acme-webserver → webserver
Diffstat (limited to 'acme-slave')
| -rwxr-xr-x | acme-slave | 269 | 
1 files changed, 0 insertions, 269 deletions
diff --git a/acme-slave b/acme-slave deleted file mode 100755 index 5cf0139..0000000 --- a/acme-slave +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/perl -T - -#---------------------------------------------------------------------- -# Let's Encrypt ACME client -# Copyright © 2015 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; - -use LWP::UserAgent (); -use Crypt::OpenSSL::RSA (); -use Crypt::OpenSSL::Bignum (); -use MIME::Base64 qw/encode_base64 encode_base64url/; -use JSON (); -use Digest::SHA qw/sha256 sha256_hex/; - -# Clean up PATH -$ENV{PATH} = join ':', qw{/usr/bin /bin}; -delete @ENV{qw/IFS CDPATH ENV BASH_ENV/}; - -my $COMMAND = shift @ARGV // die; -my $PUBKEY = shift @ARGV // die; -die unless grep {$COMMAND eq $_} qw/new-reg new-cert revoke-cert/; -my $TIMEOUT = 10; - - -# Read the public key and build the JSON Web Key (RFC 7517) -my $JWK = do { -    open my $fh, '<', $PUBKEY or die "Can't open $PUBKEY: $!"; -    my $str = do { local $/ = undef; <$fh> }; -    my $pubkey = Crypt::OpenSSL::RSA->new_public_key($str) or die; -    close $fh; - -    my ($n, $e) = $pubkey->get_key_parameters(); -    $_ = encode_base64url($_->to_bin()) foreach ($n, $e); - -    { kty => 'RSA', n => $n, e => $e } -}; -my $JSON = JSON::->new->utf8->canonical(); # breaks hashes otherwise -my $JWK_dgst64 = encode_base64url(sha256($JSON->encode($JWK))); -my $NONCE; - - -# Send an HTTP request to the ACME server -my $UA = LWP::UserAgent::->new( ssl_opts => { -    verify_hostname => 1, -    SSL_version => 'SSLv23:!TLSv1_1:!TLSv1:!SSLv3:!SSLv2', -    SSL_cipher_list => 'EECDH+AESGCM:!MEDIUM:!LOW:!EXP:!aNULL:!eNULL' -}); -sub request($$;$) { -    my ($method, $uri, $json) = @_; -    print STDERR ">>> $method $uri <<<\n" if $ENV{DEBUG}; - -    my $req = HTTP::Request::->new($method => $uri) or die "Can't $method $uri"; -    if (defined $json) { -        $req->header('Content-Type' => 'application/json'); -        $req->content($JSON->encode($json)); -    } -    my $r = $UA->request($req) or die "Can't $method $uri"; -    print STDERR ">>> ", $r->status_line, "\n", $r->headers->as_string, "\n" if $ENV{DEBUG}; -    $NONCE = $r->header('Replay-Nonce') // die; -    my $t = $r->header('Content-Type'); - -    my $content = $r->decoded_content(); -    if (defined $t and $t =~ /\Aapplication\/(?:[a-z]+\+)?json\z/) { -        $content = $JSON->decode($content); -        print STDERR $JSON->pretty->encode($content), "\n" if $ENV{DEBUG}; -    } -    elsif (defined $t and $t eq 'application/pkix-cert') { -        print STDERR encode_base64($content), "\n" if $ENV{DEBUG}; -    } -    else { -        print STDERR $content, "\n" if $ENV{DEBUG}; -    } -    unless ($r->is_success) { -        my $msg = $r->status_line; -        $msg .= " (".$content->{detail}.")" if ref $content and defined $content->{detail}; -        die $msg, "\n"; -    } - -    return $content; -} - - -# ACME client -# https://tools.ietf.org/html/draft-ietf-acme-acme-01 -sub acme($$) { -    my ($uri, $h) = @_; - -    # Produce the JSON Web Signature: RFC 7515 section 5 -    my $payload = encode_base64url($JSON->encode($h)); -    my %header = ( alg => 'RS256', jwk => $JWK ); -    my $protected = encode_base64url($JSON->encode({ %header, nonce => $NONCE })); -    my $data = $protected .'.'. $payload; -    print STDERR "Requesting a SHA-256 signature for ", $data, "\n" if $ENV{DEBUG}; -    STDOUT->printflush($data, "\n"); - -    # Ask for an (hex) sig -    my $sig = do { local $_ = <STDIN>; chomp; $_ }; -    $sig = encode_base64url(pack('H*', $sig)); -    print STDERR "Got SHA-256 signature ", $sig, "\n" if $ENV{DEBUG}; - -    # Flattened JSON Serialization, RFC 7515 section 7.2.2 -    request(POST => $uri, { -        payload => $payload, -        protected => $protected, -        header => \%header, -        signature => $sig -    }); -} - - -# Query the root ACME directory to initialize the nonce and get the resources URIs -my %RES = %{ request(GET => "https://acme-v01.api.letsencrypt.org/directory") }; - - -if ($COMMAND eq 'new-reg') { -    print STDERR "Requesting new registration ".(@ARGV ? ("for ".join(', ', @ARGV)) : "")."\n"; -    my $uri = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"; -    my $dgst = sha256_hex($UA->get($uri)->decoded_content()); -    die "Error: The CA's subscriber agreement (URL $uri) has changed!\n" if -        $dgst ne '33d233c8ab558ba6c8ebc370a509acdded8b80e5d587aa5d192193f35226540f'; - -    acme($RES{'new-reg'}, { -        resource => 'new-reg', -        contact => [ map {"mailto:$_"} @ARGV ], -        agreement => $uri, -    }); -    exit; -} - - -if ($COMMAND eq 'revoke-cert') { -    print STDERR "Requesting revocation for\n"; -    for my $cert (@ARGV) { -        open my $fh1, '-|', qw/openssl x509 -noout -subject -serial -fingerprint -sha256/, '-in', $cert -            or die "Can't run x509(1ssl): $!"; -        my ($subject, $serial, $fingerprint) = map { s/[^=]+=\s*//; chomp; $_ } <$fh1>; -        close $fh1; - -        print STDERR "\n\tSubject:             $subject\n", -                       "\tSerial:              $serial\n", -                       "\tSHA-256 fingerprint: $fingerprint\n"; - -        open my $fh2, '-|', qw/openssl x509 -outform DER/, '-in', $cert or die "Can't run x509(1ssl): $!"; -        my $der = do { local $/ = undef; <$fh2> }; -        close $fh2; - -        acme($RES{'revoke-cert'}, { -            resource => 'revoke-cert', -            certificate => encode_base64url($der) -        }); -    } -    exit; -} - - -# $COMMAND eq 'new-cert' -my ($CSR, $CHALLENGE_DIR, $X509) = @ARGV; -$CHALLENGE_DIR = $CHALLENGE_DIR =~ /\A(\/\p{Print}+)\z/ ? $1 : -    die "Error: Challenge directory is not absolute: $CHALLENGE_DIR"; - -# Parse the Certificate Signing Request -# XXX use a library instead, perhaps Crypt::OpenSSL::PKCS10 -my @domains = do { -    my @req = (qw/openssl req -noout/, '-in', $CSR); - -    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)+/; -    my %domains; - -    open my $fh1, '-|', @req, '-subject' or die "Can't run req(1ssl): $!"; -    my $subject = <$fh1>; -    close $fh1; -    $domains{$1} = 1 if $subject =~ /\Asubject=.*\/CN=($RE_domain)\n\z/o; - -    open my $fh2, '-|', @req, '-text', '-reqopt', 'no_header,no_version,no_subject,no_pubkey,no_sigdump' -        or die "Can't run req(1ssl): $!"; -    while (<$fh2>) { -        /\A\s+X509v3 Subject Alternative Name:/ or next; -        my $san = <$fh2>; -        foreach (split /,/, $san) { -            chomp; -            s/\A\s*//; -            next unless s/\ADNS://; -            if (/\A$RE_domain\z/o) { -                $domains{$_} = 1; -            } -            else { -                warn "WARNING: Ignoring invalid domain $_\n"; -            } -        } -        last; -    } -    close $fh2; - -    keys %domains; -}; -print STDERR "Found domain(s): ".join(", ", @domains), "\n" if $ENV{DEBUG}; - - -# Process DNS Authorizations -foreach my $domain (@domains) { -    print STDERR "Processing new DNS authz for $domain\n" if $ENV{DEBUG}; -    my $challenges = acme($RES{'new-authz'}, { -        resource => 'new-authz', -        identifier => { type => 'dns', value => $domain } -    }); -    die "No challenge in server response" unless defined $challenges->{challenges}; -    my ($challenge) = grep {$_->{type} eq 'http-01'} @{$challenges->{challenges}}; -    my $keyAuthorization = $challenge->{token}.'.'.$JWK_dgst64; - -    # serve $keyAuthorization at http://$domain/.well-known/acme-challenge/$challenge->{token} -    my $filename = $CHALLENGE_DIR.'/'.$challenge->{token}; -    if (-e $filename) { -        warn "WARNING: File exists: $filename\n"; -    } -    else { -        open my $fh, '>', $filename or die "Can't open $filename: $!"; -        print $fh $keyAuthorization; -        close $fh; -    } - -    acme($challenge->{uri}, { -        resource => 'challenge', -        keyAuthorization => $keyAuthorization -    }); - -    for (my $i=0;; $i++) { -        my $status = request('GET' => $challenge->{uri})->{status} // 'pending'; -        die "Error: Invalid challenge for $domain\n" if $status eq 'invalid'; -        last if $status eq 'valid'; -        die "Timeout exceeded while waiting for challenge to pass ($domain)\n" if $i >= $TIMEOUT; -        sleep 1; -    } -} - - -do { -    print STDERR "Processing new CSR\n" if $ENV{DEBUG}; -    open my $fh1, '-|', qw/openssl req -outform DER/, '-in', $CSR or die "Can't run req(1ssl): $!"; -    my $req = do { local $/ = undef; <$fh1> }; -    close $fh1; - -    # The server also gives the cert URI in its 'Location' header in -    # https://acme-v01.api.letsencrypt.org/acme/cert/$serial -    my $x509 = acme($RES{'new-cert'}, { -        resource => 'new-cert', -        csr => encode_base64url($req) -    }); - -    open my $fh2, '|-', qw/openssl x509 -inform DER/, '-out', $X509 or die "Can't run x509(1ssl): $!"; -    print $fh2 $x509; -    close $fh2; -};  | 
