aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Changelog12
-rw-r--r--INSTALL3
-rw-r--r--Makefile4
-rwxr-xr-xclient238
-rw-r--r--config/lacme.conf10
-rwxr-xr-xlacme51
-rw-r--r--lacme-accountd.md4
-rw-r--r--lacme.md59
8 files changed, 197 insertions, 184 deletions
diff --git a/Changelog b/Changelog
index 2fd2f8e..633222a 100644
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,15 @@
+lacme (0.5) upstream;
+
+ + Use ACME v2 endpoints (update protocol to the last draft of the spec
+ https://tools.ietf.org/html/draft-ietf-acme-acme-12 ). Remove the
+ 'reg=' command, and rename the 'new-reg', 'new-cert' and
+ 'revoke-cert' commands to 'account', 'newOrder', and 'revokeCert'
+ respectively, to match the the URI resource names. For backward
+ compatibility 'new-cert' and 'revoke-cert' remain supported.
+ - Fix manpage generation with pandoc >=2.1
+
+ -- Guilhem Moulin <guilhem@fripost.org> Thu, 26 Apr 2018 16:48:13 +0200
+
lacme (0.4) upstream;
* Fix generation of manpages with pandoc >=1.18
diff --git a/INSTALL b/INSTALL
index 935b052..82de279 100644
--- a/INSTALL
+++ b/INSTALL
@@ -31,12 +31,13 @@ lacme depends on OpenSSL and the following Perl modules:
- MIME::Base64 (core module)
- Net::SSLeay
- POSIX (core module)
+ - Types::Serialiser
- Socket (core module)
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
+ apt-get install openssl libconfig-tiny-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/Makefile b/Makefile
index 76057e2..5d421bf 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ all: ${MANPAGES}
# upper case the headers and remove the links
%.1: %.md
- @pandoc -S -f markdown -t json "$<" | \
+ @pandoc -f markdown -t json "$<" | \
jq " \
def fixheaders: \
if .t == \"Header\" then \
@@ -29,7 +29,7 @@ all: ${MANPAGES}
, meta \
, blocks: .blocks | map(fixheaders) | map(fixlinks) \
}" | \
- pandoc -sS -f json -t man -o "$@"
+ pandoc -s -f json -t man+smart -o "$@"
install: ${MANPAGES}
install -d $(DESTDIR)/etc/lacme
diff --git a/client b/client
index e9fb8f8..838b184 100755
--- a/client
+++ b/client
@@ -51,6 +51,7 @@ use Digest::SHA qw/sha256 sha256_hex/;
use MIME::Base64 qw/encode_base64 encode_base64url/;
use LWP::UserAgent ();
+use Types::Serialiser ();
use JSON ();
use Config::Tiny ();
@@ -75,6 +76,7 @@ open (my $S, '+<&=', $1+0) or die "fdopen $1: $!";
die "Error: Invalid client version\n" unless
$S->getline() =~ /\A(\d+) OK(?:.*)\r\n\z/ and $1 == $PROTOCOL_VERSION;
my $JWK = JSON::->new->decode($S->getline());
+my $KID;
# JSON keys need to be sorted lexicographically (for instance in the thumbprint)
sub json() { JSON::->new->utf8->canonical(); }
@@ -103,11 +105,12 @@ my $UA = do {
$ssl_opts{$_} = $args{$_} foreach grep /^SSL_/, keys %args;
LWP::UserAgent::->new( ssl_opts => \%ssl_opts );
} // die "Can't create LWP::UserAgent object";
+$UA->default_header( 'Accept-Language' => 'en' );
#############################################################################
# Send an HTTP request to the ACME server. If $json is defined, send
-# its encoding as the request content, with "application/json" as
+# its encoding as the request content, with "application/jose+json" as
# Content-Type.
#
sub request($$;$) {
@@ -116,34 +119,22 @@ sub request($$;$) {
my $req = HTTP::Request::->new($method => $uri) or die "Can't $method $uri";
if (defined $json) {
- $req->content_type('application/json');
+ $req->content_type('application/jose+json');
$req->content(json()->encode($json));
}
my $r = $UA->request($req) or die "Can't $method $uri";
- $NONCE = $r->header('Replay-Nonce'); # undef $NONCE if the header is missing
- print STDERR "[$$] >>> ", $r->status_line, "\n", $r->headers->as_string, "\n" if $ENV{DEBUG};
+ $NONCE //= $r->header('Replay-Nonce'); # undef $NONCE if the header is missing
+ print STDERR "[$$] >>> ", $r->status_line, "\n", $r->headers->as_string("\n") if $ENV{DEBUG};
return $r;
}
-# List all 'Links' headers with the relationship $rel (RFC 5988)
-sub request_link_rel($$) {
- my ($r, $rel) = @_;
- grep defined, map
- { /\A<([^>]+)>.*;\s*rel=([^;]+)/
- ; my ($link, $rels) = ($1, $2 // '')
- ; (grep { $rel eq $_ } map { /^"(.*)"/ ? $1 : $_ } split(/\s+/, $rels)) ? $link : undef
- }
- $r->header('Link');
-}
-
# The request's Status Line; if the Content-Type is
-# application/problem+json, parse the decoded content as JSON and add
-# the value of the 'detail' field to the Status Line.
-# https://tools.ietf.org/html/draft-ietf-appsawg-http-problem
+# application/problem+json (RFC 7807), parse the decoded content as JSON
+# and add the value of the 'detail' field to the Status Line.
sub request_status_line($) {
my $r = shift;
my $msg = $r->status_line;
- if ($r->content_type() eq 'application/problem+json') {
+ if (!$r->is_success() and $r->content_type() eq 'application/problem+json') {
my $content = json()->decode($r->decoded_content());
print STDERR json()->pretty->encode($content), "\n" if $ENV{DEBUG};
$msg .= " (".$content->{detail}.")" if defined $content->{detail};
@@ -154,121 +145,133 @@ sub request_status_line($) {
# 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.
-sub request_json_decode($;$) {
+sub request_json_decode($;$$) {
my $r = shift;
my $dump = shift || $ENV{DEBUG};
+ my $fh = shift // \*STDERR;
die request_status_line($r), "\n" unless $r->is_success();
my $content = $r->decoded_content();
die "Content-Type: ".$r->content_type()." is not application/json\n"
unless $r->content_type() eq 'application/json';
- $content = json()->decode($content);
- print STDERR json()->pretty->encode($content), "\n" if $dump;
- return $content;
+ my $json = json()->decode($content);
+ print $fh (-t $fh ? (json()->pretty->encode($json)."\n") : $content)
+ if $dump;
+ return $json;
}
#############################################################################
# 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
+# https://tools.ietf.org/html/draft-ietf-acme-acme-12
#
-sub acme($$) {
- my ($uri, $h) = @_;
-
- # the ACME server MUST provide a Replay-Nonce header field in
- # response to a HEAD request for any valid resource
- request(HEAD => $uri) unless defined $NONCE;
+sub acme($@) {
+ my $uri = shift;
+ die "Missing nonce\n" unless defined $NONCE;
# 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 %header = ( alg => 'RS256', nonce => $NONCE, url => $uri );
+ defined $KID ? ($header{kid} = $KID) : ($header{jwk} = $JWK);
+ my $payload = encode_base64url(json()->encode({ @_ }));
+ my $protected = encode_base64url(json()->encode(\%header));
my $data = $protected .'.'. $payload;
$S->printflush($data, "\r\n");
my $sig = $S->getline();
$sig =~ s/\r\n\z// or die;
+ undef $NONCE; # consume the nonce
# Flattened JSON Serialization, RFC 7515 section 7.2.2
request(POST => $uri, {
payload => $payload,
protected => $protected,
- header => \%header,
signature => $sig
});
}
-my $SERVER_URI = $CONFIG->{server} // 'https://acme-v01.api.letsencrypt.org/';
-$SERVER_URI .= '/' unless substr($SERVER_URI, -1, 1) eq '/';
+my $SERVER_URI = $CONFIG->{server} // 'https://acme-v02.api.letsencrypt.org/directory';
my %RES;
# Get the resource URI from the directory
sub acme_resource($%) {
my $r = shift;
- # Query the root ACME directory to initialize the nonce and get the resources URIs
- %RES = %{ request_json_decode(request(GET => $SERVER_URI.'directory')) } unless %RES;
- my $uri = $RES{$r} // die "Missing ressource for \"$r\"\n";
- acme($uri, {resource => $r, @_});
+ unless (%RES) {
+ # query the ACME directory to get resources URIs
+ %RES = %{ request_json_decode(request(GET => $SERVER_URI)) };
+ # send a HEAD request to the newNonce resource to get a fresh nonce
+ die "Unknown resource 'newNonce'\n" unless defined $RES{newNonce};
+ request(HEAD => $RES{newNonce});
+ }
+ my $uri = $RES{$r} // die "Unknown resource '$r'\n";
+ acme($uri, @_);
}
-
-#############################################################################
-# new-reg AGREEMENT_URI [CONTACT ..]
-#
-if ($COMMAND eq 'new-reg') {
- my $agreement = shift @ARGV;
- print STDERR "Requesting new registration ".(@ARGV ? ("for ".join(', ', @ARGV)) : "")."\n";
-
- my %h = (contact => \@ARGV);
- $h{agreement} = $agreement if $agreement ne '';
- my $r = acme_resource('new-reg', %h);
-
- my ($terms) = request_link_rel($r, 'terms-of-service');
- request_json_decode($r,1) if $r->is_success() and $ENV{DEBUG}; # pretty-print the JSON
- print STDERR request_status_line($r), "\n";
- print STDERR "Subscriber Agreement URI: $terms\n" if defined $terms;
- print STDERR "Registration URI: ", $r->header('Location'), "\n";
- exit ($r->is_success() ? 0 : 1);
+# Set the key ID (registration URI)
+sub set_kid(;$) {
+ my $die = shift // 1;
+ my $r = acme_resource('newAccount', onlyReturnExisting => Types::Serialiser::true );
+ if ($r->is_success()) {
+ $KID = $r->header('Location');
+ } elsif ($die) {
+ die request_status_line($r), "\n";
+ }
}
#############################################################################
-# reg=URI AGREEMENT_URI [CONTACT ..]
+# account FLAGS [CONTACT ..]
#
-elsif ($COMMAND =~ /\Areg=(\p{Print}+)\Z/) {
- die "Empty registration URI (use the 'new-reg' command to determine the URI)\n" if $1 eq '';
- my $uri = $SERVER_URI.$1;
- my $agreement = shift @ARGV;
-
- my %h = (resource => 'reg');
- $h{agreement} = $agreement if $agreement ne '';
- $h{contact} = \@ARGV if @ARGV; # don't empty the contact list
- my $r = acme($uri, \%h);
-
- my ($terms) = request_link_rel($r, 'terms-of-service');
- $r->is_success() ? request_json_decode($r,1) # pretty-print the JSON
- : print STDERR request_status_line($r), "\n";
- print STDERR "Subscriber Agreement URI: $terms\n" if defined $terms;
+if ($COMMAND eq 'account') {
+ my $flags = shift @ARGV;
+
+ my %h = ( contact => \@ARGV ) if @ARGV;
+ $h{onlyReturnExisting} = Types::Serialiser::true unless $flags & 0x01;
+ $h{termsOfServiceAgreed} = Types::Serialiser::true if $flags & 0x02;
+
+ print STDERR "Requesting new registration ".(@ARGV ? ("for ".join(', ', @ARGV)) : "")."\n"
+ if $flags & 0x01;
+
+ my $r = acme_resource('newAccount', %h);
+ # TODO: list account orders: https://github.com/letsencrypt/boulder/issues/3335
+
+ if ($r->is_success()) {
+ $KID = $r->header('Location');
+ $r = acme($KID, %h);
+ request_json_decode($r, 1, \*STDOUT)
+ if $r->is_success() and $r->content_type() eq 'application/json';
+ }
+
+ print STDERR request_status_line($r), "\n"
+ if !$r->is_success() or $ENV{DEBUG};
exit ($r->is_success() ? 0 : 1);
}
#############################################################################
-# new-cert AUTHZ [AUTHZ ..]
+# newOrder AUTHZ [AUTHZ ..]
# Read the CSR (in DER format) from STDIN, print the cert (in PEM format
# to STDOUT)
#
-elsif ($COMMAND eq 'new-cert') {
+elsif ($COMMAND eq 'newOrder') {
die unless @ARGV;
my $timeout = $CONFIG->{timeout} // 10;
- foreach my $domain (@ARGV) {
- print STDERR "Processing new DNS authz for $domain\n" if $ENV{DEBUG};
- my $r = acme_resource('new-authz', identifier => {type => 'dns', value => $domain});
+ my $csr = do { local $/ = undef; <STDIN> };
+
+ set_kid();
+ my @identifiers = map {{ type => 'dns', value => $_ }} @ARGV;
+ my $r = acme_resource('newOrder', identifiers => \@identifiers);
+ my $order = request_json_decode($r);
+
+ foreach (@{$order->{authorizations}}) {
+ my $authz = request_json_decode(request(GET => $_));
+ next unless $authz->{status} eq 'pending';
+
+ my $identifier = $authz->{identifier}->{value};
+ my ($challenge) = grep {$_->{type} eq 'http-01'} @{$authz->{challenges} // []};
+ die "Missing 'http-01' challenge in server response for '$identifier'\n"
+ unless defined $challenge;
- my ($challenge) = grep {$_->{type} eq 'http-01'}
- @{request_json_decode($r)->{challenges} // []};
- die "Missing 'http-01' challenge in server response" unless defined $challenge;
die "Invalid challenge token ".($challenge->{token} // '')."\n"
# ensure we don't write outside the cwd
unless ($challenge->{token} // '') =~ /\A[A-Za-z0-9_\-]+\z/;
@@ -285,23 +288,25 @@ elsif ($COMMAND eq 'new-cert') {
die "Can't open $challenge->{token}: $!";
}
- $r = acme($challenge->{uri}, {
- resource => 'challenge',
- keyAuthorization => $keyAuthorization
- });
- # wait until the status become 'valid'
- for ( my $i = 0, my $content, my $status;
- $content = request_json_decode($r),
- $status = $content->{status} // 'pending',
+ $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->{uri})) {
- if (defined (my $problem = $content->{error})) { # problem document (RFC 7807)
+ $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 $domain (status: ".$status.")\n" if $status ne 'pending';
+ 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'))) {
@@ -310,41 +315,39 @@ elsif ($COMMAND eq 'new-cert') {
}
$i += $sleep;
- die "Timeout exceeded while waiting for challenge to pass ($domain)\n" if $timeout > 0 and $i >= $timeout;
+ die "Timeout exceeded while waiting for challenges to pass ($identifier)\n"
+ if $timeout > 0 and $i >= $timeout;
sleep $sleep;
}
}
- my $csr = do { local $/ = undef; <STDIN> };
- my $r = acme_resource('new-cert', csr => encode_base64url($csr));
- die request_status_line($r), "\n" unless $r->is_success();
- my $uri = $r->header('Location');
- # https://acme-v01.api.letsencrypt.org/acme/cert/$serial
+ $r = acme($order->{finalize}, csr => encode_base64url($csr));
+ my $resp = request_json_decode($r);
+
+ my $uri = $resp->{certificate};
print STDERR "Certificate URI: $uri\n";
- if ($r->decoded_content() eq '') { # wait for the cert
- for (my $i = 0;;) {
- $r = request('GET' => $uri);
- die request_status_line($r), "\n" unless $r->is_success();
- last unless $r->code == 202; # Accepted
- my $retry_after = $r->header('Retry-After') // 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;
- sleep $retry_after;
- }
+ # pool until the cert is available
+ for (my $i = 0;;) {
+ $r = request('GET' => $uri);
+ die request_status_line($r), "\n" unless $r->is_success();
+ last unless $r->code == 202; # Accepted
+ my $retry_after = $r->header('Retry-After') // 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;
+ sleep $retry_after;
}
- my $der = $r->decoded_content();
- # conversion DER -> PEM
+ # keep only the leaf certificate
pipe my $rd, my $wd or die "Can't pipe: $!";
my $pid = fork // die "Can't fork: $!";
unless ($pid) {
open STDIN, '<&', $rd or die "Can't dup: $!";
- exec qw/openssl x509 -inform DER -outform PEM/ or die;
+ exec qw/openssl x509 -outform PEM/ or die;
}
$rd->close() or die "Can't close: $!";
- $wd->print($der);
+ $wd->print( $r->decoded_content() );
$wd->close() or die "Can't close: $!";
waitpid $pid => 0;
@@ -353,17 +356,20 @@ elsif ($COMMAND eq 'new-cert') {
#############################################################################
-# revoke-cert
+# revokeCert
# The certificate to revoke is passed (in DER format) to STDIN; this
# is required since the ACME client might not have read access to the
# X.509 file
#
-elsif ($COMMAND eq 'revoke-cert') {
+elsif ($COMMAND eq 'revokeCert') {
die if @ARGV;
my $der = do { local $/ = undef; <STDIN> };
close STDIN or die "Can't close: $!";
- my $r = acme_resource('revoke-cert', certificate => encode_base64url($der));
+ # send a KID if the request is signed with the acccount key, otherwise send a JWK
+ set_kid(0);
+
+ my $r = acme_resource('revokeCert', certificate => encode_base64url($der));
exit 0 if $r->is_success();
die request_status_line($r), "\n";
}
diff --git a/config/lacme.conf b/config/lacme.conf
index 3cc1b34..39c8654 100644
--- a/config/lacme.conf
+++ b/config/lacme.conf
@@ -1,4 +1,4 @@
-# For certificate issuance (new-cert command), specify a space-separated
+# For certificate issuance (newOrder command), specify a space-separated
# certificate configuration files or directories to use
#
#config-certs = lacme-certs.conf lacme-certs.conf.d/
@@ -33,11 +33,11 @@
#
#command = /usr/lib/lacme/client
-# Root URI of the ACME server. NOTE: Use the staging server
-# <https://acme-staging.api.letsencrypt.org/> for testing as it has
-# relaxed rate-limiting.
+# URI of the ACME server's directory. NOTE: Use the staging server
+# <https://acme-staging-v02.api.letsencrypt.org/directory> for testing
+# as it has relaxed rate-limiting.
#
-#server = https://acme-v01.api.letsencrypt.org/
+#server = https://acme-v02.api.letsencrypt.org/directory
# Timeout in seconds after which the client stops polling the ACME
# server and considers the request failed.
diff --git a/lacme b/lacme
index 2014f22..3e5347d 100755
--- a/lacme
+++ b/lacme
@@ -62,11 +62,11 @@ sub usage(;$$) {
}
exit $rv;
}
-usage(1) unless GetOptions(\%OPTS, qw/config=s config-certs=s@ socket=s agreement-uri=s min-days=i quiet|q debug help|h/);
+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(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
+$COMMAND = $COMMAND =~ /\A(account|newOrder|new-cert|revokeCert|revoke-cert)\z/ ? $1
: usage(1, "Invalid command: $COMMAND"); # validate and untaint $COMMAND
@ARGV = map { /\A(\p{Print}*)\z/ ? $1 : die } @ARGV; # untaint @ARGV
@@ -556,7 +556,7 @@ sub spawn($@) {
if (defined $args->{in}) {
pipe $in_rd, $in_wd or die "pipe: $!";
}
- if (defined $args->{out}) {
+ if (defined $args->{out} and ref $args->{out} ne 'GLOB') {
pipe $out_rd, $out_wd or die "pipe: $!";
}
@@ -570,11 +570,13 @@ sub spawn($@) {
} else {
open STDIN, '<', '/dev/null' or die "Can't open /dev/null: $!";
}
- if (defined $args->{out}) {
+ if (!defined $args->{out}) {
+ open STDOUT, '>', '/dev/null' or die "Can't open /dev/null: $!";
+ } elsif (ref $args->{out} ne 'GLOB') {
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: $!";
+ } elsif (fileno(STDOUT) != fileno($args->{out})) {
+ open STDOUT, '>&', $args->{out} or die "Can't dup: $!";
}
exec { $exec[0] } @exec or die;
}
@@ -590,14 +592,18 @@ sub spawn($@) {
$in_wd->print($args->{in});
$in_wd->close() or die "Can't close: $!";
}
- if (defined $args->{out}) {
+ if (defined $args->{out} and ref $args->{out} ne 'GLOB') {
$out_wd->close() or die "Can't close: $!";
- ${$args->{out}} = do { local $/ = undef; $out_rd->getline() };
+ if (ref $args->{out} eq 'CODE') {
+ $args->{out}->($out_rd);
+ } elsif (ref $args->{out} eq 'SCALAR') {
+ ${$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;
+ undef ${$args->{out}} if defined $args->{out} and ref $args->{out} eq 'SCALAR' and $? > 0;
return $? > 255 ? ($? >> 8) : $? > 0 ? 1 : 0;
}
@@ -638,25 +644,21 @@ sub install_cert($$;$) {
#############################################################################
-# new-reg [--agreement-uri=URI] [CONTACT ..]
-# reg=URI [--agreement-uri=URI] [CONTACT ..]
+# account [--tos-agreed] [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);
+if ($COMMAND eq 'account') {
+ my $flags = 0;
+ $flags |= 1 if $OPTS{'register'};
+ $flags |= 2 if $OPTS{'tos-agreed'};
+ exit acme_client({out => \*STDOUT}, $flags, @ARGV);
}
#############################################################################
-# new-cert [SECTION ..]
+# newOrder [SECTION ..]
#
-elsif ($COMMAND eq 'new-cert') {
+elsif ($COMMAND eq 'newOrder' or $COMMAND eq 'new-cert') {
+ $COMMAND = 'newOrder';
my $conffiles = defined $OPTS{'config-certs'} ? $OPTS{'config-certs'}
: defined $CONFIG->{_}->{'config-certs'} ? [ split(/\s+/, $CONFIG->{_}->{'config-certs'}) ]
: [ "$NAME-certs.conf", "$NAME-certs.conf.d/" ];
@@ -820,11 +822,12 @@ elsif ($COMMAND eq 'new-cert') {
#############################################################################
-# revoke-cert FILE [FILE ..]
+# revokeCert FILE [FILE ..]
#
-elsif ($COMMAND eq 'revoke-cert') {
+elsif ($COMMAND eq 'revokeCert' or $COMMAND eq 'revoke-cert') {
die "Nothing to revoke\n" unless @ARGV;
my $rv = 0;
+ $COMMAND = 'revokeCert';
foreach my $filename (@ARGV) {
print STDERR "Revoking $filename\n";
diff --git a/lacme-accountd.md b/lacme-accountd.md
index 4d3e1a5..59d9bd9 100644
--- a/lacme-accountd.md
+++ b/lacme-accountd.md
@@ -122,13 +122,13 @@ Run `lacme-accountd` in a first terminal:
Then, while `lacme-accountd` is running, execute locally [`lacme`(1)] in
another terminal:
- ~$ sudo lacme --socket=$XDG_RUNTIME_DIR/S.lacme new-cert
+ ~$ sudo lacme --socket=$XDG_RUNTIME_DIR/S.lacme newOrder
Alternatively, use [OpenSSH] 6.7 or later to forward the socket and
execute [`lacme`(1)] remotely:
~$ ssh -oExitOnForwardFailure=yes -tt -R /path/to/remote.sock:$XDG_RUNTIME_DIR/S.lacme user@example.org \
- sudo lacme --socket=/path/to/remote.sock new-cert
+ sudo lacme --socket=/path/to/remote.sock newOrder
See also
========
diff --git a/lacme.md b/lacme.md
index ba1e5be..2d70c49 100644
--- a/lacme.md
+++ b/lacme.md
@@ -34,7 +34,7 @@ with its own executable:
component with access to the private key material of the server
keys. It is used to fork the [ACME] client (and optionally the
[ACME] webserver) after dropping root privileges.
- For certificate issuances (`new-cert` command), it also generates
+ For certificate issuances (`newOrder` command), it also generates
Certificate Signing Requests, then verifies the validity of the
issued certificate, and optionally reloads or restarts services when
the *notify* option is set.
@@ -48,7 +48,7 @@ with its own executable:
UNIX-domain socket to the [ACME] client: data signatures are
requested by writing the data to be signed to the socket.
- 4. For certificate issuances (`new-cert` command), an optional
+ 4. For certificate issuances (`newOrder` command), an optional
webserver (specified with the *command* option of the [`[webserver]`
section](#webserver-section) of the configuration file), which is
spawned by the “master” `lacme`. (The only challenge type currently
@@ -61,44 +61,36 @@ with its own executable:
Commands
========
-`lacme` [`--agreement-uri=`*URI*] `new-reg` [*CONTACT* …]
+`lacme account` [`--tos-agreed`] [`--register`] [*CONTACT* …]
-: Register the account key managed by [`lacme-accountd`(1)]. A list
- of *CONTACT* information (such as `maito:` URIs) can be specified in
- order for the server to contact the client for issues related to
- this registration (such as notifications about server-initiated
- revocations).
+: Register (if `--registered` is set) a [`lacme-accountd`(1)]-managed
+ account key. A list of *CONTACT* information (such as `maito:`
+ URIs) can be specified in order for the [ACME] server to contact the
+ client for issues related to this registration (such as
+ notifications about server-initiated revocations). `--tos-agreed`
+ indicates agreement with the [ACME] server's Terms of Service (and
+ might be required for registration).
- `--agreement-uri=` can be used to specify a *URI* referring to a
- subscriber agreement or terms of service provided by the server;
- adding this options indicates the client's agreement with the
- referenced terms. Note that the server might require the client to
- agree to subscriber agreement before performing any further actions.
+ If the account key is already registered, update the contact info
+ with the given list of *CONTACT* information.
- If the account key is already registered, `lacme` prints the URI of
- the existing registration and aborts.
+ Upon success, `lacme` prints the new or updated Account Object from
+ the [ACME] server.
-`lacme` [`--agreement-uri=`*URI*] `reg=`*URI* [*CONTACT* …]
-
-: Dump or edit the registration *URI* (relative to the [ACME] server
- URI, which is specified with the *server* option of the [`[client]`
- section](#client-section) of the configuration file).
-
- When specified, the list of *CONTACT* information and the agreement
- *URI* are sent to the server to replace the existing values.
-
-`lacme` [`--config-certs=`*FILE*] [`--min-days=`*INT*] `new-cert` [*SECTION* …]
+`lacme` [`--config-certs=`*FILE*] [`--min-days=`*INT*] `newOrder` [*SECTION* …]
: Read the certificate configuration *FILE* (see the **[certificate
configuration file](#certificate-configuration-file)** section below
for the configuration options), and request new Certificate Issuance
for each of its sections (or the given list of *SECTION*s).
+ Command alias: `new-order`.
-`lacme` `revoke-cert` *FILE* [*FILE* …]
+`lacme` `revokeCert` *FILE* [*FILE* …]
: Request that the given certificate(s) *FILE*(s) be revoked. For
this command, [`lacme-accountd`(1)] can be pointed to either the
account key or the server's private key.
+ Command alias: `revoke-cert`.
Generic options
===============
@@ -149,7 +141,7 @@ Default section
*config-certs*
-: For certificate issuances (`new-cert` command), specify the
+: For certificate issuances (`newOrder` command), specify the
space-separated list of certificate configuration files or
directories to use (see the **[certificate configuration
file](#certificate-configuration-file)** section below for the
@@ -198,7 +190,7 @@ of [ACME] commands and dialogues with the remote [ACME] server).
*server*
: Root URI of the [ACME] server.
- Default: `https://acme-v01.api.letsencrypt.org/`.
+ Default: `https://acme-v02.api.letsencrypt.org/directory`.
*timeout*
@@ -322,7 +314,7 @@ If the section (including its header) is absent or commented out,
Certificate configuration file
==============================
-For certificate issuances (`new-cert` command), a separate file is used
+For certificate issuances (`newOrder` command), a separate file is used
to configure paths to the certificate and key, as well as the subject,
subjectAltName, etc. to generate Certificate Signing Requests.
Each section denotes a separate certificate issuance.
@@ -405,17 +397,16 @@ Valid options are:
Examples
========
- ~$ sudo lacme new-reg mailto:noreply@example.com
- ~$ sudo lacme reg=/acme/reg/123456 --agreement-uri=https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
- ~$ sudo lacme new-cert
- ~$ sudo lacme revoke-cert /path/to/server/certificate.pem
+ ~$ sudo lacme account --register --tos-agreed mailto:noreply@example.com
+ ~$ sudo lacme newOrder
+ ~$ sudo lacme revokeCert /path/to/server/certificate.pem
See also
========
[`lacme-accountd`(1)]
-[ACME]: https://tools.ietf.org/html/draft-ietf-acme-acme-02
+[ACME]: https://tools.ietf.org/html/draft-ietf-acme-acme-12
[`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