...

Source file src/github.com/letsencrypt/boulder/wfe2/verify.go

Documentation: github.com/letsencrypt/boulder/wfe2

     1  package wfe2
     2  
     3  import (
     4  	"context"
     5  	"crypto/ecdsa"
     6  	"crypto/rsa"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"github.com/prometheus/client_golang/prometheus"
    18  	"google.golang.org/grpc/status"
    19  	"gopkg.in/go-jose/go-jose.v2"
    20  
    21  	"github.com/letsencrypt/boulder/core"
    22  	berrors "github.com/letsencrypt/boulder/errors"
    23  	"github.com/letsencrypt/boulder/goodkey"
    24  	"github.com/letsencrypt/boulder/grpc"
    25  	nb "github.com/letsencrypt/boulder/grpc/noncebalancer"
    26  	"github.com/letsencrypt/boulder/nonce"
    27  	noncepb "github.com/letsencrypt/boulder/nonce/proto"
    28  	"github.com/letsencrypt/boulder/probs"
    29  	sapb "github.com/letsencrypt/boulder/sa/proto"
    30  	"github.com/letsencrypt/boulder/web"
    31  )
    32  
    33  const (
    34  	// POST requests with a JWS body must have the following Content-Type header
    35  	expectedJWSContentType = "application/jose+json"
    36  
    37  	maxRequestSize = 50000
    38  )
    39  
    40  func sigAlgorithmForKey(key *jose.JSONWebKey) (jose.SignatureAlgorithm, error) {
    41  	switch k := key.Key.(type) {
    42  	case *rsa.PublicKey:
    43  		return jose.RS256, nil
    44  	case *ecdsa.PublicKey:
    45  		switch k.Params().Name {
    46  		case "P-256":
    47  			return jose.ES256, nil
    48  		case "P-384":
    49  			return jose.ES384, nil
    50  		case "P-521":
    51  			return jose.ES512, nil
    52  		}
    53  	}
    54  	return "", errors.New("JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)")
    55  }
    56  
    57  var supportedAlgs = map[string]bool{
    58  	string(jose.RS256): true,
    59  	string(jose.ES256): true,
    60  	string(jose.ES384): true,
    61  	string(jose.ES512): true,
    62  }
    63  
    64  // Check that (1) there is a suitable algorithm for the provided key based on its
    65  // Golang type, (2) the Algorithm field on the JWK is either absent, or matches
    66  // that algorithm, and (3) the Algorithm field on the JWK is present and matches
    67  // that algorithm.
    68  func checkAlgorithm(key *jose.JSONWebKey, header jose.Header) error {
    69  	sigHeaderAlg := header.Algorithm
    70  	if !supportedAlgs[sigHeaderAlg] {
    71  		return fmt.Errorf(
    72  			"JWS signature header contains unsupported algorithm %q, expected one of RS256, ES256, ES384 or ES512",
    73  			header.Algorithm,
    74  		)
    75  	}
    76  
    77  	expectedAlg, err := sigAlgorithmForKey(key)
    78  	if err != nil {
    79  		return err
    80  	}
    81  	if sigHeaderAlg != string(expectedAlg) {
    82  		return fmt.Errorf("JWS signature header algorithm %q does not match expected algorithm %q for JWK", sigHeaderAlg, string(expectedAlg))
    83  	}
    84  	if key.Algorithm != "" && key.Algorithm != string(expectedAlg) {
    85  		return fmt.Errorf("JWK key header algorithm %q does not match expected algorithm %q for JWK", key.Algorithm, string(expectedAlg))
    86  	}
    87  	return nil
    88  }
    89  
    90  // jwsAuthType represents whether a given POST request is authenticated using
    91  // a JWS with an embedded JWK (v1 ACME style, new-account, revoke-cert) or an
    92  // embedded Key ID (v2 AMCE style) or an unsupported/unknown auth type.
    93  type jwsAuthType int
    94  
    95  const (
    96  	embeddedJWK jwsAuthType = iota
    97  	embeddedKeyID
    98  	invalidAuthType
    99  )
   100  
   101  // checkJWSAuthType examines the protected headers from a bJSONWebSignature to
   102  // determine if the request being authenticated by the JWS is identified using
   103  // an embedded JWK or an embedded key ID. If no signatures are present, or
   104  // mutually exclusive authentication types are specified at the same time, a
   105  // problem is returned. checkJWSAuthType is separate from enforceJWSAuthType so
   106  // that endpoints that need to handle both embedded JWK and embedded key ID
   107  // requests can determine which type of request they have and act accordingly
   108  // (e.g. acme v2 cert revocation).
   109  func checkJWSAuthType(header jose.Header) (jwsAuthType, *probs.ProblemDetails) {
   110  	// There must not be a Key ID *and* an embedded JWK
   111  	if header.KeyID != "" && header.JSONWebKey != nil {
   112  		return invalidAuthType, probs.Malformed(
   113  			"jwk and kid header fields are mutually exclusive")
   114  	} else if header.KeyID != "" {
   115  		return embeddedKeyID, nil
   116  	} else if header.JSONWebKey != nil {
   117  		return embeddedJWK, nil
   118  	}
   119  
   120  	return invalidAuthType, nil
   121  }
   122  
   123  // enforceJWSAuthType enforces that the protected headers from a
   124  // bJSONWebSignature have the provided auth type. If there is an error
   125  // determining the auth type or if it is not the expected auth type then a
   126  // problem is returned.
   127  func (wfe *WebFrontEndImpl) enforceJWSAuthType(
   128  	header jose.Header,
   129  	expectedAuthType jwsAuthType) *probs.ProblemDetails {
   130  	// Check the auth type for the provided JWS
   131  	authType, prob := checkJWSAuthType(header)
   132  	if prob != nil {
   133  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAuthTypeInvalid"}).Inc()
   134  		return prob
   135  	}
   136  	// If the auth type isn't the one expected return a sensible problem based on
   137  	// what was expected
   138  	if authType != expectedAuthType {
   139  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAuthTypeWrong"}).Inc()
   140  		switch expectedAuthType {
   141  		case embeddedKeyID:
   142  			return probs.Malformed("No Key ID in JWS header")
   143  		case embeddedJWK:
   144  			return probs.Malformed("No embedded JWK in JWS header")
   145  		}
   146  	}
   147  	return nil
   148  }
   149  
   150  // validPOSTRequest checks a *http.Request to ensure it has the headers
   151  // a well-formed ACME POST request has, and to ensure there is a body to
   152  // process.
   153  func (wfe *WebFrontEndImpl) validPOSTRequest(request *http.Request) *probs.ProblemDetails {
   154  	// All POSTs should have an accompanying Content-Length header
   155  	if _, present := request.Header["Content-Length"]; !present {
   156  		wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "ContentLengthRequired"}).Inc()
   157  		return probs.ContentLengthRequired()
   158  	}
   159  
   160  	// Per 6.2 ALL POSTs should have the correct JWS Content-Type for flattened
   161  	// JSON serialization.
   162  	if _, present := request.Header["Content-Type"]; !present {
   163  		wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "NoContentType"}).Inc()
   164  		return probs.InvalidContentType(fmt.Sprintf("No Content-Type header on POST. Content-Type must be %q",
   165  			expectedJWSContentType))
   166  	}
   167  	if contentType := request.Header.Get("Content-Type"); contentType != expectedJWSContentType {
   168  		wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "WrongContentType"}).Inc()
   169  		return probs.InvalidContentType(fmt.Sprintf("Invalid Content-Type header on POST. Content-Type must be %q",
   170  			expectedJWSContentType))
   171  	}
   172  
   173  	// Per 6.4.1 "Replay-Nonce" clients should not send a Replay-Nonce header in
   174  	// the HTTP request, it needs to be part of the signed JWS request body
   175  	if _, present := request.Header["Replay-Nonce"]; present {
   176  		wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "ReplayNonceOutsideJWS"}).Inc()
   177  		return probs.Malformed("HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field")
   178  	}
   179  
   180  	// All POSTs should have a non-nil body
   181  	if request.Body == nil {
   182  		wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "NoPOSTBody"}).Inc()
   183  		return probs.Malformed("No body on POST")
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  // nonceWellFormed checks a JWS' Nonce header to ensure it is well-formed,
   190  // otherwise a bad nonce problem is returned. This avoids unnecessary RPCs to
   191  // the nonce redemption service.
   192  func nonceWellFormed(nonceHeader string, prefixLen int) *probs.ProblemDetails {
   193  	errBadNonce := probs.BadNonce(fmt.Sprintf("JWS has an invalid anti-replay nonce: %q", nonceHeader))
   194  	if len(nonceHeader) <= prefixLen {
   195  		// Nonce header was an unexpected length because there is either:
   196  		// 1) no nonce, or
   197  		// 2) no nonce material after the prefix.
   198  		return errBadNonce
   199  	}
   200  	body, err := base64.RawURLEncoding.DecodeString(nonceHeader[prefixLen:])
   201  	if err != nil {
   202  		// Nonce was not valid base64url.
   203  		return errBadNonce
   204  	}
   205  	if len(body) != nonce.NonceLen {
   206  		// Nonce was an unexpected length.
   207  		return errBadNonce
   208  	}
   209  	return nil
   210  }
   211  
   212  // validNonce checks a JWS' Nonce header to ensure it is one that the
   213  // nonceService knows about, otherwise a bad nonce problem is returned.
   214  // NOTE: this function assumes the JWS has already been verified with the
   215  // correct public key.
   216  func (wfe *WebFrontEndImpl) validNonce(ctx context.Context, header jose.Header) *probs.ProblemDetails {
   217  	if len(header.Nonce) == 0 {
   218  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingNonce"}).Inc()
   219  		return probs.BadNonce("JWS has no anti-replay nonce")
   220  	}
   221  	var valid bool
   222  	var err error
   223  	if wfe.noncePrefixMap == nil {
   224  		// Dispatch nonce redemption RPCs dynamically.
   225  		prob := nonceWellFormed(header.Nonce, nonce.PrefixLen)
   226  		if prob != nil {
   227  			wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMalformedNonce"}).Inc()
   228  			return prob
   229  		}
   230  
   231  		// Populate the context with the nonce prefix and HMAC key. These are
   232  		// used by a custom gRPC balancer, known as "noncebalancer", to route
   233  		// redemption RPCs to the backend that originally issued the nonce.
   234  		ctx = context.WithValue(ctx, nonce.PrefixCtxKey{}, header.Nonce[:nonce.PrefixLen])
   235  		ctx = context.WithValue(ctx, nonce.HMACKeyCtxKey{}, wfe.rncKey)
   236  
   237  		resp, err := wfe.rnc.Redeem(ctx, &noncepb.NonceMessage{Nonce: header.Nonce})
   238  		if err != nil {
   239  			rpcStatus, ok := status.FromError(err)
   240  			if !ok || rpcStatus != nb.ErrNoBackendsMatchPrefix {
   241  				return web.ProblemDetailsForError(err, "failed to redeem nonce")
   242  			}
   243  
   244  			// ErrNoBackendsMatchPrefix suggests that the nonce backend, which
   245  			// issued this nonce, is presently unreachable or unrecognized by
   246  			// this WFE. As this is a transient failure, the client should retry
   247  			// their request with a fresh nonce.
   248  			resp = &noncepb.ValidMessage{Valid: false}
   249  			wfe.stats.nonceNoMatchingBackendCount.Inc()
   250  		}
   251  		valid = resp.Valid
   252  	} else {
   253  		// Dispatch nonce redpemption RPCs using a static mapping.
   254  		//
   255  		// TODO(#6610) Remove code below and the `npm` mapping.
   256  		prob := nonceWellFormed(header.Nonce, nonce.DeprecatedPrefixLen)
   257  		if prob != nil {
   258  			wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMalformedNonce"}).Inc()
   259  			return prob
   260  		}
   261  
   262  		valid, err = nonce.RemoteRedeem(ctx, wfe.noncePrefixMap, header.Nonce)
   263  		if err != nil {
   264  			return web.ProblemDetailsForError(err, "failed to redeem nonce")
   265  		}
   266  	}
   267  	if !valid {
   268  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidNonce"}).Inc()
   269  		return probs.BadNonce(fmt.Sprintf("JWS has an invalid anti-replay nonce: %q", header.Nonce))
   270  	}
   271  	return nil
   272  }
   273  
   274  // validPOSTURL checks the JWS' URL header against the expected URL based on the
   275  // HTTP request. This prevents a JWS intended for one endpoint being replayed
   276  // against a different endpoint. If the URL isn't present, is invalid, or
   277  // doesn't match the HTTP request a problem is returned.
   278  func (wfe *WebFrontEndImpl) validPOSTURL(
   279  	request *http.Request,
   280  	header jose.Header) *probs.ProblemDetails {
   281  	extraHeaders := header.ExtraHeaders
   282  	// Check that there is at least one Extra Header
   283  	if len(extraHeaders) == 0 {
   284  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSNoExtraHeaders"}).Inc()
   285  		return probs.Malformed("JWS header parameter 'url' required")
   286  	}
   287  	// Try to read a 'url' Extra Header as a string
   288  	headerURL, ok := extraHeaders[jose.HeaderKey("url")].(string)
   289  	if !ok || len(headerURL) == 0 {
   290  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMissingURL"}).Inc()
   291  		return probs.Malformed("JWS header parameter 'url' required")
   292  	}
   293  	// Compute the URL we expect to be in the JWS based on the HTTP request
   294  	expectedURL := url.URL{
   295  		Scheme: requestProto(request),
   296  		Host:   request.Host,
   297  		Path:   request.RequestURI,
   298  	}
   299  	// Check that the URL we expect is the one that was found in the signed JWS
   300  	// header
   301  	if expectedURL.String() != headerURL {
   302  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMismatchedURL"}).Inc()
   303  		return probs.Malformed(fmt.Sprintf(
   304  			"JWS header parameter 'url' incorrect. Expected %q got %q",
   305  			expectedURL.String(), headerURL))
   306  	}
   307  	return nil
   308  }
   309  
   310  // matchJWSURLs checks two JWS' URL headers are equal. This is used during key
   311  // rollover to check that the inner JWS URL matches the outer JWS URL. If the
   312  // JWS URLs do not match a problem is returned.
   313  func (wfe *WebFrontEndImpl) matchJWSURLs(outer, inner jose.Header) *probs.ProblemDetails {
   314  	// Verify that the outer JWS has a non-empty URL header. This is strictly
   315  	// defensive since the expectation is that endpoints using `matchJWSURLs`
   316  	// have received at least one of their JWS from calling validPOSTForAccount(),
   317  	// which checks the outer JWS has the expected URL header before processing
   318  	// the inner JWS.
   319  	outerURL, ok := outer.ExtraHeaders[jose.HeaderKey("url")].(string)
   320  	if !ok || len(outerURL) == 0 {
   321  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverOuterJWSNoURL"}).Inc()
   322  		return probs.Malformed("Outer JWS header parameter 'url' required")
   323  	}
   324  
   325  	// Verify the inner JWS has a non-empty URL header.
   326  	innerURL, ok := inner.ExtraHeaders[jose.HeaderKey("url")].(string)
   327  	if !ok || len(innerURL) == 0 {
   328  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverInnerJWSNoURL"}).Inc()
   329  		return probs.Malformed("Inner JWS header parameter 'url' required")
   330  	}
   331  
   332  	// Verify that the outer URL matches the inner URL
   333  	if outerURL != innerURL {
   334  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverMismatchedURLs"}).Inc()
   335  		return probs.Malformed(fmt.Sprintf(
   336  			"Outer JWS 'url' value %q does not match inner JWS 'url' value %q",
   337  			outerURL, innerURL))
   338  	}
   339  
   340  	return nil
   341  }
   342  
   343  // bJSONWebSignature is a new distinct type which embeds the
   344  // *jose.JSONWebSignature concrete type. Callers must never create their own
   345  // bJSONWebSignature. Instead they should rely upon wfe.parseJWS instead.
   346  type bJSONWebSignature struct {
   347  	*jose.JSONWebSignature
   348  }
   349  
   350  // parseJWS extracts a JSONWebSignature from a byte slice. If there is an error
   351  // reading the JWS or it is unacceptable (e.g. too many/too few signatures,
   352  // presence of unprotected headers) a problem is returned, otherwise a
   353  // *bJSONWebSignature is returned.
   354  func (wfe *WebFrontEndImpl) parseJWS(body []byte) (*bJSONWebSignature, *probs.ProblemDetails) {
   355  	// Parse the raw JWS JSON to check that:
   356  	// * the unprotected Header field is not being used.
   357  	// * the "signatures" member isn't present, just "signature".
   358  	//
   359  	// This must be done prior to `jose.parseSigned` since it will strip away
   360  	// these headers.
   361  	var unprotected struct {
   362  		Header     map[string]string
   363  		Signatures []interface{}
   364  	}
   365  	err := json.Unmarshal(body, &unprotected)
   366  	if err != nil {
   367  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSUnmarshalFailed"}).Inc()
   368  		return nil, probs.Malformed("Parse error reading JWS")
   369  	}
   370  
   371  	// ACME v2 never uses values from the unprotected JWS header. Reject JWS that
   372  	// include unprotected headers.
   373  	if unprotected.Header != nil {
   374  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSUnprotectedHeaders"}).Inc()
   375  		return nil, probs.Malformed(
   376  			"JWS \"header\" field not allowed. All headers must be in \"protected\" field")
   377  	}
   378  
   379  	// ACME v2 never uses the "signatures" array of JSON serialized JWS, just the
   380  	// mandatory "signature" field. Reject JWS that include the "signatures" array.
   381  	if len(unprotected.Signatures) > 0 {
   382  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSMultiSig"}).Inc()
   383  		return nil, probs.Malformed(
   384  			"JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature")
   385  	}
   386  
   387  	// Parse the JWS using go-jose and enforce that the expected one non-empty
   388  	// signature is present in the parsed JWS.
   389  	bodyStr := string(body)
   390  	parsedJWS, err := jose.ParseSigned(bodyStr)
   391  	if err != nil {
   392  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSParseError"}).Inc()
   393  		return nil, probs.Malformed("Parse error reading JWS")
   394  	}
   395  	if len(parsedJWS.Signatures) > 1 {
   396  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSTooManySignatures"}).Inc()
   397  		return nil, probs.Malformed("Too many signatures in POST body")
   398  	}
   399  	if len(parsedJWS.Signatures) == 0 {
   400  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSNoSignatures"}).Inc()
   401  		return nil, probs.Malformed("POST JWS not signed")
   402  	}
   403  	if len(parsedJWS.Signatures) == 1 && len(parsedJWS.Signatures[0].Signature) == 0 {
   404  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSEmptySignature"}).Inc()
   405  		return nil, probs.Malformed("POST JWS not signed")
   406  	}
   407  
   408  	return &bJSONWebSignature{parsedJWS}, nil
   409  }
   410  
   411  // parseJWSRequest extracts a bJSONWebSignature from an HTTP POST request's body using parseJWS.
   412  func (wfe *WebFrontEndImpl) parseJWSRequest(request *http.Request) (*bJSONWebSignature, *probs.ProblemDetails) {
   413  	// Verify that the POST request has the expected headers
   414  	if prob := wfe.validPOSTRequest(request); prob != nil {
   415  		return nil, prob
   416  	}
   417  
   418  	// Read the POST request body's bytes. validPOSTRequest has already checked
   419  	// that the body is non-nil
   420  	bodyBytes, err := io.ReadAll(http.MaxBytesReader(nil, request.Body, maxRequestSize))
   421  	if err != nil {
   422  		if err.Error() == "http: request body too large" {
   423  			return nil, probs.Unauthorized("request body too large")
   424  		}
   425  		wfe.stats.httpErrorCount.With(prometheus.Labels{"type": "UnableToReadReqBody"}).Inc()
   426  		return nil, probs.ServerInternal("unable to read request body")
   427  	}
   428  
   429  	jws, prob := wfe.parseJWS(bodyBytes)
   430  	if prob != nil {
   431  		return nil, prob
   432  	}
   433  
   434  	return jws, nil
   435  }
   436  
   437  // extractJWK extracts a JWK from the protected headers of a bJSONWebSignature
   438  // or returns a problem. It expects that the JWS is using the embedded JWK style
   439  // of authentication and does not contain an embedded Key ID. Callers should
   440  // have acquired the headers from a bJSONWebSignature returned by parseJWS to
   441  // ensure it has the correct number of signatures present.
   442  func (wfe *WebFrontEndImpl) extractJWK(header jose.Header) (*jose.JSONWebKey, *probs.ProblemDetails) {
   443  	// extractJWK expects the request to be using an embedded JWK auth type and
   444  	// to not contain the mutually exclusive KeyID.
   445  	if prob := wfe.enforceJWSAuthType(header, embeddedJWK); prob != nil {
   446  		return nil, prob
   447  	}
   448  
   449  	// We can be sure that JSONWebKey is != nil because we have already called
   450  	// enforceJWSAuthType()
   451  	key := header.JSONWebKey
   452  
   453  	// If the key isn't considered valid by go-jose return a problem immediately
   454  	if !key.Valid() {
   455  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWKInvalid"}).Inc()
   456  		return nil, probs.Malformed("Invalid JWK in JWS header")
   457  	}
   458  
   459  	return key, nil
   460  }
   461  
   462  // acctIDFromURL extracts the numeric int64 account ID from a ACMEv1 or ACMEv2
   463  // account URL. If the acctURL has an invalid URL or the account ID in the
   464  // acctURL is non-numeric a MalformedProblem is returned.
   465  func (wfe *WebFrontEndImpl) acctIDFromURL(acctURL string, request *http.Request) (int64, *probs.ProblemDetails) {
   466  	// For normal ACME v2 accounts we expect the account URL has a prefix composed
   467  	// of the Host header and the acctPath.
   468  	expectedURLPrefix := web.RelativeEndpoint(request, acctPath)
   469  
   470  	// Process the acctURL to find only the trailing numeric account ID. Both the
   471  	// expected URL prefix and a legacy URL prefix are permitted in order to allow
   472  	// ACME v1 clients to use legacy accounts with unmodified account URLs for V2
   473  	// requests.
   474  	var accountIDStr string
   475  	if strings.HasPrefix(acctURL, expectedURLPrefix) {
   476  		accountIDStr = strings.TrimPrefix(acctURL, expectedURLPrefix)
   477  	} else if strings.HasPrefix(acctURL, wfe.LegacyKeyIDPrefix) {
   478  		accountIDStr = strings.TrimPrefix(acctURL, wfe.LegacyKeyIDPrefix)
   479  	} else {
   480  		return 0, probs.Malformed(
   481  			fmt.Sprintf("KeyID header contained an invalid account URL: %q", acctURL))
   482  	}
   483  
   484  	// Convert the raw account ID string to an int64 for use with the SA's
   485  	// GetRegistration RPC
   486  	accountID, err := strconv.ParseInt(accountIDStr, 10, 64)
   487  	if err != nil {
   488  		return 0, probs.Malformed("Malformed account ID in KeyID header URL: %q", acctURL)
   489  	}
   490  	return accountID, nil
   491  }
   492  
   493  // lookupJWK finds a JWK associated with the Key ID present in the provided
   494  // headers, returning the JWK and a pointer to the associated account, or a
   495  // problem. It expects that the JWS header is using the embedded Key ID style of
   496  // authentication and does not contain an embedded JWK. Callers should have
   497  // acquired headers from a bJSONWebSignature.
   498  func (wfe *WebFrontEndImpl) lookupJWK(
   499  	header jose.Header,
   500  	ctx context.Context,
   501  	request *http.Request,
   502  	logEvent *web.RequestEvent) (*jose.JSONWebKey, *core.Registration, *probs.ProblemDetails) {
   503  	// We expect the request to be using an embedded Key ID auth type and to not
   504  	// contain the mutually exclusive embedded JWK.
   505  	if prob := wfe.enforceJWSAuthType(header, embeddedKeyID); prob != nil {
   506  		return nil, nil, prob
   507  	}
   508  
   509  	accountURL := header.KeyID
   510  	accountID, prob := wfe.acctIDFromURL(accountURL, request)
   511  	if prob != nil {
   512  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSInvalidKeyID"}).Inc()
   513  		return nil, nil, prob
   514  	}
   515  
   516  	// Try to find the account for this account ID
   517  	account, err := wfe.accountGetter.GetRegistration(ctx, &sapb.RegistrationID{Id: accountID})
   518  	if err != nil {
   519  		// If the account isn't found, return a suitable problem
   520  		if errors.Is(err, berrors.NotFound) {
   521  			wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDNotFound"}).Inc()
   522  			return nil, nil, probs.AccountDoesNotExist(fmt.Sprintf(
   523  				"Account %q not found", accountURL))
   524  		}
   525  
   526  		// If there was an error and it isn't a "Not Found" error, return
   527  		// a ServerInternal problem since this is unexpected.
   528  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDLookupFailed"}).Inc()
   529  		// Add an error to the log event with the internal error message
   530  		logEvent.AddError("calling SA.GetRegistration: %s", err)
   531  		return nil, nil, web.ProblemDetailsForError(err, fmt.Sprintf("Error retrieving account %q", accountURL))
   532  	}
   533  
   534  	// Verify the account is not deactivated
   535  	if core.AcmeStatus(account.Status) != core.StatusValid {
   536  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSKeyIDAccountInvalid"}).Inc()
   537  		return nil, nil, probs.Unauthorized(
   538  			fmt.Sprintf("Account is not valid, has status %q", account.Status))
   539  	}
   540  
   541  	// Update the logEvent with the account information and return the JWK
   542  	logEvent.Requester = account.Id
   543  
   544  	acct, err := grpc.PbToRegistration(account)
   545  	if err != nil {
   546  		return nil, nil, probs.ServerInternal(fmt.Sprintf(
   547  			"Error unmarshalling account %q", accountURL))
   548  	}
   549  	return acct.Key, &acct, nil
   550  }
   551  
   552  // validJWSForKey checks a provided JWS for a given HTTP request validates
   553  // correctly using the provided JWK. If the JWS verifies the protected payload
   554  // is returned. The key/JWS algorithms are verified and
   555  // the JWK is checked against the keyPolicy before any signature validation is
   556  // done. If the JWS signature validates correctly then the JWS nonce value
   557  // and the JWS URL are verified to ensure that they are correct.
   558  func (wfe *WebFrontEndImpl) validJWSForKey(
   559  	ctx context.Context,
   560  	jws *bJSONWebSignature,
   561  	jwk *jose.JSONWebKey,
   562  	request *http.Request) ([]byte, *probs.ProblemDetails) {
   563  
   564  	// Check that the public key and JWS algorithms match expected
   565  	err := checkAlgorithm(jwk, jws.Signatures[0].Header)
   566  	if err != nil {
   567  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSAlgorithmCheckFailed"}).Inc()
   568  		return nil, probs.BadSignatureAlgorithm(err.Error())
   569  	}
   570  
   571  	// Verify the JWS signature with the public key.
   572  	// NOTE: It might seem insecure for the WFE to be trusted to verify
   573  	// client requests, i.e., that the verification should be done at the
   574  	// RA.  However the WFE is the RA's only view of the outside world
   575  	// *anyway*, so it could always lie about what key was used by faking
   576  	// the signature itself.
   577  	payload, err := jws.Verify(jwk)
   578  	if err != nil {
   579  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSVerifyFailed"}).Inc()
   580  		return nil, probs.Malformed("JWS verification error")
   581  	}
   582  
   583  	// Check that the JWS contains a correct Nonce header
   584  	if prob := wfe.validNonce(ctx, jws.Signatures[0].Header); prob != nil {
   585  		return nil, prob
   586  	}
   587  
   588  	// Check that the HTTP request URL matches the URL in the signed JWS
   589  	if prob := wfe.validPOSTURL(request, jws.Signatures[0].Header); prob != nil {
   590  		return nil, prob
   591  	}
   592  
   593  	// In the WFE1 package the check for the request URL required unmarshalling
   594  	// the payload JSON to check the "resource" field of the protected JWS body.
   595  	// This caught invalid JSON early and so we preserve this check by explicitly
   596  	// trying to unmarshal the payload (when it is non-empty to allow POST-as-GET
   597  	// behaviour) as part of the verification and failing early if it isn't valid JSON.
   598  	var parsedBody struct{}
   599  	err = json.Unmarshal(payload, &parsedBody)
   600  	if string(payload) != "" && err != nil {
   601  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWSBodyUnmarshalFailed"}).Inc()
   602  		return nil, probs.Malformed("Request payload did not parse as JSON")
   603  	}
   604  
   605  	return payload, nil
   606  }
   607  
   608  // validJWSForAccount checks that a given JWS is valid and verifies with the
   609  // public key associated to a known account specified by the JWS Key ID. If the
   610  // JWS is valid (e.g. the JWS is well formed, verifies with the JWK stored for the
   611  // specified key ID, specifies the correct URL, and has a valid nonce) then
   612  // `validJWSForAccount` returns the validated JWS body, the parsed
   613  // JSONWebSignature, and a pointer to the JWK's associated account. If any of
   614  // these conditions are not met or an error occurs only a problem is returned.
   615  func (wfe *WebFrontEndImpl) validJWSForAccount(
   616  	jws *bJSONWebSignature,
   617  	request *http.Request,
   618  	ctx context.Context,
   619  	logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, *probs.ProblemDetails) {
   620  	// Lookup the account and JWK for the key ID that authenticated the JWS
   621  	pubKey, account, prob := wfe.lookupJWK(jws.Signatures[0].Header, ctx, request, logEvent)
   622  	if prob != nil {
   623  		return nil, nil, nil, prob
   624  	}
   625  
   626  	// Verify the JWS with the JWK from the SA
   627  	payload, prob := wfe.validJWSForKey(ctx, jws, pubKey, request)
   628  	if prob != nil {
   629  		return nil, nil, nil, prob
   630  	}
   631  
   632  	return payload, jws, account, nil
   633  }
   634  
   635  // validPOSTForAccount checks that a given POST request has a valid JWS
   636  // using `validJWSForAccount`. If valid, the authenticated JWS body and the
   637  // registration that authenticated the body are returned. Otherwise a problem is
   638  // returned. The returned JWS body may be empty if the request is a POST-as-GET
   639  // request.
   640  func (wfe *WebFrontEndImpl) validPOSTForAccount(
   641  	request *http.Request,
   642  	ctx context.Context,
   643  	logEvent *web.RequestEvent) ([]byte, *bJSONWebSignature, *core.Registration, *probs.ProblemDetails) {
   644  	// Parse the JWS from the POST request
   645  	jws, prob := wfe.parseJWSRequest(request)
   646  	if prob != nil {
   647  		return nil, nil, nil, prob
   648  	}
   649  	return wfe.validJWSForAccount(jws, request, ctx, logEvent)
   650  }
   651  
   652  // validPOSTAsGETForAccount checks that a given POST request is valid using
   653  // `validPOSTForAccount`. It additionally validates that the JWS request payload
   654  // is empty, indicating that it is a POST-as-GET request per ACME draft 15+
   655  // section 6.3 "GET and POST-as-GET requests". If a non empty payload is
   656  // provided in the JWS the invalidPOSTAsGETErr problem is returned. This
   657  // function is useful only for endpoints that do not need to handle both POSTs
   658  // with a body and POST-as-GET requests (e.g. Order, Certificate).
   659  func (wfe *WebFrontEndImpl) validPOSTAsGETForAccount(
   660  	request *http.Request,
   661  	ctx context.Context,
   662  	logEvent *web.RequestEvent) (*core.Registration, *probs.ProblemDetails) {
   663  	// Call validPOSTForAccount to verify the JWS and extract the body.
   664  	body, _, reg, prob := wfe.validPOSTForAccount(request, ctx, logEvent)
   665  	if prob != nil {
   666  		return nil, prob
   667  	}
   668  	// Verify the POST-as-GET payload is empty
   669  	if string(body) != "" {
   670  		return nil, probs.Malformed("POST-as-GET requests must have an empty payload")
   671  	}
   672  	// To make log analysis easier we choose to elevate the pseudo ACME HTTP
   673  	// method "POST-as-GET" to the logEvent's Method, replacing the
   674  	// http.MethodPost value.
   675  	logEvent.Method = "POST-as-GET"
   676  	return reg, prob
   677  }
   678  
   679  // validSelfAuthenticatedJWS checks that a given JWS verifies with the JWK
   680  // embedded in the JWS itself (e.g. self-authenticated). This type of JWS
   681  // is only used for creating new accounts or revoking a certificate by signing
   682  // the request with the private key corresponding to the certificate's public
   683  // key and embedding that public key in the JWS. All other request should be
   684  // validated using `validJWSforAccount`.
   685  // If the JWS validates (e.g. the JWS is well formed, verifies with the JWK
   686  // embedded in it, has the correct URL, and includes a valid nonce) then
   687  // `validSelfAuthenticatedJWS` returns the validated JWS body and the JWK that
   688  // was embedded in the JWS. Otherwise if the valid JWS conditions are not met or
   689  // an error occurs only a problem is returned.
   690  // Note that this function does *not* enforce that the JWK abides by our goodkey
   691  // policies. This is because this method is used by the RevokeCertificate path,
   692  // which must allow JWKs which are signed by blocklisted (i.e. already revoked
   693  // due to compromise) keys, in case multiple clients attempt to revoke the same
   694  // cert.
   695  func (wfe *WebFrontEndImpl) validSelfAuthenticatedJWS(
   696  	ctx context.Context,
   697  	jws *bJSONWebSignature,
   698  	request *http.Request) ([]byte, *jose.JSONWebKey, *probs.ProblemDetails) {
   699  	// Extract the embedded JWK from the parsed protected JWS' headers
   700  	pubKey, prob := wfe.extractJWK(jws.Signatures[0].Header)
   701  	if prob != nil {
   702  		return nil, nil, prob
   703  	}
   704  
   705  	// Verify the JWS with the embedded JWK
   706  	payload, prob := wfe.validJWSForKey(ctx, jws, pubKey, request)
   707  	if prob != nil {
   708  		return nil, nil, prob
   709  	}
   710  
   711  	return payload, pubKey, nil
   712  }
   713  
   714  // validSelfAuthenticatedPOST checks that a given POST request has a valid JWS
   715  // using `validSelfAuthenticatedJWS`. It enforces that the JWK abides by our
   716  // goodkey policies (key algorithm, length, blocklist, etc).
   717  func (wfe *WebFrontEndImpl) validSelfAuthenticatedPOST(
   718  	ctx context.Context,
   719  	request *http.Request) ([]byte, *jose.JSONWebKey, *probs.ProblemDetails) {
   720  	// Parse the JWS from the POST request
   721  	jws, prob := wfe.parseJWSRequest(request)
   722  	if prob != nil {
   723  		return nil, nil, prob
   724  	}
   725  
   726  	// Extract and validate the embedded JWK from the parsed JWS
   727  	payload, pubKey, prob := wfe.validSelfAuthenticatedJWS(ctx, jws, request)
   728  	if prob != nil {
   729  		return nil, nil, prob
   730  	}
   731  
   732  	// If the key doesn't meet the GoodKey policy return a problem
   733  	err := wfe.keyPolicy.GoodKey(ctx, pubKey.Key)
   734  	if err != nil {
   735  		if errors.Is(err, goodkey.ErrBadKey) {
   736  			wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "JWKRejectedByGoodKey"}).Inc()
   737  			return nil, nil, probs.BadPublicKey(err.Error())
   738  		}
   739  		return nil, nil, probs.ServerInternal("error checking key quality")
   740  	}
   741  
   742  	return payload, pubKey, nil
   743  }
   744  
   745  // rolloverRequest is a client request to change the key for the account ID
   746  // provided from the specified old key to a new key (the embedded JWK in the
   747  // inner JWS).
   748  type rolloverRequest struct {
   749  	OldKey  jose.JSONWebKey
   750  	Account string
   751  }
   752  
   753  // rolloverOperation is a struct representing a requested rollover operation
   754  // from the specified old key to the new key for the given account ID.
   755  type rolloverOperation struct {
   756  	rolloverRequest
   757  	NewKey jose.JSONWebKey
   758  }
   759  
   760  // validKeyRollover checks if the innerJWS is a valid key rollover operation
   761  // given the outer JWS that carried it. It is assumed that the outerJWS has
   762  // already been validated per the normal ACME process using `validPOSTForAccount`.
   763  // It is *critical* this is the case since `validKeyRollover` does not check the
   764  // outerJWS signature. This function checks that:
   765  // 1) the inner JWS is valid and well formed
   766  // 2) the inner JWS has the same "url" header as the outer JWS
   767  // 3) the inner JWS is self-authenticated with an embedded JWK
   768  //
   769  // This function verifies that the inner JWS' body is a rolloverRequest instance
   770  // that specifies the correct oldKey. The returned rolloverOperation's NewKey
   771  // field will be set to the JWK from the inner JWS.
   772  //
   773  // If the request is valid a *rolloverOperation object is returned,
   774  // otherwise a problem is returned. The caller is left to verify
   775  // whether the new key is appropriate (e.g. isn't being used by another existing
   776  // account) and that the account field of the rollover object matches the
   777  // account that verified the outer JWS.
   778  func (wfe *WebFrontEndImpl) validKeyRollover(
   779  	ctx context.Context,
   780  	outerJWS *bJSONWebSignature,
   781  	innerJWS *bJSONWebSignature,
   782  	oldKey *jose.JSONWebKey) (*rolloverOperation, *probs.ProblemDetails) {
   783  
   784  	// Extract the embedded JWK from the inner JWS' protected headers
   785  	jwk, prob := wfe.extractJWK(innerJWS.Signatures[0].Header)
   786  	if prob != nil {
   787  		return nil, prob
   788  	}
   789  
   790  	// If the key doesn't meet the GoodKey policy return a problem immediately
   791  	err := wfe.keyPolicy.GoodKey(ctx, jwk.Key)
   792  	if err != nil {
   793  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverJWKRejectedByGoodKey"}).Inc()
   794  		return nil, probs.BadPublicKey(err.Error())
   795  	}
   796  
   797  	// Check that the public key and JWS algorithms match expected
   798  	err = checkAlgorithm(jwk, innerJWS.Signatures[0].Header)
   799  	if err != nil {
   800  		return nil, probs.Malformed(err.Error())
   801  	}
   802  
   803  	// Verify the inner JWS signature with the public key from the embedded JWK.
   804  	// NOTE(@cpu): We do not use `wfe.validJWSForKey` here because the inner JWS
   805  	// of a key rollover operation is special (e.g. has no nonce, doesn't have an
   806  	// HTTP request to match the URL to)
   807  	innerPayload, err := innerJWS.Verify(jwk)
   808  	if err != nil {
   809  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverJWSVerifyFailed"}).Inc()
   810  		return nil, probs.Malformed("Inner JWS does not verify with embedded JWK")
   811  	}
   812  	// NOTE(@cpu): we do not stomp the web.RequestEvent's payload here since that is set
   813  	// from the outerJWS in validPOSTForAccount and contains the inner JWS and inner
   814  	// payload already.
   815  
   816  	// Verify that the outer and inner JWS protected URL headers match
   817  	if prob := wfe.matchJWSURLs(outerJWS.Signatures[0].Header, innerJWS.Signatures[0].Header); prob != nil {
   818  		return nil, prob
   819  	}
   820  
   821  	var req rolloverRequest
   822  	if json.Unmarshal(innerPayload, &req) != nil {
   823  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverUnmarshalFailed"}).Inc()
   824  		return nil, probs.Malformed(
   825  			"Inner JWS payload did not parse as JSON key rollover object")
   826  	}
   827  
   828  	// If there's no oldkey specified fail before trying to use
   829  	// core.PublicKeyEqual on a nil argument.
   830  	if req.OldKey.Key == nil {
   831  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverWrongOldKey"}).Inc()
   832  		return nil, probs.Malformed("Inner JWS does not contain old key field matching current account key")
   833  	}
   834  
   835  	// We must validate that the inner JWS' rollover request specifies the correct
   836  	// oldKey.
   837  	if keysEqual, err := core.PublicKeysEqual(req.OldKey.Key, oldKey.Key); err != nil {
   838  		return nil, probs.Malformed("Unable to compare new and old keys: %s", err.Error())
   839  	} else if !keysEqual {
   840  		wfe.stats.joseErrorCount.With(prometheus.Labels{"type": "KeyRolloverWrongOldKey"}).Inc()
   841  		return nil, probs.Malformed("Inner JWS does not contain old key field matching current account key")
   842  	}
   843  
   844  	// Return a rolloverOperation populated with the validated old JWK, the
   845  	// requested account, and the new JWK extracted from the inner JWS.
   846  	return &rolloverOperation{
   847  		rolloverRequest: rolloverRequest{
   848  			OldKey:  *oldKey,
   849  			Account: req.Account,
   850  		},
   851  		NewKey: *jwk,
   852  	}, nil
   853  }
   854  

View as plain text