From 9898b1877ce2973bbc336921969bd7f16d3698fa Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sun, 21 Feb 2021 18:49:14 +0100 Subject: lacme-accountd(1): new setting 'keyid'. This saves a round trip and provides a safeguard against malicious clients. --- client | 56 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 15 deletions(-) (limited to 'client') diff --git a/client b/client index 7a63259..d7874b7 100755 --- a/client +++ b/client @@ -90,7 +90,7 @@ do { print STDERR "WARN: Ignoring extra greeting data from accountd \"$extra\"\n"; } else { print STDERR "Received extra greeting data from accountd: $extra\n" if $ENV{DEBUG}; - ($JWK_thumbprint, $ALG) = @$h{qw/jwk-thumbprint alg/}; + ($JWK_thumbprint, $ALG, $KID) = @$h{qw/jwk-thumbprint alg kid/}; } } my $jwk_str = $S->getline() // die "ERROR: No JWK from lacme-accountd\n"; @@ -194,39 +194,50 @@ 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). +# JSON-encode the hash reference $payload and send it to the ACME server +# $url encapsulated it in a JSON Web Signature (JWS). $header MUST +# contain either "jwk" (JSON Web Key) or "kid" per RFC 8555 sec. 6.2 # https://tools.ietf.org/html/rfc8555 # -sub acme($;$) { - my ($uri, $h) = @_; - die "Missing nonce\n" unless defined $NONCE; +sub acme2($$;$) { + my ($url, $header, $payload) = @_; # Produce the JSON Web Signature: RFC 7515 section 5 - my %header = ( alg => $ALG, nonce => $NONCE, url => $uri ); - defined $KID ? ($header{kid} = $KID) : ($header{jwk} = $JWK); - 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"); + $header->{alg} = $ALG; + $header->{nonce} = $NONCE // die "Missing nonce\n"; + $header->{url} = $url; + my $protected = encode_base64url(json()->encode($header)); + $payload = defined $payload ? encode_base64url(json()->encode($payload)) : ""; + + $S->printflush($protected, ".", $payload, "\r\n"); my $sig = $S->getline() // die "ERROR: No response from lacme-accountd\n"; $sig =~ s/\r\n\z// or die; undef $NONCE; # consume the nonce # Flattened JSON Serialization, RFC 7515 section 7.2.2 - request(POST => $uri, { + request(POST => $url, { payload => $payload, protected => $protected, signature => $sig }); } +# Like above, but always use "kid" +sub acme($;$) { + my ($url, $payload) = @_; + die "Missing KID\n" unless defined $KID; + acme2($url, {kid => $KID}, $payload) +} + my $SERVER_URI = $CONFIG->{server} // '@@acmeapi_server@@'; my %RES; # Get the resource URI from the directory sub acme_resource($%) { my $r = shift; + my %payload = @_; + my %protected; + unless (%RES) { # query the ACME directory to get resources URIs %RES = %{ request_json_decode(request(GET => $SERVER_URI)) }; @@ -235,12 +246,23 @@ sub acme_resource($%) { request(HEAD => $RES{newNonce}); } my $uri = $RES{$r} // die "Unknown resource '$r'\n"; - acme($uri, {@_}); + + if ($r eq "newAccount" or ($r eq "revokeCert" and !defined $KID)) { + # per RFC 8555 sec. 6.2 these requests MUST have a JWK + print STDERR "WARNING: lacme-accountd supplied an empty JWK; try removing 'keyid' ", + "setting from lacme-accountd.conf if the ACME resource request fails.\n" + unless %$JWK; + return acme2($uri, {jwk => $JWK}, \%payload); + } else { + # per RFC 8555 sec. 6.2 all other requests MUST have a KID + return acme($uri, \%payload); + } } # Set the key ID (registration URI) sub set_kid(;$) { my $die = shift // 1; + return if defined $KID; # already set my $r = acme_resource('newAccount', onlyReturnExisting => JSON::true ); if ($r->is_success()) { $KID = $r->header('Location'); @@ -269,6 +291,7 @@ if ($COMMAND eq 'account') { if ($r->is_success()) { $KID = $r->header('Location'); + print STDERR "Key ID: $KID\n"; $r = acme($KID, \%h); request_json_decode($r, 1, \*STDOUT) if $r->is_success() and $r->content_type() eq 'application/json'; @@ -391,7 +414,10 @@ elsif ($COMMAND eq 'revokeCert') { my $der = do { local $/ = undef; }; close STDIN or die "close: $!"; - # send a KID if the request is signed with the acccount key, otherwise send a JWK + # RFC 8555 sec. 6.2: send a KID if the request is signed with the + # acccount key, otherwise send a JWK + # We have no way to know which of the account key or certificate key + # is used, so we try to get a KID and fallback to sending the JWK set_kid(0); my $r = acme_resource('revokeCert', certificate => encode_base64url($der)); -- cgit v1.2.3