aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--Changelog17
-rw-r--r--INSTALL10
-rwxr-xr-xclient115
-rwxr-xr-xlacme44
-rw-r--r--lacme-accountd.md2
-rw-r--r--lacme.md2
7 files changed, 112 insertions, 83 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..813d896
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# vim swapfiles
+.*.sw[po]
+
+# generated man-pages
+*.1
diff --git a/Changelog b/Changelog
index 633222a..c7cc0b3 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,20 @@
+lacme (0.6) upstream;
+
+ + client: poll order URL instead of each authz URL successively.
+ + lacme: new option 'account --deactivate' for client-initiated account
+ deactivation, see RFC 8555 sec. 7.3.6.
+ - lacme, client: new dependency Date::Parse, don't parse RFC 3339
+ datetime strings from X.509 certs manually.
+ - lacme: assume that the iptables(1) binaries are under /usr/sbin not
+ /sbin. As of Buster this is the case, and the maintainer plans to
+ drop compatibility symlinks once Bullseye is released.
+ - Link to RFC 8555 <https://tools.ietf.org/html/rfc8555> instead of the
+ ACME I-D URL.
+ - Issue GET and POST-as-GET requests (RFC 8555 sec. 6.3) for the
+ authorizations, order and certificate URLs.
+
+ -- Guilhem Moulin <guilhem@fripost.org> Wed, 21 Aug 2019 18:23:50 +0200
+
lacme (0.5) upstream;
+ Use ACME v2 endpoints (update protocol to the last draft of the spec
diff --git a/INSTALL b/INSTALL
index 82de279..155c7aa 100644
--- a/INSTALL
+++ b/INSTALL
@@ -20,6 +20,7 @@ lacme depends on OpenSSL and the following Perl modules:
- Config::Tiny
- Digest::SHA (core module)
+ - Date::Parse
- Errno (core module)
- Fcntl (core module)
- File::Temp (core module)
@@ -37,7 +38,14 @@ lacme depends on OpenSSL and the following Perl modules:
On Debian GNU/Linux systems, these dependencies can be installed with
the following command:
- apt-get install openssl libconfig-tiny-perl libjson-perl libwww-perl liblwp-protocol-https-perl libnet-ssleay-perl libtypes-serialiser-perl
+ apt-get install openssl \
+ libconfig-tiny-perl \
+ libtimedate-perl \
+ libjson-perl \
+ libwww-perl \
+ liblwp-protocol-https-perl \
+ libnet-ssleay-perl \
+ libtypes-serialiser-perl
However Debian GNU/Linux users can also use gbp(1) from git-buildpackage
to build their own package:
diff --git a/client b/client
index 838b184..2eebbf0 100755
--- a/client
+++ b/client
@@ -50,6 +50,7 @@ use Fcntl qw/O_CREAT O_EXCL O_WRONLY/;
use Digest::SHA qw/sha256 sha256_hex/;
use MIME::Base64 qw/encode_base64 encode_base64url/;
+use Date::Parse ();
use LWP::UserAgent ();
use Types::Serialiser ();
use JSON ();
@@ -142,6 +143,21 @@ sub request_status_line($) {
return $msg;
}
+# The request's Retry-After header (RFC 7231 sec. 7.1.3), converted to
+# waiting time in seconds.
+sub request_retry_after($) {
+ my $r = shift;
+ my $v = $r->header('Retry-After');
+ if (defined $v and $v !~ /\A\d+\z/) {
+ $v = Date::Parse::str2time($v);
+ if (defined $v) {
+ $v = $v - time;
+ undef $v if $v <= 0;
+ }
+ }
+ return $v;
+}
+
# Parse and return the request's decoded content as JSON; or print the
# Status Line and fail if the request failed.
# If $dump is set, also pretty-print the decoded content.
@@ -165,16 +181,16 @@ sub request_json_decode($;$$) {
#############################################################################
# JSON-encode the hash reference $h and send it to the ACME server $uri
# encapsulated it in a JSON Web Signature (JWS).
-# https://tools.ietf.org/html/draft-ietf-acme-acme-12
+# https://tools.ietf.org/html/rfc8555
#
-sub acme($@) {
- my $uri = shift;
+sub acme($;$) {
+ my ($uri, $h) = @_;
die "Missing nonce\n" unless defined $NONCE;
# Produce the JSON Web Signature: RFC 7515 section 5
my %header = ( alg => 'RS256', nonce => $NONCE, url => $uri );
defined $KID ? ($header{kid} = $KID) : ($header{jwk} = $JWK);
- my $payload = encode_base64url(json()->encode({ @_ }));
+ my $payload = defined $h ? encode_base64url(json()->encode($h)) : "";
my $protected = encode_base64url(json()->encode(\%header));
my $data = $protected .'.'. $payload;
$S->printflush($data, "\r\n");
@@ -204,7 +220,7 @@ sub acme_resource($%) {
request(HEAD => $RES{newNonce});
}
my $uri = $RES{$r} // die "Unknown resource '$r'\n";
- acme($uri, @_);
+ acme($uri, {@_});
}
# Set the key ID (registration URI)
@@ -228,6 +244,7 @@ if ($COMMAND eq 'account') {
my %h = ( contact => \@ARGV ) if @ARGV;
$h{onlyReturnExisting} = Types::Serialiser::true unless $flags & 0x01;
$h{termsOfServiceAgreed} = Types::Serialiser::true if $flags & 0x02;
+ $h{status} = "deactivated" if $flags & 0x04;
print STDERR "Requesting new registration ".(@ARGV ? ("for ".join(', ', @ARGV)) : "")."\n"
if $flags & 0x01;
@@ -237,7 +254,7 @@ if ($COMMAND eq 'account') {
if ($r->is_success()) {
$KID = $r->header('Location');
- $r = acme($KID, %h);
+ $r = acme($KID, \%h);
request_json_decode($r, 1, \*STDOUT)
if $r->is_success() and $r->content_type() eq 'application/json';
}
@@ -262,9 +279,10 @@ elsif ($COMMAND eq 'newOrder') {
my @identifiers = map {{ type => 'dns', value => $_ }} @ARGV;
my $r = acme_resource('newOrder', identifiers => \@identifiers);
my $order = request_json_decode($r);
+ my $orderurl = $r->header('Location');
foreach (@{$order->{authorizations}}) {
- my $authz = request_json_decode(request(GET => $_));
+ my $authz = request_json_decode(acme($_));
next unless $authz->{status} eq 'pending';
my $identifier = $authz->{identifier}->{value};
@@ -287,52 +305,57 @@ elsif ($COMMAND eq 'newOrder') {
} else {
die "Can't open $challenge->{token}: $!";
}
-
- $r = acme($challenge->{url});
-
- # poll until the status become 'valid'
- # XXX poll the order URL instead, to get the status of all
- # challenges at once
- # https://github.com/letsencrypt/boulder/issues/3530
- for ( my $i = 0, my $resp, my $status;
- $resp = request_json_decode($r),
- $status = $resp->{status} // 'pending',
- $status ne 'valid';
- $r = request('GET' => $challenge->{url})) {
- if (defined (my $problem = $resp->{error})) { # problem document (RFC 7807)
- my $msg = $problem->{status};
- $msg .= " " .$problem->{title} if defined $problem->{title};
- $msg .= " (".$problem->{detail}.")" if defined $problem->{detail};
- die $msg, "\n";
- }
- die "Error: Invalid challenge for $identifier (status: ".$status.")\n"
- if $status ne 'pending';
-
- my $sleep = 1;
- if (defined (my $retry_after = $r->header('Retry-After'))) {
- print STDERR "Retrying after $retry_after seconds...\n";
- $sleep = $retry_after;
- }
-
- $i += $sleep;
- die "Timeout exceeded while waiting for challenges to pass ($identifier)\n"
- if $timeout > 0 and $i >= $timeout;
- sleep $sleep;
- }
+ my $r = acme($challenge->{url}, {});
+ request_json_decode($r);
}
- $r = acme($order->{finalize}, csr => encode_base64url($csr));
- my $resp = request_json_decode($r);
+ # poll the order URL (to get the status of all challenges at once)
+ # until the status become 'valid'
+ my $orderstr = join(', ', map {uc($_->{type}) .":". $_->{value}} @identifiers);
+ my $certuri;
+ for (my $i = 0;;) {
+ my $r = acme($orderurl);
+ my $resp = request_json_decode($r);
+ if (defined (my $problem = $resp->{error})) { # problem document (RFC 7807)
+ my $msg = $problem->{status};
+ $msg .= " " .$problem->{title} if defined $problem->{title};
+ $msg .= " (".$problem->{detail}.")" if defined $problem->{detail};
+ die $msg, "\n";
+ }
+ my $status = $resp->{status};
+ if (!defined $status or $status eq "invalid") {
+ die "Error: Invalid order $orderstr\n";
+ }
+ elsif ($status eq "ready") {
+ my $r = acme($order->{finalize}, {csr => encode_base64url($csr)});
+ my $resp = request_json_decode($r);
+ $certuri = $resp->{certificate};
+ last;
+ }
+ elsif ($status eq "valid") {
+ $certuri = $resp->{certificate} //
+ die "Error: Missing \"certificate\" field in \"valid\" order\n";
+ last;
+ }
+ elsif ($status ne "pending" and $status ne "processing") {
+ warn "Unknown order status: $status\n";
+ }
- my $uri = $resp->{certificate};
- print STDERR "Certificate URI: $uri\n";
+ my $retry_after = request_retry_after($r) // 1;
+ print STDERR "Retrying after $retry_after seconds...\n";
+ $i += $retry_after;
+ die "Timeout exceeded while waiting for challenges to pass ($orderstr)\n"
+ if $timeout > 0 and $i >= $timeout;
+ sleep $retry_after;
+ }
- # pool until the cert is available
+ # poll until the cert is available
+ print STDERR "Certificate URI: $certuri\n";
for (my $i = 0;;) {
- $r = request('GET' => $uri);
+ $r = acme($certuri);
die request_status_line($r), "\n" unless $r->is_success();
last unless $r->code == 202; # Accepted
- my $retry_after = $r->header('Retry-After') // 1;
+ my $retry_after = request_retry_after($r) // 1;
print STDERR "Retrying after $retry_after seconds...\n";
$i += $retry_after;
die "Timeout exceeded while waiting for certificate\n" if $timeout > 0 and $i >= $timeout;
diff --git a/lacme b/lacme
index 3e5347d..d5e8933 100755
--- a/lacme
+++ b/lacme
@@ -36,6 +36,7 @@ use Socket 1.95 qw/AF_UNIX AF_INET AF_INET6 PF_UNIX PF_INET PF_INET6 PF_UNSPEC
SOCK_STREAM SOL_SOCKET SO_REUSEADDR SHUT_RDWR/;
use Config::Tiny ();
+use Date::Parse ();
use Net::SSLeay ();
# Clean up PATH
@@ -62,7 +63,7 @@ sub usage(;$$) {
}
exit $rv;
}
-usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s register tos-agreed min-days=i quiet|q debug help|h/);
+usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s register tos-agreed deactivate min-days=i quiet|q debug help|h/);
usage(0) if $OPTS{help};
$COMMAND = shift(@ARGV) // usage(1, "Missing command");
@@ -199,33 +200,7 @@ sub x509_enddate($) {
$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);
- }
- }
+ my $t = Date::Parse::str2time($dt) if defined $dt;
Net::SSLeay::X509_free($x509) if defined $x509;
Net::SSLeay::BIO_free($bio) if defined $bio;
@@ -424,10 +399,10 @@ sub iptables_save($@) {
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;
+ exec "/usr/sbin/$iptables_bin-save", "-c" or die;
}
waitpid $pid => 0;
- die "Error: /sbin/$iptables_bin-save exited with value ".($? >> 8) if $? > 0;
+ die "Error: /usr/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
@@ -441,10 +416,10 @@ sub iptables_save($@) {
unless ($pid) {
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;
+ exec "/usr/sbin/$iptables_bin-restore", "-c" or die;
}
waitpid $pid => 0;
- warn "Warning: /sbin/$iptables_bin-restore exited with value ".($? >> 8) if $? > 0;
+ warn "Warning: /usr/sbin/$iptables_bin-restore exited with value ".($? >> 8) if $? > 0;
};
@@ -462,10 +437,10 @@ sub iptables_save($@) {
}
my $dest = Socket::inet_ntop($domain, $addr) .'/'. $mask;
- system ("/sbin/$iptables_bin", qw/-I INPUT -p tcp -m tcp -m state/,
+ system ("/usr/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/,
+ system ("/usr/sbin/$iptables_bin", qw/-I OUTPUT -p tcp -m tcp -m state/,
'-s', $dest, '--sport', $port,
'--state', 'ESTABLISHED', '-j', 'ACCEPT') == 0 or die;
}
@@ -650,6 +625,7 @@ if ($COMMAND eq 'account') {
my $flags = 0;
$flags |= 1 if $OPTS{'register'};
$flags |= 2 if $OPTS{'tos-agreed'};
+ $flags |= 4 if $OPTS{'deactivate'};
exit acme_client({out => \*STDOUT}, $flags, @ARGV);
}
diff --git a/lacme-accountd.md b/lacme-accountd.md
index 59d9bd9..403c68c 100644
--- a/lacme-accountd.md
+++ b/lacme-accountd.md
@@ -135,7 +135,7 @@ See also
[`lacme`(1)], [`ssh`(1)]
-[ACME]: https://tools.ietf.org/html/draft-ietf-acme-acme-02
+[ACME]: https://tools.ietf.org/html/rfc8555
[`lacme`(1)]: lacme.1.html
[`signal`(7)]: http://linux.die.net/man/7/signal
[`gpg`(1)]: https://www.gnupg.org/documentation/manpage.en.html
diff --git a/lacme.md b/lacme.md
index 2d70c49..ca9a6a9 100644
--- a/lacme.md
+++ b/lacme.md
@@ -406,7 +406,7 @@ See also
[`lacme-accountd`(1)]
-[ACME]: https://tools.ietf.org/html/draft-ietf-acme-acme-12
+[ACME]: https://tools.ietf.org/html/rfc8555
[`lacme-accountd`(1)]: lacme-accountd.1.html
[`iptables`(8)]: http://linux.die.net/man/8/iptables
[`ciphers`(1ssl)]: https://www.openssl.org/docs/manmaster/apps/ciphers.html