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