aboutsummaryrefslogtreecommitdiffstats
path: root/acme-slave
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2015-12-18 01:40:56 +0100
committerGuilhem Moulin <guilhem@fripost.org>2015-12-18 01:41:58 +0100
commit29b96a13f83fcb95dac1d320cce071790fb98e0c (patch)
treeda338012d0b73854f00e54668d79b61b7fd61629 /acme-slave
parent970ffbb595b6c07b3c03730c54de53189e3368b7 (diff)
acme-slave → client; acme-webserver → webserver
Diffstat (limited to 'acme-slave')
-rwxr-xr-xacme-slave269
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;
-};