github.com/letsencrypt/boulder@v0.20251208.0/wfe2/verify_test.go (about) 1 package wfe2 2 3 import ( 4 "context" 5 "crypto" 6 "crypto/dsa" 7 "crypto/ecdsa" 8 "crypto/elliptic" 9 "crypto/rsa" 10 "errors" 11 "fmt" 12 "net/http" 13 "slices" 14 "strings" 15 "testing" 16 17 "github.com/go-jose/go-jose/v4" 18 "github.com/prometheus/client_golang/prometheus" 19 "google.golang.org/grpc" 20 21 "github.com/letsencrypt/boulder/core" 22 corepb "github.com/letsencrypt/boulder/core/proto" 23 berrors "github.com/letsencrypt/boulder/errors" 24 "github.com/letsencrypt/boulder/goodkey" 25 bgrpc "github.com/letsencrypt/boulder/grpc" 26 "github.com/letsencrypt/boulder/grpc/noncebalancer" 27 noncepb "github.com/letsencrypt/boulder/nonce/proto" 28 sapb "github.com/letsencrypt/boulder/sa/proto" 29 "github.com/letsencrypt/boulder/test" 30 "github.com/letsencrypt/boulder/web" 31 ) 32 33 // sigAlgForKey uses `signatureAlgorithmForKey` but fails immediately using the 34 // testing object if the sig alg is unknown. 35 func sigAlgForKey(t *testing.T, key any) jose.SignatureAlgorithm { 36 var sigAlg jose.SignatureAlgorithm 37 var err error 38 // Gracefully handle the case where a non-pointer public key is given where 39 // sigAlgorithmForKey always wants a pointer. It may be tempting to try and do 40 // `sigAlgorithmForKey(&jose.JSONWebKey{Key: &key})` without a type switch but this produces 41 // `*interface {}` and not the desired `*rsa.PublicKey` or `*ecdsa.PublicKey`. 42 switch k := key.(type) { 43 case rsa.PublicKey: 44 sigAlg, err = sigAlgorithmForKey(&jose.JSONWebKey{Key: &k}) 45 case ecdsa.PublicKey: 46 sigAlg, err = sigAlgorithmForKey(&jose.JSONWebKey{Key: &k}) 47 default: 48 sigAlg, err = sigAlgorithmForKey(&jose.JSONWebKey{Key: k}) 49 } 50 test.Assert(t, err == nil, fmt.Sprintf("Error getting signature algorithm for key %#v", key)) 51 return sigAlg 52 } 53 54 // keyAlgForKey returns a JWK key algorithm based on the provided private key. 55 // Only ECDSA and RSA private keys are supported. 56 func keyAlgForKey(t *testing.T, key any) string { 57 switch key.(type) { 58 case *rsa.PrivateKey, rsa.PrivateKey: 59 return "RSA" 60 case *ecdsa.PrivateKey, ecdsa.PrivateKey: 61 return "ECDSA" 62 } 63 t.Fatalf("Can't figure out keyAlgForKey: %#v", key) 64 return "" 65 } 66 67 // pubKeyForKey returns the public key of an RSA/ECDSA private key provided as 68 // argument. 69 func pubKeyForKey(t *testing.T, privKey any) any { 70 switch k := privKey.(type) { 71 case *rsa.PrivateKey: 72 return k.PublicKey 73 case *ecdsa.PrivateKey: 74 return k.PublicKey 75 } 76 t.Fatalf("Unable to get public key for private key %#v", privKey) 77 return nil 78 } 79 80 // requestSigner offers methods to sign requests that will be accepted by a 81 // specific WFE in unittests. It is only valid for the lifetime of a single 82 // unittest. 83 type requestSigner struct { 84 t *testing.T 85 nonceService jose.NonceSource 86 } 87 88 // embeddedJWK creates a JWS for a given request body with an embedded JWK 89 // corresponding to the private key provided. The URL and nonce extra headers 90 // are set based on the additional arguments. A computed JWS, the corresponding 91 // embedded JWK and the JWS in serialized string form are returned. 92 func (rs requestSigner) embeddedJWK( 93 privateKey any, 94 url string, 95 req string) (*jose.JSONWebSignature, *jose.JSONWebKey, string) { 96 // if no key is provided default to test1KeyPrivatePEM 97 var publicKey any 98 if privateKey == nil { 99 signer := loadKey(rs.t, []byte(test1KeyPrivatePEM)) 100 privateKey = signer 101 publicKey = signer.Public() 102 } else { 103 publicKey = pubKeyForKey(rs.t, privateKey) 104 } 105 106 signerKey := jose.SigningKey{ 107 Key: privateKey, 108 Algorithm: sigAlgForKey(rs.t, publicKey), 109 } 110 111 opts := &jose.SignerOptions{ 112 NonceSource: rs.nonceService, 113 EmbedJWK: true, 114 } 115 if url != "" { 116 opts.ExtraHeaders = map[jose.HeaderKey]any{ 117 "url": url, 118 } 119 } 120 121 signer, err := jose.NewSigner(signerKey, opts) 122 test.AssertNotError(rs.t, err, "Failed to make signer") 123 124 jws, err := signer.Sign([]byte(req)) 125 test.AssertNotError(rs.t, err, "Failed to sign req") 126 127 body := jws.FullSerialize() 128 parsedJWS, err := jose.ParseSigned(body, getSupportedAlgs()) 129 test.AssertNotError(rs.t, err, "Failed to parse generated JWS") 130 131 return parsedJWS, parsedJWS.Signatures[0].Header.JSONWebKey, body 132 } 133 134 // signRequestKeyID creates a JWS for a given request body with key ID specified 135 // based on the ID number provided. The URL and nonce extra headers 136 // are set based on the additional arguments. A computed JWS, the corresponding 137 // embedded JWK and the JWS in serialized string form are returned. 138 func (rs requestSigner) byKeyID( 139 keyID int64, 140 privateKey any, 141 url string, 142 req string) (*jose.JSONWebSignature, *jose.JSONWebKey, string) { 143 // if no key is provided default to test1KeyPrivatePEM 144 if privateKey == nil { 145 privateKey = loadKey(rs.t, []byte(test1KeyPrivatePEM)) 146 } 147 148 jwk := &jose.JSONWebKey{ 149 Key: privateKey, 150 Algorithm: keyAlgForKey(rs.t, privateKey), 151 KeyID: fmt.Sprintf("http://localhost/acme/acct/%d", keyID), 152 } 153 154 signerKey := jose.SigningKey{ 155 Key: jwk, 156 Algorithm: jose.RS256, 157 } 158 159 opts := &jose.SignerOptions{ 160 NonceSource: rs.nonceService, 161 ExtraHeaders: map[jose.HeaderKey]any{ 162 "url": url, 163 }, 164 } 165 166 signer, err := jose.NewSigner(signerKey, opts) 167 test.AssertNotError(rs.t, err, "Failed to make signer") 168 jws, err := signer.Sign([]byte(req)) 169 test.AssertNotError(rs.t, err, "Failed to sign req") 170 171 body := jws.FullSerialize() 172 parsedJWS, err := jose.ParseSigned(body, getSupportedAlgs()) 173 test.AssertNotError(rs.t, err, "Failed to parse generated JWS") 174 175 return parsedJWS, jwk, body 176 } 177 178 func (rs requestSigner) makeJWS(ns jose.NonceSource) *jose.JSONWebSignature { 179 privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM)) 180 jwk := &jose.JSONWebKey{ 181 Key: privateKey, 182 Algorithm: keyAlgForKey(rs.t, privateKey), 183 KeyID: "http://localhost/acme/acct/1", 184 } 185 signerKey := jose.SigningKey{ 186 Key: jwk, 187 Algorithm: jose.RS256, 188 } 189 190 opts := &jose.SignerOptions{ 191 NonceSource: ns, 192 ExtraHeaders: map[jose.HeaderKey]any{ 193 "url": "https://example.com/acme/foo", 194 }, 195 } 196 197 signer, err := jose.NewSigner(signerKey, opts) 198 test.AssertNotError(rs.t, err, "Failed to make signer") 199 jws, err := signer.Sign([]byte("")) 200 test.AssertNotError(rs.t, err, "Failed to sign req") 201 202 body := jws.FullSerialize() 203 parsedJWS, err := jose.ParseSigned(body, getSupportedAlgs()) 204 test.AssertNotError(rs.t, err, "Failed to parse generated JWS") 205 206 return parsedJWS 207 } 208 209 func TestRejectsNone(t *testing.T) { 210 noneJWSBody := ` 211 { 212 "header": { 213 "alg": "none", 214 "jwk": { 215 "kty": "RSA", 216 "n": "vrjT", 217 "e": "AQAB" 218 } 219 }, 220 "payload": "aGkK", 221 "signature": "ghTIjrhiRl2pQ09vAkUUBbF5KziJdhzOTB-okM9SPRzU8Hyj0W1H5JA1Zoc-A-LuJGNAtYYHWqMw1SeZbT0l9FHcbMPeWDaJNkHS9jz5_g_Oyol8vcrWur2GDtB2Jgw6APtZKrbuGATbrF7g41Wijk6Kk9GXDoCnlfOQOhHhsrFFcWlCPLG-03TtKD6EBBoVBhmlp8DRLs7YguWRZ6jWNaEX-1WiRntBmhLqoqQFtvZxCBw_PRuaRw_RZBd1x2_BNYqEdOmVNC43UHMSJg3y_3yrPo905ur09aUTscf-C_m4Sa4M0FuDKn3bQ_pFrtz-aCCq6rcTIyxYpDqNvHMT2Q" 222 } 223 ` 224 _, err := jose.ParseSigned(noneJWSBody, getSupportedAlgs()) 225 test.AssertError(t, err, "Should not have been able to parse 'none' algorithm") 226 } 227 228 func TestRejectsHS256(t *testing.T) { 229 hs256JWSBody := ` 230 { 231 "header": { 232 "alg": "HS256", 233 "jwk": { 234 "kty": "RSA", 235 "n": "vrjT", 236 "e": "AQAB" 237 } 238 }, 239 "payload": "aGkK", 240 "signature": "ghTIjrhiRl2pQ09vAkUUBbF5KziJdhzOTB-okM9SPRzU8Hyj0W1H5JA1Zoc-A-LuJGNAtYYHWqMw1SeZbT0l9FHcbMPeWDaJNkHS9jz5_g_Oyol8vcrWur2GDtB2Jgw6APtZKrbuGATbrF7g41Wijk6Kk9GXDoCnlfOQOhHhsrFFcWlCPLG-03TtKD6EBBoVBhmlp8DRLs7YguWRZ6jWNaEX-1WiRntBmhLqoqQFtvZxCBw_PRuaRw_RZBd1x2_BNYqEdOmVNC43UHMSJg3y_3yrPo905ur09aUTscf-C_m4Sa4M0FuDKn3bQ_pFrtz-aCCq6rcTIyxYpDqNvHMT2Q" 241 } 242 ` 243 244 _, err := jose.ParseSigned(hs256JWSBody, getSupportedAlgs()) 245 fmt.Println(err) 246 test.AssertError(t, err, "Parsed hs256JWSBody, but should not have") 247 } 248 249 func TestCheckAlgorithm(t *testing.T) { 250 testCases := []struct { 251 key jose.JSONWebKey 252 jws jose.JSONWebSignature 253 expectedErr string 254 }{ 255 { 256 jose.JSONWebKey{}, 257 jose.JSONWebSignature{ 258 Signatures: []jose.Signature{ 259 { 260 Header: jose.Header{ 261 Algorithm: "RS256", 262 }, 263 }, 264 }, 265 }, 266 "JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)", 267 }, 268 { 269 jose.JSONWebKey{ 270 Algorithm: "HS256", 271 Key: &rsa.PublicKey{}, 272 }, 273 jose.JSONWebSignature{ 274 Signatures: []jose.Signature{ 275 { 276 Header: jose.Header{ 277 Algorithm: "HS256", 278 }, 279 }, 280 }, 281 }, 282 "JWS signature header contains unsupported algorithm \"HS256\", expected one of [RS256 ES256 ES384 ES512]", 283 }, 284 { 285 jose.JSONWebKey{ 286 Algorithm: "ES256", 287 Key: &dsa.PublicKey{}, 288 }, 289 jose.JSONWebSignature{ 290 Signatures: []jose.Signature{ 291 { 292 Header: jose.Header{ 293 Algorithm: "ES512", 294 }, 295 }, 296 }, 297 }, 298 "JWK contains unsupported key type (expected RSA, or ECDSA P-256, P-384, or P-521)", 299 }, 300 { 301 jose.JSONWebKey{ 302 Algorithm: "RS256", 303 Key: &rsa.PublicKey{}, 304 }, 305 jose.JSONWebSignature{ 306 Signatures: []jose.Signature{ 307 { 308 Header: jose.Header{ 309 Algorithm: "ES512", 310 }, 311 }, 312 }, 313 }, 314 "JWS signature header algorithm \"ES512\" does not match expected algorithm \"RS256\" for JWK", 315 }, 316 { 317 jose.JSONWebKey{ 318 Algorithm: "HS256", 319 Key: &rsa.PublicKey{}, 320 }, 321 jose.JSONWebSignature{ 322 Signatures: []jose.Signature{ 323 { 324 Header: jose.Header{ 325 Algorithm: "RS256", 326 }, 327 }, 328 }, 329 }, 330 "JWK key header algorithm \"HS256\" does not match expected algorithm \"RS256\" for JWK", 331 }, 332 } 333 for i, tc := range testCases { 334 err := checkAlgorithm(&tc.key, tc.jws.Signatures[0].Header) 335 if tc.expectedErr != "" && err.Error() != tc.expectedErr { 336 t.Errorf("TestCheckAlgorithm %d: Expected %q, got %q", i, tc.expectedErr, err) 337 } 338 } 339 } 340 341 func TestCheckAlgorithmSuccess(t *testing.T) { 342 jwsRS256 := &jose.JSONWebSignature{ 343 Signatures: []jose.Signature{ 344 { 345 Header: jose.Header{ 346 Algorithm: "RS256", 347 }, 348 }, 349 }, 350 } 351 goodJSONWebKeyRS256 := &jose.JSONWebKey{ 352 Algorithm: "RS256", 353 Key: &rsa.PublicKey{}, 354 } 355 err := checkAlgorithm(goodJSONWebKeyRS256, jwsRS256.Signatures[0].Header) 356 test.AssertNotError(t, err, "RS256 key: Expected nil error") 357 358 badJSONWebKeyRS256 := &jose.JSONWebKey{ 359 Algorithm: "ObviouslyWrongButNotZeroValue", 360 Key: &rsa.PublicKey{}, 361 } 362 err = checkAlgorithm(badJSONWebKeyRS256, jwsRS256.Signatures[0].Header) 363 test.AssertError(t, err, "RS256 key: Expected nil error") 364 test.AssertContains(t, err.Error(), "JWK key header algorithm \"ObviouslyWrongButNotZeroValue\" does not match expected algorithm \"RS256\" for JWK") 365 366 jwsES256 := &jose.JSONWebSignature{ 367 Signatures: []jose.Signature{ 368 { 369 Header: jose.Header{ 370 Algorithm: "ES256", 371 }, 372 }, 373 }, 374 } 375 goodJSONWebKeyES256 := &jose.JSONWebKey{ 376 Algorithm: "ES256", 377 Key: &ecdsa.PublicKey{ 378 Curve: elliptic.P256(), 379 }, 380 } 381 err = checkAlgorithm(goodJSONWebKeyES256, jwsES256.Signatures[0].Header) 382 test.AssertNotError(t, err, "ES256 key: Expected nil error") 383 384 badJSONWebKeyES256 := &jose.JSONWebKey{ 385 Algorithm: "ObviouslyWrongButNotZeroValue", 386 Key: &ecdsa.PublicKey{ 387 Curve: elliptic.P256(), 388 }, 389 } 390 err = checkAlgorithm(badJSONWebKeyES256, jwsES256.Signatures[0].Header) 391 test.AssertError(t, err, "ES256 key: Expected nil error") 392 test.AssertContains(t, err.Error(), "JWK key header algorithm \"ObviouslyWrongButNotZeroValue\" does not match expected algorithm \"ES256\" for JWK") 393 } 394 395 func TestValidPOSTRequest(t *testing.T) { 396 wfe, _, _ := setupWFE(t) 397 398 dummyContentLength := []string{"pretty long, idk, maybe a nibble or two?"} 399 400 testCases := []struct { 401 Name string 402 Headers map[string][]string 403 Body *string 404 HTTPStatus int 405 ErrorDetail string 406 ErrorStatType string 407 EnforceContentType bool 408 }{ 409 // POST requests with a Replay-Nonce header should produce a problem 410 { 411 Name: "POST with a Replay-Nonce HTTP header", 412 Headers: map[string][]string{ 413 "Content-Length": dummyContentLength, 414 "Replay-Nonce": {"ima-misplaced-nonce"}, 415 "Content-Type": {expectedJWSContentType}, 416 }, 417 HTTPStatus: http.StatusBadRequest, 418 ErrorDetail: "HTTP requests should NOT contain Replay-Nonce header. Use JWS nonce field", 419 ErrorStatType: "ReplayNonceOutsideJWS", 420 }, 421 // POST requests without a body should produce a problem 422 { 423 Name: "POST with an empty POST body", 424 Headers: map[string][]string{ 425 "Content-Length": dummyContentLength, 426 "Content-Type": {expectedJWSContentType}, 427 }, 428 HTTPStatus: http.StatusBadRequest, 429 ErrorDetail: "No body on POST", 430 ErrorStatType: "NoPOSTBody", 431 }, 432 { 433 Name: "POST without a Content-Type header", 434 Headers: map[string][]string{ 435 "Content-Length": dummyContentLength, 436 }, 437 HTTPStatus: http.StatusUnsupportedMediaType, 438 ErrorDetail: fmt.Sprintf( 439 "No Content-Type header on POST. Content-Type must be %q", 440 expectedJWSContentType), 441 ErrorStatType: "NoContentType", 442 EnforceContentType: true, 443 }, 444 { 445 Name: "POST with an invalid Content-Type header", 446 Headers: map[string][]string{ 447 "Content-Length": dummyContentLength, 448 "Content-Type": {"fresh.and.rare"}, 449 }, 450 HTTPStatus: http.StatusUnsupportedMediaType, 451 ErrorDetail: fmt.Sprintf( 452 "Invalid Content-Type header on POST. Content-Type must be %q", 453 expectedJWSContentType), 454 ErrorStatType: "WrongContentType", 455 EnforceContentType: true, 456 }, 457 } 458 459 for _, tc := range testCases { 460 input := &http.Request{ 461 Method: "POST", 462 URL: mustParseURL("/"), 463 Header: tc.Headers, 464 } 465 t.Run(tc.Name, func(t *testing.T) { 466 err := wfe.validPOSTRequest(input) 467 test.AssertError(t, err, "No error returned for invalid POST") 468 test.AssertErrorIs(t, err, berrors.Malformed) 469 test.AssertContains(t, err.Error(), tc.ErrorDetail) 470 test.AssertMetricWithLabelsEquals( 471 t, wfe.stats.httpErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) 472 }) 473 } 474 } 475 476 func TestEnforceJWSAuthType(t *testing.T) { 477 wfe, _, signer := setupWFE(t) 478 479 testKeyIDJWS, _, _ := signer.byKeyID(1, nil, "", "") 480 testEmbeddedJWS, _, _ := signer.embeddedJWK(nil, "", "") 481 482 // A hand crafted JWS that has both a Key ID and an embedded JWK 483 conflictJWSBody := ` 484 { 485 "header": { 486 "alg": "RS256", 487 "jwk": { 488 "e": "AQAB", 489 "kty": "RSA", 490 "n": "ppbqGaMFnnq9TeMUryR6WW4Lr5WMgp46KlBXZkNaGDNQoifWt6LheeR5j9MgYkIFU7Z8Jw5-bpJzuBeEVwb-yHGh4Umwo_qKtvAJd44iLjBmhBSxq-OSe6P5hX1LGCByEZlYCyoy98zOtio8VK_XyS5VoOXqchCzBXYf32ksVUTrtH1jSlamKHGz0Q0pRKIsA2fLqkE_MD3jP6wUDD6ExMw_tKYLx21lGcK41WSrRpDH-kcZo1QdgCy2ceNzaliBX1eHmKG0-H8tY4tPQudk-oHQmWTdvUIiHO6gSKMGDZNWv6bq74VTCsRfUEAkuWhqUhgRSGzlvlZ24wjHv5Qdlw" 491 } 492 }, 493 "protected": "eyJub25jZSI6ICJibTl1WTJVIiwgInVybCI6ICJodHRwOi8vbG9jYWxob3N0L3Rlc3QiLCAia2lkIjogInRlc3RrZXkifQ", 494 "payload": "Zm9v", 495 "signature": "ghTIjrhiRl2pQ09vAkUUBbF5KziJdhzOTB-okM9SPRzU8Hyj0W1H5JA1Zoc-A-LuJGNAtYYHWqMw1SeZbT0l9FHcbMPeWDaJNkHS9jz5_g_Oyol8vcrWur2GDtB2Jgw6APtZKrbuGATbrF7g41Wijk6Kk9GXDoCnlfOQOhHhsrFFcWlCPLG-03TtKD6EBBoVBhmlp8DRLs7YguWRZ6jWNaEX-1WiRntBmhLqoqQFtvZxCBw_PRuaRw_RZBd1x2_BNYqEdOmVNC43UHMSJg3y_3yrPo905ur09aUTscf-C_m4Sa4M0FuDKn3bQ_pFrtz-aCCq6rcTIyxYpDqNvHMT2Q" 496 } 497 ` 498 499 conflictJWS, err := jose.ParseSigned(conflictJWSBody, getSupportedAlgs()) 500 if err != nil { 501 t.Fatal("Unable to parse conflict JWS") 502 } 503 504 testCases := []struct { 505 Name string 506 JWS *jose.JSONWebSignature 507 AuthType jwsAuthType 508 WantErrType berrors.ErrorType 509 WantErrDetail string 510 WantStatType string 511 }{ 512 { 513 Name: "Key ID and embedded JWS", 514 JWS: conflictJWS, 515 AuthType: invalidAuthType, 516 WantErrType: berrors.Malformed, 517 WantErrDetail: "jwk and kid header fields are mutually exclusive", 518 WantStatType: "JWSAuthTypeInvalid", 519 }, 520 { 521 Name: "Key ID when expected is embedded JWK", 522 JWS: testKeyIDJWS, 523 AuthType: embeddedJWK, 524 WantErrType: berrors.Malformed, 525 WantErrDetail: "No embedded JWK in JWS header", 526 WantStatType: "JWSAuthTypeWrong", 527 }, 528 { 529 Name: "Embedded JWK when expected is Key ID", 530 JWS: testEmbeddedJWS, 531 AuthType: embeddedKeyID, 532 WantErrType: berrors.Malformed, 533 WantErrDetail: "No Key ID in JWS header", 534 WantStatType: "JWSAuthTypeWrong", 535 }, 536 { 537 Name: "Key ID when expected is KeyID", 538 JWS: testKeyIDJWS, 539 AuthType: embeddedKeyID, 540 }, 541 { 542 Name: "Embedded JWK when expected is embedded JWK", 543 JWS: testEmbeddedJWS, 544 AuthType: embeddedJWK, 545 }, 546 } 547 548 for _, tc := range testCases { 549 t.Run(tc.Name, func(t *testing.T) { 550 wfe.stats.joseErrorCount.Reset() 551 in := tc.JWS.Signatures[0].Header 552 553 gotErr := wfe.enforceJWSAuthType(in, tc.AuthType) 554 if tc.WantErrDetail == "" { 555 if gotErr != nil { 556 t.Fatalf("enforceJWSAuthType(%#v, %#v) = %#v, want nil", in, tc.AuthType, gotErr) 557 } 558 } else { 559 berr, ok := gotErr.(*berrors.BoulderError) 560 if !ok { 561 t.Fatalf("enforceJWSAuthType(%#v, %#v) returned %T, want BoulderError", in, tc.AuthType, gotErr) 562 } 563 if berr.Type != tc.WantErrType { 564 t.Errorf("enforceJWSAuthType(%#v, %#v) = %#v, want %#v", in, tc.AuthType, berr.Type, tc.WantErrType) 565 } 566 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 567 t.Errorf("enforceJWSAuthType(%#v, %#v) = %q, want %q", in, tc.AuthType, berr.Detail, tc.WantErrDetail) 568 } 569 test.AssertMetricWithLabelsEquals( 570 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 571 } 572 }) 573 } 574 } 575 576 type badNonceProvider struct { 577 malformed bool 578 shortNonce bool 579 } 580 581 func (b badNonceProvider) Nonce() (string, error) { 582 if b.malformed { 583 return "im-a-nonce", nil 584 } 585 if b.shortNonce { 586 // A nonce length of 4 is considered "short" because there is no nonce 587 // material to be redeemed after the prefix. Derived prefixes are 8 588 // characters and static prefixes are 4 characters. 589 return "woww", nil 590 } 591 return "mlolmlol3ov77I5Ui-cdaY_k8IcjK58FvbG0y_BCRrx5rGQ8rjA", nil 592 } 593 594 func TestValidNonce(t *testing.T) { 595 wfe, _, signer := setupWFE(t) 596 597 goodJWS, _, _ := signer.embeddedJWK(nil, "", "") 598 599 testCases := []struct { 600 Name string 601 JWS *jose.JSONWebSignature 602 WantErrType berrors.ErrorType 603 WantErrDetail string 604 WantStatType string 605 }{ 606 { 607 Name: "No nonce in JWS", 608 JWS: signer.makeJWS(nil), 609 WantErrType: berrors.BadNonce, 610 WantErrDetail: "JWS has no anti-replay nonce", 611 WantStatType: "JWSMissingNonce", 612 }, 613 { 614 Name: "Malformed nonce in JWS", 615 JWS: signer.makeJWS(badNonceProvider{malformed: true}), 616 WantErrType: berrors.BadNonce, 617 WantErrDetail: "JWS has a malformed anti-replay nonce: \"im-a-nonce\"", 618 WantStatType: "JWSMalformedNonce", 619 }, 620 { 621 Name: "Canned nonce shorter than prefixLength in JWS", 622 JWS: signer.makeJWS(badNonceProvider{shortNonce: true}), 623 WantErrType: berrors.BadNonce, 624 WantErrDetail: "JWS has a malformed anti-replay nonce: \"woww\"", 625 WantStatType: "JWSMalformedNonce", 626 }, 627 { 628 Name: "Unrecognized nonce in JWS", 629 JWS: signer.makeJWS(badNonceProvider{}), 630 WantErrType: berrors.BadNonce, 631 WantErrDetail: "unable to decrypt nonce", 632 WantStatType: "JWSUnredeemableNonce", 633 }, 634 // We don't have a test case for "invalid" (i.e. no backend matching the 635 // prefix) because the unit tests don't use the noncebalancer that does 636 // that routing. 637 { 638 Name: "Valid nonce in JWS", 639 JWS: goodJWS, 640 }, 641 } 642 643 for _, tc := range testCases { 644 t.Run(tc.Name, func(t *testing.T) { 645 in := tc.JWS.Signatures[0].Header 646 wfe.stats.joseErrorCount.Reset() 647 648 gotErr := wfe.validNonce(context.Background(), in) 649 if tc.WantErrDetail == "" { 650 if gotErr != nil { 651 t.Fatalf("validNonce(%#v) = %#v, want nil", in, gotErr) 652 } 653 } else { 654 berr, ok := gotErr.(*berrors.BoulderError) 655 if !ok { 656 t.Fatalf("validNonce(%#v) returned %T, want BoulderError", in, gotErr) 657 } 658 if berr.Type != tc.WantErrType { 659 t.Errorf("validNonce(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) 660 } 661 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 662 t.Errorf("validNonce(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) 663 } 664 test.AssertMetricWithLabelsEquals( 665 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 666 } 667 }) 668 } 669 } 670 671 // noBackendsNonceRedeemer is a nonce redeemer that always returns an error 672 // indicating that the prefix matches no known nonce provider. 673 type noBackendsNonceRedeemer struct{} 674 675 func (n noBackendsNonceRedeemer) Redeem(ctx context.Context, _ *noncepb.NonceMessage, opts ...grpc.CallOption) (*noncepb.ValidMessage, error) { 676 return nil, noncebalancer.ErrNoBackendsMatchPrefix.Err() 677 } 678 679 func TestValidNonce_NoMatchingBackendFound(t *testing.T) { 680 wfe, _, signer := setupWFE(t) 681 goodJWS, _, _ := signer.embeddedJWK(nil, "", "") 682 wfe.rnc = noBackendsNonceRedeemer{} 683 684 // A valid JWS with a nonce whose prefix matches no known nonce provider should 685 // result in a BadNonceProblem. 686 err := wfe.validNonce(context.Background(), goodJWS.Signatures[0].Header) 687 test.AssertError(t, err, "Expected error for valid nonce with no backend") 688 test.AssertErrorIs(t, err, berrors.BadNonce) 689 test.AssertContains(t, err.Error(), "JWS has a nonce whose prefix matches no nonce service") 690 test.AssertMetricWithLabelsEquals(t, wfe.stats.nonceNoMatchingBackendCount, prometheus.Labels{}, 1) 691 } 692 693 func (rs requestSigner) signExtraHeaders( 694 headers map[jose.HeaderKey]any) (*jose.JSONWebSignature, string) { 695 privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM)) 696 697 signerKey := jose.SigningKey{ 698 Key: privateKey, 699 Algorithm: sigAlgForKey(rs.t, privateKey.Public()), 700 } 701 702 opts := &jose.SignerOptions{ 703 NonceSource: rs.nonceService, 704 EmbedJWK: true, 705 ExtraHeaders: headers, 706 } 707 708 signer, err := jose.NewSigner(signerKey, opts) 709 test.AssertNotError(rs.t, err, "Failed to make signer") 710 711 jws, err := signer.Sign([]byte("")) 712 test.AssertNotError(rs.t, err, "Failed to sign req") 713 714 body := jws.FullSerialize() 715 parsedJWS, err := jose.ParseSigned(body, getSupportedAlgs()) 716 test.AssertNotError(rs.t, err, "Failed to parse generated JWS") 717 718 return parsedJWS, body 719 } 720 721 func TestValidPOSTURL(t *testing.T) { 722 wfe, _, signer := setupWFE(t) 723 724 // A JWS and HTTP request with no extra headers 725 noHeadersJWS, noHeadersJWSBody := signer.signExtraHeaders(nil) 726 noHeadersRequest := makePostRequestWithPath("test-path", noHeadersJWSBody) 727 728 // A JWS and HTTP request with extra headers, but no "url" extra header 729 noURLHeaders := map[jose.HeaderKey]any{ 730 "nifty": "swell", 731 } 732 noURLHeaderJWS, noURLHeaderJWSBody := signer.signExtraHeaders(noURLHeaders) 733 noURLHeaderRequest := makePostRequestWithPath("test-path", noURLHeaderJWSBody) 734 735 // A JWS and HTTP request with a mismatched HTTP URL to JWS "url" header 736 wrongURLHeaders := map[jose.HeaderKey]any{ 737 "url": "foobar", 738 } 739 wrongURLHeaderJWS, wrongURLHeaderJWSBody := signer.signExtraHeaders(wrongURLHeaders) 740 wrongURLHeaderRequest := makePostRequestWithPath("test-path", wrongURLHeaderJWSBody) 741 742 correctURLHeaderJWS, _, correctURLHeaderJWSBody := signer.embeddedJWK(nil, "http://localhost/test-path", "") 743 correctURLHeaderRequest := makePostRequestWithPath("test-path", correctURLHeaderJWSBody) 744 745 testCases := []struct { 746 Name string 747 JWS *jose.JSONWebSignature 748 Request *http.Request 749 WantErrType berrors.ErrorType 750 WantErrDetail string 751 WantStatType string 752 }{ 753 { 754 Name: "No extra headers in JWS", 755 JWS: noHeadersJWS, 756 Request: noHeadersRequest, 757 WantErrType: berrors.Malformed, 758 WantErrDetail: "JWS header parameter 'url' required", 759 WantStatType: "JWSNoExtraHeaders", 760 }, 761 { 762 Name: "No URL header in JWS", 763 JWS: noURLHeaderJWS, 764 Request: noURLHeaderRequest, 765 WantErrType: berrors.Malformed, 766 WantErrDetail: "JWS header parameter 'url' required", 767 WantStatType: "JWSMissingURL", 768 }, 769 { 770 Name: "Wrong URL header in JWS", 771 JWS: wrongURLHeaderJWS, 772 Request: wrongURLHeaderRequest, 773 WantErrType: berrors.Malformed, 774 WantErrDetail: "JWS header parameter 'url' incorrect. Expected \"http://localhost/test-path\" got \"foobar\"", 775 WantStatType: "JWSMismatchedURL", 776 }, 777 { 778 Name: "Correct URL header in JWS", 779 JWS: correctURLHeaderJWS, 780 Request: correctURLHeaderRequest, 781 }, 782 } 783 784 for _, tc := range testCases { 785 t.Run(tc.Name, func(t *testing.T) { 786 in := tc.JWS.Signatures[0].Header 787 tc.Request.Header.Add("Content-Type", expectedJWSContentType) 788 wfe.stats.joseErrorCount.Reset() 789 790 got := wfe.validPOSTURL(tc.Request, in) 791 if tc.WantErrDetail == "" { 792 if got != nil { 793 t.Fatalf("validPOSTURL(%#v) = %#v, want nil", in, got) 794 } 795 } else { 796 berr, ok := got.(*berrors.BoulderError) 797 if !ok { 798 t.Fatalf("validPOSTURL(%#v) returned %T, want BoulderError", in, got) 799 } 800 if berr.Type != tc.WantErrType { 801 t.Errorf("validPOSTURL(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) 802 } 803 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 804 t.Errorf("validPOSTURL(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) 805 } 806 test.AssertMetricWithLabelsEquals( 807 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 808 } 809 }) 810 } 811 } 812 813 func (rs requestSigner) multiSigJWS() (*jose.JSONWebSignature, string) { 814 privateKeyA := loadKey(rs.t, []byte(test1KeyPrivatePEM)) 815 privateKeyB := loadKey(rs.t, []byte(test2KeyPrivatePEM)) 816 817 signerKeyA := jose.SigningKey{ 818 Key: privateKeyA, 819 Algorithm: sigAlgForKey(rs.t, privateKeyA.Public()), 820 } 821 822 signerKeyB := jose.SigningKey{ 823 Key: privateKeyB, 824 Algorithm: sigAlgForKey(rs.t, privateKeyB.Public()), 825 } 826 827 opts := &jose.SignerOptions{ 828 NonceSource: rs.nonceService, 829 EmbedJWK: true, 830 } 831 832 signer, err := jose.NewMultiSigner([]jose.SigningKey{signerKeyA, signerKeyB}, opts) 833 test.AssertNotError(rs.t, err, "Failed to make multi signer") 834 835 jws, err := signer.Sign([]byte("")) 836 test.AssertNotError(rs.t, err, "Failed to sign req") 837 838 body := jws.FullSerialize() 839 parsedJWS, err := jose.ParseSigned(body, getSupportedAlgs()) 840 test.AssertNotError(rs.t, err, "Failed to parse generated JWS") 841 842 return parsedJWS, body 843 } 844 845 func TestParseJWSRequest(t *testing.T) { 846 wfe, _, signer := setupWFE(t) 847 848 _, tooManySigsJWSBody := signer.multiSigJWS() 849 850 _, _, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test-path", "") 851 validJWSRequest := makePostRequestWithPath("test-path", validJWSBody) 852 853 _, _, validJWSBody2 := signer.embeddedJWK(nil, "http://localhost/test-path", "") 854 validJWSChunkedRequest := makeChunkedPostRequestWithPath("test-path", validJWSBody2) 855 856 missingSigsJWSBody := `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ"}` 857 missingSigsJWSRequest := makePostRequestWithPath("test-path", missingSigsJWSBody) 858 859 unprotectedHeadersJWSBody := ` 860 { 861 "header": { 862 "alg": "RS256", 863 "kid": "unprotected key id" 864 }, 865 "protected": "eyJub25jZSI6ICJibTl1WTJVIiwgInVybCI6ICJodHRwOi8vbG9jYWxob3N0L3Rlc3QiLCAia2lkIjogInRlc3RrZXkifQ", 866 "payload": "Zm9v", 867 "signature": "PKWWclRsiHF4bm-nmpxDez6Y_3Mdtu263YeYklbGYt1EiMOLiKY_dr_EqhUUKAKEWysFLO-hQLXVU7kVkHeYWQFFOA18oFgcZgkSF2Pr3DNZrVj9e2gl0eZ2i2jk6X5GYPt1lIfok_DrL92wrxEKGcrmxqXXGm0JgP6Al2VGapKZK2HaYbCHoGvtzNmzUX9rC21sKewq5CquJRvTmvQp5bmU7Q9KeafGibFr0jl6IA3W5LBGgf6xftuUtEVEbKmKaKtaG7tXsQH1mIVOPUZZoLWz9sWJSFLmV0QSXm3ZHV0DrOhLfcADbOCoQBMeGdseBQZuUO541A3BEKGv2Aikjw" 868 } 869 ` 870 871 wrongSignaturesFieldJWSBody := ` 872 { 873 "protected": "eyJub25jZSI6ICJibTl1WTJVIiwgInVybCI6ICJodHRwOi8vbG9jYWxob3N0L3Rlc3QiLCAia2lkIjogInRlc3RrZXkifQ", 874 "payload": "Zm9v", 875 "signatures": ["PKWWclRsiHF4bm-nmpxDez6Y_3Mdtu263YeYklbGYt1EiMOLiKY_dr_EqhUUKAKEWysFLO-hQLXVU7kVkHeYWQFFOA18oFgcZgkSF2Pr3DNZrVj9e2gl0eZ2i2jk6X5GYPt1lIfok_DrL92wrxEKGcrmxqXXGm0JgP6Al2VGapKZK2HaYbCHoGvtzNmzUX9rC21sKewq5CquJRvTmvQp5bmU7Q9KeafGibFr0jl6IA3W5LBGgf6xftuUtEVEbKmKaKtaG7tXsQH1mIVOPUZZoLWz9sWJSFLmV0QSXm3ZHV0DrOhLfcADbOCoQBMeGdseBQZuUO541A3BEKGv2Aikjw"] 876 } 877 ` 878 wrongSignatureTypeJWSBody := ` 879 { 880 "protected": "eyJhbGciOiJIUzI1NiJ9", 881 "payload" : "IiI", 882 "signature" : "5WiUupHzCWfpJza6EMteSxMDY8_6xIV7HnKaUqmykIQ" 883 } 884 ` 885 886 testCases := []struct { 887 Name string 888 Request *http.Request 889 WantErrType berrors.ErrorType 890 WantErrDetail string 891 WantStatType string 892 }{ 893 { 894 Name: "Invalid JWS in POST body", 895 Request: makePostRequestWithPath("test-path", `{`), 896 WantErrType: berrors.Malformed, 897 WantErrDetail: "Parse error reading JWS", 898 WantStatType: "JWSUnmarshalFailed", 899 }, 900 { 901 Name: "Too few signatures in JWS", 902 Request: missingSigsJWSRequest, 903 WantErrType: berrors.Malformed, 904 WantErrDetail: "POST JWS not signed", 905 WantStatType: "JWSEmptySignature", 906 }, 907 { 908 Name: "Too many signatures in JWS", 909 Request: makePostRequestWithPath("test-path", tooManySigsJWSBody), 910 WantErrType: berrors.Malformed, 911 WantErrDetail: "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature", 912 WantStatType: "JWSMultiSig", 913 }, 914 { 915 Name: "Unprotected JWS headers", 916 Request: makePostRequestWithPath("test-path", unprotectedHeadersJWSBody), 917 WantErrType: berrors.Malformed, 918 WantErrDetail: "JWS \"header\" field not allowed. All headers must be in \"protected\" field", 919 WantStatType: "JWSUnprotectedHeaders", 920 }, 921 { 922 Name: "Unsupported signatures field in JWS", 923 Request: makePostRequestWithPath("test-path", wrongSignaturesFieldJWSBody), 924 WantErrType: berrors.Malformed, 925 WantErrDetail: "JWS \"signatures\" field not allowed. Only the \"signature\" field should contain a signature", 926 WantStatType: "JWSMultiSig", 927 }, 928 { 929 Name: "JWS with an invalid algorithm", 930 Request: makePostRequestWithPath("test-path", wrongSignatureTypeJWSBody), 931 WantErrType: berrors.BadSignatureAlgorithm, 932 WantErrDetail: "JWS signature header contains unsupported algorithm \"HS256\", expected one of [RS256 ES256 ES384 ES512]", 933 WantStatType: "JWSAlgorithmCheckFailed", 934 }, 935 { 936 Name: "Valid JWS in POST request", 937 Request: validJWSRequest, 938 }, 939 { 940 Name: "Valid JWS in chunked encoded POST request", 941 Request: validJWSChunkedRequest, 942 }, 943 { 944 Name: "POST body too large", 945 Request: makePostRequestWithPath("test-path", fmt.Sprintf(`{"a":"%s"}`, strings.Repeat("a", 50000))), 946 WantErrType: berrors.Unauthorized, 947 WantErrDetail: "request body too large", 948 }, 949 { 950 Name: "chunked encoded POST body too large", 951 Request: makeChunkedPostRequestWithPath("test-path", fmt.Sprintf(`{"a":"%s"}`, strings.Repeat("a", 50000))), 952 WantErrType: berrors.Unauthorized, 953 WantErrDetail: "request body too large", 954 }, 955 } 956 957 for _, tc := range testCases { 958 t.Run(tc.Name, func(t *testing.T) { 959 wfe.stats.joseErrorCount.Reset() 960 961 _, gotErr := wfe.parseJWSRequest(tc.Request) 962 if tc.WantErrDetail == "" { 963 if gotErr != nil { 964 t.Fatalf("parseJWSRequest(%#v) = %#v, want nil", tc.Request, gotErr) 965 } 966 } else { 967 berr, ok := gotErr.(*berrors.BoulderError) 968 if !ok { 969 t.Fatalf("parseJWSRequest(%#v) returned %T, want BoulderError", tc.Request, gotErr) 970 } 971 if berr.Type != tc.WantErrType { 972 t.Errorf("parseJWSRequest(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) 973 } 974 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 975 t.Errorf("parseJWSRequest(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) 976 } 977 if tc.WantStatType != "" { 978 test.AssertMetricWithLabelsEquals( 979 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 980 } 981 } 982 }) 983 } 984 } 985 986 func TestExtractJWK(t *testing.T) { 987 wfe, _, signer := setupWFE(t) 988 989 keyIDJWS, _, _ := signer.byKeyID(1, nil, "", "") 990 goodJWS, goodJWK, _ := signer.embeddedJWK(nil, "", "") 991 992 testCases := []struct { 993 Name string 994 JWS *jose.JSONWebSignature 995 WantKey *jose.JSONWebKey 996 WantErrType berrors.ErrorType 997 WantErrDetail string 998 }{ 999 { 1000 Name: "JWS with wrong auth type (Key ID vs embedded JWK)", 1001 JWS: keyIDJWS, 1002 WantErrType: berrors.Malformed, 1003 WantErrDetail: "No embedded JWK in JWS header", 1004 }, 1005 { 1006 Name: "Valid JWS with embedded JWK", 1007 JWS: goodJWS, 1008 WantKey: goodJWK, 1009 }, 1010 } 1011 1012 for _, tc := range testCases { 1013 t.Run(tc.Name, func(t *testing.T) { 1014 in := tc.JWS.Signatures[0].Header 1015 1016 gotKey, gotErr := wfe.extractJWK(in) 1017 if tc.WantErrDetail == "" { 1018 if gotErr != nil { 1019 t.Fatalf("extractJWK(%#v) = %#v, want nil", in, gotKey) 1020 } 1021 test.AssertMarshaledEquals(t, gotKey, tc.WantKey) 1022 } else { 1023 berr, ok := gotErr.(*berrors.BoulderError) 1024 if !ok { 1025 t.Fatalf("extractJWK(%#v) returned %T, want BoulderError", in, gotErr) 1026 } 1027 if berr.Type != tc.WantErrType { 1028 t.Errorf("extractJWK(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) 1029 } 1030 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1031 t.Errorf("extractJWK(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) 1032 } 1033 } 1034 }) 1035 } 1036 } 1037 1038 func (rs requestSigner) specifyKeyID(keyID string) (*jose.JSONWebSignature, string) { 1039 privateKey := loadKey(rs.t, []byte(test1KeyPrivatePEM)) 1040 1041 if keyID == "" { 1042 keyID = "this is an invalid non-numeric key ID" 1043 } 1044 1045 jwk := &jose.JSONWebKey{ 1046 Key: privateKey, 1047 Algorithm: "RSA", 1048 KeyID: keyID, 1049 } 1050 1051 signerKey := jose.SigningKey{ 1052 Key: jwk, 1053 Algorithm: jose.RS256, 1054 } 1055 1056 opts := &jose.SignerOptions{ 1057 NonceSource: rs.nonceService, 1058 ExtraHeaders: map[jose.HeaderKey]any{ 1059 "url": "http://localhost", 1060 }, 1061 } 1062 1063 signer, err := jose.NewSigner(signerKey, opts) 1064 test.AssertNotError(rs.t, err, "Failed to make signer") 1065 1066 jws, err := signer.Sign([]byte("")) 1067 test.AssertNotError(rs.t, err, "Failed to sign req") 1068 1069 body := jws.FullSerialize() 1070 parsedJWS, err := jose.ParseSigned(body, getSupportedAlgs()) 1071 test.AssertNotError(rs.t, err, "Failed to parse generated JWS") 1072 1073 return parsedJWS, body 1074 } 1075 1076 func TestLookupJWK(t *testing.T) { 1077 wfe, _, signer := setupWFE(t) 1078 1079 embeddedJWS, _, embeddedJWSBody := signer.embeddedJWK(nil, "", "") 1080 invalidKeyIDJWS, invalidKeyIDJWSBody := signer.specifyKeyID("https://acme-99.lettuceencrypt.org/acme/reg/1") 1081 // ID 100 is mocked to return a non-missing error from sa.GetRegistration 1082 errorIDJWS, _, errorIDJWSBody := signer.byKeyID(100, nil, "", "") 1083 // ID 102 is mocked to return an account does not exist error from sa.GetRegistration 1084 missingIDJWS, _, missingIDJWSBody := signer.byKeyID(102, nil, "", "") 1085 // ID 3 is mocked to return a deactivated account from sa.GetRegistration 1086 deactivatedIDJWS, _, deactivatedIDJWSBody := signer.byKeyID(3, nil, "", "") 1087 1088 wfe.LegacyKeyIDPrefix = "https://acme-v00.lettuceencrypt.org/acme/reg/" 1089 legacyKeyIDJWS, legacyKeyIDJWSBody := signer.specifyKeyID(wfe.LegacyKeyIDPrefix + "1") 1090 1091 nonNumericKeyIDJWS, nonNumericKeyIDJWSBody := signer.specifyKeyID(wfe.LegacyKeyIDPrefix + "abcd") 1092 1093 validJWS, validKey, validJWSBody := signer.byKeyID(1, nil, "", "") 1094 validAccountPB, _ := wfe.sa.GetRegistration(context.Background(), &sapb.RegistrationID{Id: 1}) 1095 validAccount, _ := bgrpc.PbToRegistration(validAccountPB) 1096 1097 // good key, log event requester is set 1098 1099 testCases := []struct { 1100 Name string 1101 JWS *jose.JSONWebSignature 1102 Request *http.Request 1103 WantJWK *jose.JSONWebKey 1104 WantAccount *core.Registration 1105 WantErrType berrors.ErrorType 1106 WantErrDetail string 1107 WantStatType string 1108 }{ 1109 { 1110 Name: "JWS with wrong auth type (embedded JWK vs Key ID)", 1111 JWS: embeddedJWS, 1112 Request: makePostRequestWithPath("test-path", embeddedJWSBody), 1113 WantErrType: berrors.Malformed, 1114 WantErrDetail: "No Key ID in JWS header", 1115 WantStatType: "JWSAuthTypeWrong", 1116 }, 1117 { 1118 Name: "JWS with invalid key ID URL", 1119 JWS: invalidKeyIDJWS, 1120 Request: makePostRequestWithPath("test-path", invalidKeyIDJWSBody), 1121 WantErrType: berrors.Malformed, 1122 WantErrDetail: "KeyID header contained an invalid account URL: \"https://acme-99.lettuceencrypt.org/acme/reg/1\"", 1123 WantStatType: "JWSInvalidKeyID", 1124 }, 1125 { 1126 Name: "JWS with non-numeric account ID in key ID URL", 1127 JWS: nonNumericKeyIDJWS, 1128 Request: makePostRequestWithPath("test-path", nonNumericKeyIDJWSBody), 1129 WantErrType: berrors.Malformed, 1130 WantErrDetail: "Malformed account ID in KeyID header URL: \"https://acme-v00.lettuceencrypt.org/acme/reg/abcd\"", 1131 WantStatType: "JWSInvalidKeyID", 1132 }, 1133 { 1134 Name: "JWS with account ID that causes GetRegistration error", 1135 JWS: errorIDJWS, 1136 Request: makePostRequestWithPath("test-path", errorIDJWSBody), 1137 WantErrType: berrors.InternalServer, 1138 WantErrDetail: "Error retrieving account \"http://localhost/acme/acct/100\"", 1139 WantStatType: "JWSKeyIDLookupFailed", 1140 }, 1141 { 1142 Name: "JWS with account ID that doesn't exist", 1143 JWS: missingIDJWS, 1144 Request: makePostRequestWithPath("test-path", missingIDJWSBody), 1145 WantErrType: berrors.AccountDoesNotExist, 1146 WantErrDetail: "Account \"http://localhost/acme/acct/102\" not found", 1147 WantStatType: "JWSKeyIDNotFound", 1148 }, 1149 { 1150 Name: "JWS with account ID that is deactivated", 1151 JWS: deactivatedIDJWS, 1152 Request: makePostRequestWithPath("test-path", deactivatedIDJWSBody), 1153 WantErrType: berrors.Unauthorized, 1154 WantErrDetail: "Account is not valid, has status \"deactivated\"", 1155 WantStatType: "JWSKeyIDAccountInvalid", 1156 }, 1157 { 1158 Name: "Valid JWS with legacy account ID", 1159 JWS: legacyKeyIDJWS, 1160 Request: makePostRequestWithPath("test-path", legacyKeyIDJWSBody), 1161 WantJWK: validKey, 1162 WantAccount: &validAccount, 1163 }, 1164 { 1165 Name: "Valid JWS with valid account ID", 1166 JWS: validJWS, 1167 Request: makePostRequestWithPath("test-path", validJWSBody), 1168 WantJWK: validKey, 1169 WantAccount: &validAccount, 1170 }, 1171 } 1172 for _, tc := range testCases { 1173 t.Run(tc.Name, func(t *testing.T) { 1174 wfe.stats.joseErrorCount.Reset() 1175 in := tc.JWS.Signatures[0].Header 1176 inputLogEvent := newRequestEvent() 1177 1178 gotJWK, gotAcct, gotErr := wfe.lookupJWK(in, context.Background(), tc.Request, inputLogEvent) 1179 if tc.WantErrDetail == "" { 1180 if gotErr != nil { 1181 t.Fatalf("lookupJWK(%#v) = %#v, want nil", in, gotErr) 1182 } 1183 gotThumb, _ := gotJWK.Thumbprint(crypto.SHA256) 1184 wantThumb, _ := tc.WantJWK.Thumbprint(crypto.SHA256) 1185 if !slices.Equal(gotThumb, wantThumb) { 1186 t.Fatalf("lookupJWK(%#v) = %#v, want %#v", tc.Request, gotThumb, wantThumb) 1187 } 1188 test.AssertMarshaledEquals(t, gotAcct, tc.WantAccount) 1189 test.AssertEquals(t, inputLogEvent.Requester, gotAcct.ID) 1190 } else { 1191 var berr *berrors.BoulderError 1192 ok := errors.As(gotErr, &berr) 1193 if !ok { 1194 t.Fatalf("lookupJWK(%#v) returned %T, want BoulderError", in, gotErr) 1195 } 1196 if berr.Type != tc.WantErrType { 1197 t.Errorf("lookupJWK(%#v) = %#v, want %#v", in, berr.Type, tc.WantErrType) 1198 } 1199 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1200 t.Errorf("lookupJWK(%#v) = %q, want %q", in, berr.Detail, tc.WantErrDetail) 1201 } 1202 test.AssertMetricWithLabelsEquals( 1203 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 1204 } 1205 }) 1206 } 1207 } 1208 1209 func TestValidJWSForKey(t *testing.T) { 1210 wfe, _, signer := setupWFE(t) 1211 1212 payload := `{ "test": "payload" }` 1213 testURL := "http://localhost/test" 1214 goodJWS, goodJWK, _ := signer.embeddedJWK(nil, testURL, payload) 1215 1216 // badSigJWSBody is a JWS that has had the payload changed by 1 byte to break the signature 1217 badSigJWSBody := `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}` 1218 badJWS, err := jose.ParseSigned(badSigJWSBody, getSupportedAlgs()) 1219 test.AssertNotError(t, err, "error loading badSigJWS body") 1220 1221 // wrongAlgJWS is a JWS that has an invalid "HS256" algorithm in its header 1222 wrongAlgJWS := &jose.JSONWebSignature{ 1223 Signatures: []jose.Signature{ 1224 { 1225 Header: jose.Header{ 1226 Algorithm: "HS256", 1227 }, 1228 }, 1229 }, 1230 } 1231 1232 // A JWS and HTTP request with a mismatched HTTP URL to JWS "url" header 1233 wrongURLHeaders := map[jose.HeaderKey]any{ 1234 "url": "foobar", 1235 } 1236 wrongURLHeaderJWS, _ := signer.signExtraHeaders(wrongURLHeaders) 1237 1238 // badJSONJWS has a valid signature over a body that is not valid JSON 1239 badJSONJWS, _, _ := signer.embeddedJWK(nil, testURL, `{`) 1240 1241 testCases := []struct { 1242 Name string 1243 JWS bJSONWebSignature 1244 JWK *jose.JSONWebKey 1245 Body string 1246 WantErrType berrors.ErrorType 1247 WantErrDetail string 1248 WantStatType string 1249 }{ 1250 { 1251 Name: "JWS with an invalid algorithm", 1252 JWS: bJSONWebSignature{wrongAlgJWS}, 1253 JWK: goodJWK, 1254 WantErrType: berrors.BadSignatureAlgorithm, 1255 WantErrDetail: "JWS signature header contains unsupported algorithm \"HS256\", expected one of [RS256 ES256 ES384 ES512]", 1256 WantStatType: "JWSAlgorithmCheckFailed", 1257 }, 1258 { 1259 Name: "JWS with an unredeemable nonce", 1260 JWS: bJSONWebSignature{signer.makeJWS(badNonceProvider{})}, 1261 JWK: goodJWK, 1262 WantErrType: berrors.BadNonce, 1263 WantErrDetail: "unable to decrypt nonce", 1264 WantStatType: "JWSUnredeemableNonce", 1265 }, 1266 { 1267 Name: "JWS with broken signature", 1268 JWS: bJSONWebSignature{badJWS}, 1269 JWK: badJWS.Signatures[0].Header.JSONWebKey, 1270 WantErrType: berrors.Malformed, 1271 WantErrDetail: "JWS verification error", 1272 WantStatType: "JWSVerifyFailed", 1273 }, 1274 { 1275 Name: "JWS with incorrect URL", 1276 JWS: bJSONWebSignature{wrongURLHeaderJWS}, 1277 JWK: wrongURLHeaderJWS.Signatures[0].Header.JSONWebKey, 1278 WantErrType: berrors.Malformed, 1279 WantErrDetail: "JWS header parameter 'url' incorrect. Expected \"http://localhost/test\" got \"foobar\"", 1280 WantStatType: "JWSMismatchedURL", 1281 }, 1282 { 1283 Name: "Valid JWS with invalid JSON in the protected body", 1284 JWS: bJSONWebSignature{badJSONJWS}, 1285 JWK: goodJWK, 1286 WantErrType: berrors.Malformed, 1287 WantErrDetail: "Request payload did not parse as JSON", 1288 WantStatType: "JWSBodyUnmarshalFailed", 1289 }, 1290 { 1291 Name: "Good JWS and JWK", 1292 JWS: bJSONWebSignature{goodJWS}, 1293 JWK: goodJWK, 1294 }, 1295 } 1296 1297 for _, tc := range testCases { 1298 t.Run(tc.Name, func(t *testing.T) { 1299 wfe.stats.joseErrorCount.Reset() 1300 request := makePostRequestWithPath("test", tc.Body) 1301 1302 gotPayload, gotErr := wfe.validJWSForKey(context.Background(), &tc.JWS, tc.JWK, request) 1303 if tc.WantErrDetail == "" { 1304 if gotErr != nil { 1305 t.Fatalf("validJWSForKey(%#v, %#v, %#v) = %#v, want nil", tc.JWS, tc.JWK, request, gotErr) 1306 } 1307 if string(gotPayload) != payload { 1308 t.Fatalf("validJWSForKey(%#v, %#v, %#v) = %q, want %q", tc.JWS, tc.JWK, request, string(gotPayload), payload) 1309 } 1310 } else { 1311 berr, ok := gotErr.(*berrors.BoulderError) 1312 if !ok { 1313 t.Fatalf("validJWSForKey(%#v, %#v, %#v) returned %T, want BoulderError", tc.JWS, tc.JWK, request, gotErr) 1314 } 1315 if berr.Type != tc.WantErrType { 1316 t.Errorf("validJWSForKey(%#v, %#v, %#v) = %#v, want %#v", tc.JWS, tc.JWK, request, berr.Type, tc.WantErrType) 1317 } 1318 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1319 t.Errorf("validJWSForKey(%#v, %#v, %#v) = %q, want %q", tc.JWS, tc.JWK, request, berr.Detail, tc.WantErrDetail) 1320 } 1321 test.AssertMetricWithLabelsEquals( 1322 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 1323 } 1324 }) 1325 } 1326 } 1327 1328 func TestValidPOSTForAccount(t *testing.T) { 1329 wfe, _, signer := setupWFE(t) 1330 1331 validJWS, _, validJWSBody := signer.byKeyID(1, nil, "http://localhost/test", `{"test":"passed"}`) 1332 validAccountPB, _ := wfe.sa.GetRegistration(context.Background(), &sapb.RegistrationID{Id: 1}) 1333 validAccount, _ := bgrpc.PbToRegistration(validAccountPB) 1334 1335 // ID 102 is mocked to return missing 1336 _, _, missingJWSBody := signer.byKeyID(102, nil, "http://localhost/test", "{}") 1337 1338 // ID 3 is mocked to return deactivated 1339 key3 := loadKey(t, []byte(test3KeyPrivatePEM)) 1340 _, _, deactivatedJWSBody := signer.byKeyID(3, key3, "http://localhost/test", "{}") 1341 1342 _, _, embeddedJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) 1343 1344 testCases := []struct { 1345 Name string 1346 Request *http.Request 1347 WantPayload string 1348 WantAcct *core.Registration 1349 WantJWS *jose.JSONWebSignature 1350 WantErrType berrors.ErrorType 1351 WantErrDetail string 1352 WantStatType string 1353 }{ 1354 { 1355 Name: "Invalid JWS", 1356 Request: makePostRequestWithPath("test", "foo"), 1357 WantErrType: berrors.Malformed, 1358 WantErrDetail: "Parse error reading JWS", 1359 WantStatType: "JWSUnmarshalFailed", 1360 }, 1361 { 1362 Name: "Embedded Key JWS", 1363 Request: makePostRequestWithPath("test", embeddedJWSBody), 1364 WantErrType: berrors.Malformed, 1365 WantErrDetail: "No Key ID in JWS header", 1366 WantStatType: "JWSAuthTypeWrong", 1367 }, 1368 { 1369 Name: "JWS signed by account that doesn't exist", 1370 Request: makePostRequestWithPath("test", missingJWSBody), 1371 WantErrType: berrors.AccountDoesNotExist, 1372 WantErrDetail: "Account \"http://localhost/acme/acct/102\" not found", 1373 WantStatType: "JWSKeyIDNotFound", 1374 }, 1375 { 1376 Name: "JWS signed by account that's deactivated", 1377 Request: makePostRequestWithPath("test", deactivatedJWSBody), 1378 WantErrType: berrors.Unauthorized, 1379 WantErrDetail: "Account is not valid, has status \"deactivated\"", 1380 WantStatType: "JWSKeyIDAccountInvalid", 1381 }, 1382 { 1383 Name: "Valid JWS for account", 1384 Request: makePostRequestWithPath("test", validJWSBody), 1385 WantPayload: `{"test":"passed"}`, 1386 WantAcct: &validAccount, 1387 WantJWS: validJWS, 1388 }, 1389 } 1390 1391 for _, tc := range testCases { 1392 t.Run(tc.Name, func(t *testing.T) { 1393 wfe.stats.joseErrorCount.Reset() 1394 inputLogEvent := newRequestEvent() 1395 1396 gotPayload, gotJWS, gotAcct, gotErr := wfe.validPOSTForAccount(tc.Request, context.Background(), inputLogEvent) 1397 if tc.WantErrDetail == "" { 1398 if gotErr != nil { 1399 t.Fatalf("validPOSTForAccount(%#v) = %#v, want nil", tc.Request, gotErr) 1400 } 1401 if string(gotPayload) != tc.WantPayload { 1402 t.Fatalf("validPOSTForAccount(%#v) = %q, want %q", tc.Request, string(gotPayload), tc.WantPayload) 1403 } 1404 test.AssertMarshaledEquals(t, gotJWS, tc.WantJWS) 1405 test.AssertMarshaledEquals(t, gotAcct, tc.WantAcct) 1406 } else { 1407 berr, ok := gotErr.(*berrors.BoulderError) 1408 if !ok { 1409 t.Fatalf("validPOSTForAccount(%#v) returned %T, want BoulderError", tc.Request, gotErr) 1410 } 1411 if berr.Type != tc.WantErrType { 1412 t.Errorf("validPOSTForAccount(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) 1413 } 1414 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1415 t.Errorf("validPOSTForAccount(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) 1416 } 1417 test.AssertMetricWithLabelsEquals( 1418 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 1419 } 1420 }) 1421 } 1422 } 1423 1424 // TestValidPOSTAsGETForAccount tests POST-as-GET processing. Because 1425 // wfe.validPOSTAsGETForAccount calls `wfe.validPOSTForAccount` to do all 1426 // processing except the empty body test we do not duplicate the 1427 // `TestValidPOSTForAccount` testcases here. 1428 func TestValidPOSTAsGETForAccount(t *testing.T) { 1429 wfe, _, signer := setupWFE(t) 1430 1431 // an invalid POST-as-GET request contains a non-empty payload. In this case 1432 // we test with the empty JSON payload ("{}") 1433 _, _, invalidPayloadRequest := signer.byKeyID(1, nil, "http://localhost/test", "{}") 1434 // a valid POST-as-GET request contains an empty payload. 1435 _, _, validRequest := signer.byKeyID(1, nil, "http://localhost/test", "") 1436 1437 testCases := []struct { 1438 Name string 1439 Request *http.Request 1440 WantErrType berrors.ErrorType 1441 WantErrDetail string 1442 WantLogEvent web.RequestEvent 1443 }{ 1444 { 1445 Name: "Non-empty JWS payload", 1446 Request: makePostRequestWithPath("test", invalidPayloadRequest), 1447 WantErrType: berrors.Malformed, 1448 WantErrDetail: "POST-as-GET requests must have an empty payload", 1449 WantLogEvent: web.RequestEvent{}, 1450 }, 1451 { 1452 Name: "Valid POST-as-GET", 1453 Request: makePostRequestWithPath("test", validRequest), 1454 WantLogEvent: web.RequestEvent{ 1455 Method: "POST-as-GET", 1456 }, 1457 }, 1458 } 1459 1460 for _, tc := range testCases { 1461 t.Run(tc.Name, func(t *testing.T) { 1462 ev := newRequestEvent() 1463 _, gotErr := wfe.validPOSTAsGETForAccount(tc.Request, context.Background(), ev) 1464 if tc.WantErrDetail == "" { 1465 if gotErr != nil { 1466 t.Fatalf("validPOSTAsGETForAccount(%#v) = %#v, want nil", tc.Request, gotErr) 1467 } 1468 } else { 1469 berr, ok := gotErr.(*berrors.BoulderError) 1470 if !ok { 1471 t.Fatalf("validPOSTAsGETForAccount(%#v) returned %T, want BoulderError", tc.Request, gotErr) 1472 } 1473 if berr.Type != tc.WantErrType { 1474 t.Errorf("validPOSTAsGETForAccount(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) 1475 } 1476 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1477 t.Errorf("validPOSTAsGETForAccount(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) 1478 } 1479 } 1480 test.AssertMarshaledEquals(t, *ev, tc.WantLogEvent) 1481 }) 1482 } 1483 } 1484 1485 type mockSADifferentStoredKey struct { 1486 sapb.StorageAuthorityReadOnlyClient 1487 } 1488 1489 // mockSADifferentStoredKey has a GetRegistration that will always return an 1490 // account with the test 2 key, no matter the provided ID 1491 func (sa mockSADifferentStoredKey) GetRegistration(_ context.Context, _ *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error) { 1492 return &corepb.Registration{ 1493 Key: []byte(test2KeyPublicJSON), 1494 Status: string(core.StatusValid), 1495 }, nil 1496 } 1497 1498 func TestValidPOSTForAccountSwappedKey(t *testing.T) { 1499 wfe, _, signer := setupWFE(t) 1500 wfe.sa = &mockSADifferentStoredKey{} 1501 wfe.accountGetter = wfe.sa 1502 event := newRequestEvent() 1503 1504 payload := `{"resource":"ima-payload"}` 1505 // Sign a request using test1key 1506 _, _, body := signer.byKeyID(1, nil, "http://localhost:4001/test", payload) 1507 request := makePostRequestWithPath("test", body) 1508 1509 // Ensure that ValidPOSTForAccount produces an error since the 1510 // mockSADifferentStoredKey will return a different key than the one we used to 1511 // sign the request 1512 _, _, _, err := wfe.validPOSTForAccount(request, ctx, event) 1513 test.AssertError(t, err, "No error returned for request signed by wrong key") 1514 test.AssertErrorIs(t, err, berrors.Malformed) 1515 test.AssertContains(t, err.Error(), "JWS verification error") 1516 } 1517 1518 func TestValidSelfAuthenticatedPOSTGoodKeyErrors(t *testing.T) { 1519 wfe, _, signer := setupWFE(t) 1520 1521 timeoutErrCheckFunc := func(ctx context.Context, keyHash []byte) (bool, error) { 1522 return false, context.DeadlineExceeded 1523 } 1524 1525 kp, err := goodkey.NewPolicy(nil, timeoutErrCheckFunc) 1526 test.AssertNotError(t, err, "making key policy") 1527 1528 wfe.keyPolicy = kp 1529 1530 _, _, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) 1531 request := makePostRequestWithPath("test", validJWSBody) 1532 1533 _, _, err = wfe.validSelfAuthenticatedPOST(context.Background(), request) 1534 test.AssertErrorIs(t, err, berrors.InternalServer) 1535 1536 badKeyCheckFunc := func(ctx context.Context, keyHash []byte) (bool, error) { 1537 return false, fmt.Errorf("oh no: %w", goodkey.ErrBadKey) 1538 } 1539 1540 kp, err = goodkey.NewPolicy(nil, badKeyCheckFunc) 1541 test.AssertNotError(t, err, "making key policy") 1542 1543 wfe.keyPolicy = kp 1544 1545 _, _, validJWSBody = signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) 1546 request = makePostRequestWithPath("test", validJWSBody) 1547 1548 _, _, err = wfe.validSelfAuthenticatedPOST(context.Background(), request) 1549 test.AssertErrorIs(t, err, berrors.BadPublicKey) 1550 } 1551 1552 func TestValidSelfAuthenticatedPOST(t *testing.T) { 1553 wfe, _, signer := setupWFE(t) 1554 1555 _, validKey, validJWSBody := signer.embeddedJWK(nil, "http://localhost/test", `{"test":"passed"}`) 1556 1557 _, _, keyIDJWSBody := signer.byKeyID(1, nil, "http://localhost/test", `{"test":"passed"}`) 1558 1559 testCases := []struct { 1560 Name string 1561 Request *http.Request 1562 WantPayload string 1563 WantJWK *jose.JSONWebKey 1564 WantErrType berrors.ErrorType 1565 WantErrDetail string 1566 WantStatType string 1567 }{ 1568 { 1569 Name: "Invalid JWS", 1570 Request: makePostRequestWithPath("test", "foo"), 1571 WantErrType: berrors.Malformed, 1572 WantErrDetail: "Parse error reading JWS", 1573 WantStatType: "JWSUnmarshalFailed", 1574 }, 1575 { 1576 Name: "JWS with key ID", 1577 Request: makePostRequestWithPath("test", keyIDJWSBody), 1578 WantErrType: berrors.Malformed, 1579 WantErrDetail: "No embedded JWK in JWS header", 1580 WantStatType: "JWSAuthTypeWrong", 1581 }, 1582 { 1583 Name: "Valid JWS", 1584 Request: makePostRequestWithPath("test", validJWSBody), 1585 WantPayload: `{"test":"passed"}`, 1586 WantJWK: validKey, 1587 }, 1588 } 1589 1590 for _, tc := range testCases { 1591 t.Run(tc.Name, func(t *testing.T) { 1592 wfe.stats.joseErrorCount.Reset() 1593 gotPayload, gotJWK, gotErr := wfe.validSelfAuthenticatedPOST(context.Background(), tc.Request) 1594 if tc.WantErrDetail == "" { 1595 if gotErr != nil { 1596 t.Fatalf("validSelfAuthenticatedPOST(%#v) = %#v, want nil", tc.Request, gotErr) 1597 } 1598 if string(gotPayload) != tc.WantPayload { 1599 t.Fatalf("validSelfAuthenticatedPOST(%#v) = %q, want %q", tc.Request, string(gotPayload), tc.WantPayload) 1600 } 1601 gotThumb, _ := gotJWK.Thumbprint(crypto.SHA256) 1602 wantThumb, _ := tc.WantJWK.Thumbprint(crypto.SHA256) 1603 if !slices.Equal(gotThumb, wantThumb) { 1604 t.Fatalf("validSelfAuthenticatedPOST(%#v) = %#v, want %#v", tc.Request, gotThumb, wantThumb) 1605 } 1606 } else { 1607 berr, ok := gotErr.(*berrors.BoulderError) 1608 if !ok { 1609 t.Fatalf("validSelfAuthenticatedPOST(%#v) returned %T, want BoulderError", tc.Request, gotErr) 1610 } 1611 if berr.Type != tc.WantErrType { 1612 t.Errorf("validSelfAuthenticatedPOST(%#v) = %#v, want %#v", tc.Request, berr.Type, tc.WantErrType) 1613 } 1614 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1615 t.Errorf("validSelfAuthenticatedPOST(%#v) = %q, want %q", tc.Request, berr.Detail, tc.WantErrDetail) 1616 } 1617 test.AssertMetricWithLabelsEquals( 1618 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 1619 } 1620 }) 1621 } 1622 } 1623 1624 func TestMatchJWSURLs(t *testing.T) { 1625 wfe, _, signer := setupWFE(t) 1626 1627 noURLJWS, _, _ := signer.embeddedJWK(nil, "", "") 1628 urlAJWS, _, _ := signer.embeddedJWK(nil, "example.com", "") 1629 urlBJWS, _, _ := signer.embeddedJWK(nil, "example.org", "") 1630 1631 testCases := []struct { 1632 Name string 1633 Outer *jose.JSONWebSignature 1634 Inner *jose.JSONWebSignature 1635 WantErrType berrors.ErrorType 1636 WantErrDetail string 1637 WantStatType string 1638 }{ 1639 { 1640 Name: "Outer JWS without URL", 1641 Outer: noURLJWS, 1642 Inner: urlAJWS, 1643 WantErrType: berrors.Malformed, 1644 WantErrDetail: "Outer JWS header parameter 'url' required", 1645 WantStatType: "KeyRolloverOuterJWSNoURL", 1646 }, 1647 { 1648 Name: "Inner JWS without URL", 1649 Outer: urlAJWS, 1650 Inner: noURLJWS, 1651 WantErrType: berrors.Malformed, 1652 WantErrDetail: "Inner JWS header parameter 'url' required", 1653 WantStatType: "KeyRolloverInnerJWSNoURL", 1654 }, 1655 { 1656 Name: "Inner and outer JWS without URL", 1657 Outer: noURLJWS, 1658 Inner: noURLJWS, 1659 WantErrType: berrors.Malformed, 1660 WantErrDetail: "Outer JWS header parameter 'url' required", 1661 WantStatType: "KeyRolloverOuterJWSNoURL", 1662 }, 1663 { 1664 Name: "Mismatched inner and outer JWS URLs", 1665 Outer: urlAJWS, 1666 Inner: urlBJWS, 1667 WantErrType: berrors.Malformed, 1668 WantErrDetail: "Outer JWS 'url' value \"example.com\" does not match inner JWS 'url' value \"example.org\"", 1669 WantStatType: "KeyRolloverMismatchedURLs", 1670 }, 1671 { 1672 Name: "Matching inner and outer JWS URLs", 1673 Outer: urlAJWS, 1674 Inner: urlAJWS, 1675 }, 1676 } 1677 1678 for _, tc := range testCases { 1679 t.Run(tc.Name, func(t *testing.T) { 1680 wfe.stats.joseErrorCount.Reset() 1681 outer := tc.Outer.Signatures[0].Header 1682 inner := tc.Inner.Signatures[0].Header 1683 1684 gotErr := wfe.matchJWSURLs(outer, inner) 1685 if tc.WantErrDetail == "" { 1686 if gotErr != nil { 1687 t.Fatalf("matchJWSURLs(%#v, %#v) = %#v, want nil", outer, inner, gotErr) 1688 } 1689 } else { 1690 berr, ok := gotErr.(*berrors.BoulderError) 1691 if !ok { 1692 t.Fatalf("matchJWSURLs(%#v, %#v) returned %T, want BoulderError", outer, inner, gotErr) 1693 } 1694 if berr.Type != tc.WantErrType { 1695 t.Errorf("matchJWSURLs(%#v, %#v) = %#v, want %#v", outer, inner, berr.Type, tc.WantErrType) 1696 } 1697 if !strings.Contains(berr.Detail, tc.WantErrDetail) { 1698 t.Errorf("matchJWSURLs(%#v, %#v) = %q, want %q", outer, inner, berr.Detail, tc.WantErrDetail) 1699 } 1700 test.AssertMetricWithLabelsEquals( 1701 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.WantStatType}, 1) 1702 } 1703 }) 1704 } 1705 }