From d1bc3ad109a3000bda8a7876673ff9a0281e8c7b Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Thu, 26 Apr 2018 20:29:44 +0200 Subject: Use ACME v2 endpoints https://tools.ietf.org/html/draft-ietf-acme-acme-12 --- Changelog | 8 +- client | 238 ++++++++++++++++++++++++++++-------------------------- config/lacme.conf | 10 +-- lacme | 51 ++++++------ lacme-accountd.md | 4 +- lacme.md | 59 ++++++-------- 6 files changed, 188 insertions(+), 182 deletions(-) diff --git a/Changelog b/Changelog index a8ebfe6..633222a 100644 --- a/Changelog +++ b/Changelog @@ -1,6 +1,12 @@ lacme (0.5) upstream; - * Fix manpage generation with pandoc >=2.1 + + 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 Thu, 26 Apr 2018 16:48:13 +0200 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; }; + + 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; }; - 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; }; 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 -# for testing as it has -# relaxed rate-limiting. +# URI of the ACME server's directory. NOTE: Use the staging server +# 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 -- cgit v1.2.3