github.com/letsencrypt/boulder@v0.20251208.0/test/load-generator/boulder-calls.go (about) 1 package main 2 3 import ( 4 "crypto" 5 "crypto/ecdsa" 6 "crypto/elliptic" 7 "crypto/rand" 8 "crypto/sha256" 9 "crypto/x509" 10 "encoding/base64" 11 "encoding/hex" 12 "encoding/json" 13 "encoding/pem" 14 "errors" 15 "fmt" 16 "io" 17 mrand "math/rand/v2" 18 "net/http" 19 "time" 20 21 "github.com/go-jose/go-jose/v4" 22 23 "github.com/letsencrypt/boulder/core" 24 "github.com/letsencrypt/boulder/identifier" 25 "github.com/letsencrypt/boulder/probs" 26 "github.com/letsencrypt/boulder/revocation" 27 "github.com/letsencrypt/boulder/test/load-generator/acme" 28 ) 29 30 var ( 31 // stringToOperation maps a configured plan action to a function that can 32 // operate on a state/context. 33 stringToOperation = map[string]func(*State, *acmeCache) error{ 34 "newAccount": newAccount, 35 "getAccount": getAccount, 36 "newOrder": newOrder, 37 "fulfillOrder": fulfillOrder, 38 "finalizeOrder": finalizeOrder, 39 "revokeCertificate": revokeCertificate, 40 } 41 ) 42 43 // OrderJSON is used because it's awkward to work with core.Order or corepb.Order 44 // when the API returns a different object than either of these types can represent without 45 // converting field values. The WFE uses an unexported `orderJSON` type for the 46 // API results that contain an order. We duplicate it here instead of moving it 47 // somewhere exported for this one utility. 48 type OrderJSON struct { 49 // The URL field isn't returned by the API, we populate it manually with the 50 // `Location` header. 51 URL string 52 Status core.AcmeStatus `json:"status"` 53 Expires time.Time `json:"expires"` 54 Identifiers identifier.ACMEIdentifiers `json:"identifiers"` 55 Authorizations []string `json:"authorizations"` 56 Finalize string `json:"finalize"` 57 Certificate string `json:"certificate,omitempty"` 58 Error *probs.ProblemDetails `json:"error,omitempty"` 59 } 60 61 // getAccount takes a randomly selected v2 account from `state.accts` and puts it 62 // into `c.acct`. The context `nonceSource` is also populated as convenience. 63 func getAccount(s *State, c *acmeCache) error { 64 s.rMu.RLock() 65 defer s.rMu.RUnlock() 66 67 // There must be an existing v2 account in the state 68 if len(s.accts) == 0 { 69 return errors.New("no accounts to return") 70 } 71 72 // Select a random account from the state and put it into the context 73 c.acct = s.accts[mrand.IntN(len(s.accts))] 74 c.ns = &nonceSource{s: s} 75 return nil 76 } 77 78 // newAccount puts a V2 account into the provided context. If the state provided 79 // has too many accounts already (based on `state.NumAccts` and `state.maxRegs`) 80 // then `newAccount` puts an existing account from the state into the context, 81 // otherwise it creates a new account and puts it into both the state and the 82 // context. 83 func newAccount(s *State, c *acmeCache) error { 84 // Check the max regs and if exceeded, just return an existing account instead 85 // of creating a new one. 86 if s.maxRegs != 0 && s.numAccts() >= s.maxRegs { 87 return getAccount(s, c) 88 } 89 90 // Create a random signing key 91 signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 92 if err != nil { 93 return err 94 } 95 c.acct = &account{ 96 key: signKey, 97 } 98 c.ns = &nonceSource{s: s} 99 100 // Prepare an account registration message body 101 reqBody := struct { 102 ToSAgreed bool `json:"termsOfServiceAgreed"` 103 Contact []string 104 }{ 105 ToSAgreed: true, 106 } 107 // Set the account contact email if configured 108 if s.email != "" { 109 reqBody.Contact = []string{fmt.Sprintf("mailto:%s", s.email)} 110 } 111 reqBodyStr, err := json.Marshal(&reqBody) 112 if err != nil { 113 return err 114 } 115 116 // Sign the new account registration body using a JWS with an embedded JWK 117 // because we do not have a key ID from the server yet. 118 newAccountURL := s.directory.EndpointURL(acme.NewAccountEndpoint) 119 jws, err := c.signEmbeddedV2Request(reqBodyStr, newAccountURL) 120 if err != nil { 121 return err 122 } 123 bodyBuf := []byte(jws.FullSerialize()) 124 125 resp, err := s.post( 126 newAccountURL, 127 bodyBuf, 128 c.ns, 129 string(acme.NewAccountEndpoint), 130 http.StatusCreated) 131 if err != nil { 132 return fmt.Errorf("%s, post failed: %s", newAccountURL, err) 133 } 134 defer resp.Body.Close() 135 136 // Populate the context account's key ID with the Location header returned by 137 // the server 138 locHeader := resp.Header.Get("Location") 139 if locHeader == "" { 140 return fmt.Errorf("%s, bad response - no Location header with account ID", newAccountURL) 141 } 142 c.acct.id = locHeader 143 144 // Add the account to the state 145 s.addAccount(c.acct) 146 return nil 147 } 148 149 // randDomain generates a random(-ish) domain name as a subdomain of the 150 // provided base domain. 151 func randDomain(base string) string { 152 // This approach will cause some repeat domains but not enough to make rate 153 // limits annoying! 154 var bytes [3]byte 155 _, _ = rand.Read(bytes[:]) 156 return hex.EncodeToString(bytes[:]) + base 157 } 158 159 // newOrder creates a new pending order object for a random set of domains using 160 // the context's account. 161 func newOrder(s *State, c *acmeCache) error { 162 // Pick a random number of names within the constraints of the maxNamesPerCert 163 // parameter 164 orderSize := 1 + mrand.IntN(s.maxNamesPerCert-1) 165 // Generate that many random domain names. There may be some duplicates, we 166 // don't care. The ACME server will collapse those down for us, how handy! 167 dnsNames := identifier.ACMEIdentifiers{} 168 for range orderSize { 169 dnsNames = append(dnsNames, identifier.NewDNS(randDomain(s.domainBase))) 170 } 171 172 // create the new order request object 173 initOrder := struct { 174 Identifiers identifier.ACMEIdentifiers 175 }{ 176 Identifiers: dnsNames, 177 } 178 initOrderStr, err := json.Marshal(&initOrder) 179 if err != nil { 180 return err 181 } 182 183 // Sign the new order request with the context account's key/key ID 184 newOrderURL := s.directory.EndpointURL(acme.NewOrderEndpoint) 185 jws, err := c.signKeyIDV2Request(initOrderStr, newOrderURL) 186 if err != nil { 187 return err 188 } 189 bodyBuf := []byte(jws.FullSerialize()) 190 191 resp, err := s.post( 192 newOrderURL, 193 bodyBuf, 194 c.ns, 195 string(acme.NewOrderEndpoint), 196 http.StatusCreated) 197 if err != nil { 198 return fmt.Errorf("%s, post failed: %s", newOrderURL, err) 199 } 200 defer resp.Body.Close() 201 body, err := io.ReadAll(resp.Body) 202 if err != nil { 203 return fmt.Errorf("%s, bad response: %s", newOrderURL, body) 204 } 205 206 // Unmarshal the Order object 207 var orderJSON OrderJSON 208 err = json.Unmarshal(body, &orderJSON) 209 if err != nil { 210 return err 211 } 212 213 // Populate the URL of the order from the Location header 214 orderURL := resp.Header.Get("Location") 215 if orderURL == "" { 216 return fmt.Errorf("%s, bad response - no Location header with order ID", newOrderURL) 217 } 218 orderJSON.URL = orderURL 219 220 // Store the pending order in the context 221 c.pendingOrders = append(c.pendingOrders, &orderJSON) 222 return nil 223 } 224 225 // popPendingOrder *removes* a random pendingOrder from the context, returning 226 // it. 227 func popPendingOrder(c *acmeCache) *OrderJSON { 228 orderIndex := mrand.IntN(len(c.pendingOrders)) 229 order := c.pendingOrders[orderIndex] 230 c.pendingOrders = append(c.pendingOrders[:orderIndex], c.pendingOrders[orderIndex+1:]...) 231 return order 232 } 233 234 // getAuthorization fetches an authorization by GET-ing the provided URL. It 235 // records the latency and result of the GET operation in the state. 236 func getAuthorization(s *State, c *acmeCache, url string) (*core.Authorization, error) { 237 latencyTag := "/acme/authz/{ID}" 238 resp, err := postAsGet(s, c, url, latencyTag) 239 // If there was an error, note the state and return 240 if err != nil { 241 return nil, fmt.Errorf("%s bad response: %s", url, err) 242 } 243 244 // Read the response body 245 defer resp.Body.Close() 246 body, err := io.ReadAll(resp.Body) 247 if err != nil { 248 return nil, err 249 } 250 251 // Unmarshal an authorization from the HTTP response body 252 var authz core.Authorization 253 err = json.Unmarshal(body, &authz) 254 if err != nil { 255 return nil, fmt.Errorf("%s response: %s", url, body) 256 } 257 // The Authorization ID is not set in the response so we populate it using the 258 // URL 259 authz.ID = url 260 return &authz, nil 261 } 262 263 // completeAuthorization processes a provided authorization by solving its 264 // HTTP-01 challenge using the context's account and the state's challenge 265 // server. Aftering POSTing the authorization's HTTP-01 challenge the 266 // authorization will be polled waiting for a state change. 267 func completeAuthorization(authz *core.Authorization, s *State, c *acmeCache) error { 268 // Skip if the authz isn't pending 269 if authz.Status != core.StatusPending { 270 return nil 271 } 272 273 // Find a challenge to solve from the pending authorization using the 274 // challenge selection strategy from the load-generator state. 275 chalToSolve, err := s.challStrat.PickChallenge(authz) 276 if err != nil { 277 return err 278 } 279 280 // Compute the key authorization from the context account's key 281 jwk := &jose.JSONWebKey{Key: &c.acct.key.PublicKey} 282 thumbprint, err := jwk.Thumbprint(crypto.SHA256) 283 if err != nil { 284 return err 285 } 286 authStr := fmt.Sprintf("%s.%s", chalToSolve.Token, base64.RawURLEncoding.EncodeToString(thumbprint)) 287 288 // Add the challenge response to the state's test server and defer a clean-up. 289 switch chalToSolve.Type { 290 case core.ChallengeTypeHTTP01: 291 s.challSrv.AddHTTPOneChallenge(chalToSolve.Token, authStr) 292 defer s.challSrv.DeleteHTTPOneChallenge(chalToSolve.Token) 293 case core.ChallengeTypeDNS01: 294 // Compute the digest of the key authorization 295 h := sha256.New() 296 h.Write([]byte(authStr)) 297 authorizedKeysDigest := base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 298 domain := "_acme-challenge." + authz.Identifier.Value + "." 299 s.challSrv.AddDNSOneChallenge(domain, authorizedKeysDigest) 300 defer s.challSrv.DeleteDNSOneChallenge(domain) 301 case core.ChallengeTypeTLSALPN01: 302 s.challSrv.AddTLSALPNChallenge(authz.Identifier.Value, authStr) 303 defer s.challSrv.DeleteTLSALPNChallenge(authz.Identifier.Value) 304 default: 305 return fmt.Errorf("challenge strategy picked challenge with unknown type: %q", chalToSolve.Type) 306 } 307 308 // Prepare the Challenge POST body 309 jws, err := c.signKeyIDV2Request([]byte(`{}`), chalToSolve.URL) 310 if err != nil { 311 return err 312 } 313 requestPayload := []byte(jws.FullSerialize()) 314 315 resp, err := s.post( 316 chalToSolve.URL, 317 requestPayload, 318 c.ns, 319 "/acme/challenge/{ID}", // We want all challenge POST latencies to be grouped 320 http.StatusOK, 321 ) 322 if err != nil { 323 return err 324 } 325 326 // Read the response body and cleanup when finished 327 defer resp.Body.Close() 328 _, err = io.ReadAll(resp.Body) 329 if err != nil { 330 return err 331 } 332 333 // Poll the authorization waiting for the challenge response to be recorded in 334 // a change of state. The polling may sleep and retry a few times if required 335 err = pollAuthorization(authz, s, c) 336 if err != nil { 337 return err 338 } 339 340 // The challenge is completed, the authz is valid 341 return nil 342 } 343 344 // pollAuthorization GETs a provided authorization up to three times, sleeping 345 // in between attempts, waiting for the status of the returned authorization to 346 // be valid. If the status is invalid, or if three GETs do not produce the 347 // correct authorization state an error is returned. If no error is returned 348 // then the authorization is valid and ready. 349 func pollAuthorization(authz *core.Authorization, s *State, c *acmeCache) error { 350 authzURL := authz.ID 351 for range 3 { 352 // Fetch the authz by its URL 353 authz, err := getAuthorization(s, c, authzURL) 354 if err != nil { 355 return nil 356 } 357 // If the authz is invalid, abort with an error 358 if authz.Status == "invalid" { 359 return fmt.Errorf("Authorization %q failed challenge and is status invalid", authzURL) 360 } 361 // If the authz is valid, return with no error - the authz is ready to go! 362 if authz.Status == "valid" { 363 return nil 364 } 365 // Otherwise sleep and try again 366 time.Sleep(3 * time.Second) 367 } 368 return fmt.Errorf("Timed out polling authorization %q", authzURL) 369 } 370 371 // fulfillOrder processes a pending order from the context, completing each 372 // authorization's HTTP-01 challenge using the context's account, and finally 373 // placing the now-ready-to-be-finalized order into the context's list of 374 // fulfilled orders. 375 func fulfillOrder(s *State, c *acmeCache) error { 376 // There must be at least one pending order in the context to fulfill 377 if len(c.pendingOrders) == 0 { 378 return errors.New("no pending orders to fulfill") 379 } 380 381 // Get an order to fulfill from the context 382 order := popPendingOrder(c) 383 384 // Each of its authorizations need to be processed 385 for _, url := range order.Authorizations { 386 // Fetch the authz by its URL 387 authz, err := getAuthorization(s, c, url) 388 if err != nil { 389 return err 390 } 391 392 // Complete the authorization by solving a challenge 393 err = completeAuthorization(authz, s, c) 394 if err != nil { 395 return err 396 } 397 } 398 399 // Once all of the authorizations have been fulfilled the order is fulfilled 400 // and ready for future finalization. 401 c.fulfilledOrders = append(c.fulfilledOrders, order.URL) 402 return nil 403 } 404 405 // getOrder GETs an order by URL, returning an OrderJSON object. It tracks the 406 // latency of the GET operation in the provided state. 407 func getOrder(s *State, c *acmeCache, url string) (*OrderJSON, error) { 408 latencyTag := "/acme/order/{ID}" 409 // POST-as-GET the order URL 410 resp, err := postAsGet(s, c, url, latencyTag) 411 // If there was an error, track that result 412 if err != nil { 413 return nil, fmt.Errorf("%s bad response: %s", url, err) 414 } 415 // Read the response body 416 defer resp.Body.Close() 417 body, err := io.ReadAll(resp.Body) 418 if err != nil { 419 return nil, fmt.Errorf("%s, bad response: %s", url, body) 420 } 421 422 // Unmarshal the Order object from the response body 423 var orderJSON OrderJSON 424 err = json.Unmarshal(body, &orderJSON) 425 if err != nil { 426 return nil, err 427 } 428 429 // Populate the order's URL based on the URL we fetched it from 430 orderJSON.URL = url 431 return &orderJSON, nil 432 } 433 434 // pollOrderForCert polls a provided order, waiting for the status to change to 435 // valid such that a certificate URL for the order is known. Three attempts are 436 // made to check the order status, sleeping 3s between each. If these attempts 437 // expire without the status becoming valid an error is returned. 438 func pollOrderForCert(order *OrderJSON, s *State, c *acmeCache) (*OrderJSON, error) { 439 for range 3 { 440 // Fetch the order by its URL 441 order, err := getOrder(s, c, order.URL) 442 if err != nil { 443 return nil, err 444 } 445 // If the order is invalid, fail 446 if order.Status == "invalid" { 447 return nil, fmt.Errorf("Order %q failed and is status invalid", order.URL) 448 } 449 // If the order is valid, return with no error - the authz is ready to go! 450 if order.Status == "valid" { 451 return order, nil 452 } 453 // Otherwise sleep and try again 454 time.Sleep(3 * time.Second) 455 } 456 return nil, fmt.Errorf("Timed out polling order %q", order.URL) 457 } 458 459 // popFulfilledOrder **removes** a fulfilled order from the context, returning 460 // it. Fulfilled orders have all of their authorizations satisfied. 461 func popFulfilledOrder(c *acmeCache) string { 462 orderIndex := mrand.IntN(len(c.fulfilledOrders)) 463 order := c.fulfilledOrders[orderIndex] 464 c.fulfilledOrders = append(c.fulfilledOrders[:orderIndex], c.fulfilledOrders[orderIndex+1:]...) 465 return order 466 } 467 468 // finalizeOrder removes a fulfilled order from the context and POSTs a CSR to 469 // the order's finalization URL. The CSR's key is set from the state's 470 // `certKey`. The order is then polled for the status to change to valid so that 471 // the certificate URL can be added to the context. The context's `certs` list 472 // is updated with the URL for the order's certificate. 473 func finalizeOrder(s *State, c *acmeCache) error { 474 // There must be at least one fulfilled order in the context 475 if len(c.fulfilledOrders) < 1 { 476 return errors.New("No fulfilled orders in the context ready to be finalized") 477 } 478 479 // Pop a fulfilled order to process, and then GET its contents 480 orderID := popFulfilledOrder(c) 481 order, err := getOrder(s, c, orderID) 482 if err != nil { 483 return err 484 } 485 486 if order.Status != core.StatusReady { 487 return fmt.Errorf("order %s was status %q, expected %q", 488 orderID, order.Status, core.StatusReady) 489 } 490 491 // Mark down the finalization URL for the order 492 finalizeURL := order.Finalize 493 494 // Pull the values from the order identifiers for use in the CSR 495 dnsNames := make([]string, len(order.Identifiers)) 496 for i, ident := range order.Identifiers { 497 dnsNames[i] = ident.Value 498 } 499 500 // Create a CSR using the state's certKey 501 csr, err := x509.CreateCertificateRequest( 502 rand.Reader, 503 &x509.CertificateRequest{DNSNames: dnsNames}, 504 s.certKey, 505 ) 506 if err != nil { 507 return err 508 } 509 510 // Create the finalization request body with the encoded CSR 511 request := fmt.Sprintf( 512 `{"csr":"%s"}`, 513 base64.RawURLEncoding.EncodeToString(csr), 514 ) 515 516 // Sign the request body with the context's account key/keyID 517 jws, err := c.signKeyIDV2Request([]byte(request), finalizeURL) 518 if err != nil { 519 return err 520 } 521 requestPayload := []byte(jws.FullSerialize()) 522 523 resp, err := s.post( 524 finalizeURL, 525 requestPayload, 526 c.ns, 527 "/acme/order/finalize", // We want all order finalizations to be grouped. 528 http.StatusOK, 529 ) 530 if err != nil { 531 return err 532 } 533 defer resp.Body.Close() 534 // Read the body to ensure there isn't an error. We don't need the actual 535 // contents. 536 _, err = io.ReadAll(resp.Body) 537 if err != nil { 538 return err 539 } 540 541 // Poll the order waiting for the certificate to be ready 542 completedOrder, err := pollOrderForCert(order, s, c) 543 if err != nil { 544 return err 545 } 546 547 // The valid order should have a certificate URL 548 certURL := completedOrder.Certificate 549 if certURL == "" { 550 return fmt.Errorf("Order %q was finalized but has no cert URL", order.URL) 551 } 552 553 // Append the certificate URL into the context's list of certificates 554 c.certs = append(c.certs, certURL) 555 c.finalizedOrders = append(c.finalizedOrders, order.URL) 556 return nil 557 } 558 559 // postAsGet performs a POST-as-GET request to the provided URL authenticated by 560 // the context's account. A HTTP status code other than StatusOK (200) 561 // in response to a POST-as-GET request is considered an error. The caller is 562 // responsible for closing the HTTP response body. 563 // 564 // See RFC 8555 Section 6.3 for more information on POST-as-GET requests. 565 func postAsGet(s *State, c *acmeCache, url string, latencyTag string) (*http.Response, error) { 566 // Create the POST-as-GET request JWS 567 jws, err := c.signKeyIDV2Request([]byte(""), url) 568 if err != nil { 569 return nil, err 570 } 571 requestPayload := []byte(jws.FullSerialize()) 572 573 return s.post(url, requestPayload, c.ns, latencyTag, http.StatusOK) 574 } 575 576 func popCertificate(c *acmeCache) string { 577 certIndex := mrand.IntN(len(c.certs)) 578 certURL := c.certs[certIndex] 579 c.certs = append(c.certs[:certIndex], c.certs[certIndex+1:]...) 580 return certURL 581 } 582 583 func getCert(s *State, c *acmeCache, url string) ([]byte, error) { 584 latencyTag := "/acme/cert/{serial}" 585 resp, err := postAsGet(s, c, url, latencyTag) 586 if err != nil { 587 return nil, fmt.Errorf("%s bad response: %s", url, err) 588 } 589 defer resp.Body.Close() 590 return io.ReadAll(resp.Body) 591 } 592 593 // revokeCertificate removes a certificate url from the context, retrieves it, 594 // and sends a revocation request for the certificate to the ACME server. 595 // The revocation request is signed with the account key rather than the certificate 596 // key. 597 func revokeCertificate(s *State, c *acmeCache) error { 598 if len(c.certs) < 1 { 599 return errors.New("No certificates in the context that can be revoked") 600 } 601 602 if r := mrand.Float32(); r > s.revokeChance { 603 return nil 604 } 605 606 certURL := popCertificate(c) 607 certPEM, err := getCert(s, c, certURL) 608 if err != nil { 609 return err 610 } 611 612 pemBlock, _ := pem.Decode(certPEM) 613 revokeObj := struct { 614 Certificate string 615 Reason revocation.Reason 616 }{ 617 Certificate: base64.URLEncoding.EncodeToString(pemBlock.Bytes), 618 Reason: revocation.Unspecified, 619 } 620 621 revokeJSON, err := json.Marshal(revokeObj) 622 if err != nil { 623 return err 624 } 625 revokeURL := s.directory.EndpointURL(acme.RevokeCertEndpoint) 626 // TODO(roland): randomly use the certificate key to sign the request instead of 627 // the account key 628 jws, err := c.signKeyIDV2Request(revokeJSON, revokeURL) 629 if err != nil { 630 return err 631 } 632 requestPayload := []byte(jws.FullSerialize()) 633 634 resp, err := s.post( 635 revokeURL, 636 requestPayload, 637 c.ns, 638 "/acme/revoke-cert", 639 http.StatusOK, 640 ) 641 if err != nil { 642 return err 643 } 644 defer resp.Body.Close() 645 646 _, err = io.ReadAll(resp.Body) 647 if err != nil { 648 return err 649 } 650 651 return nil 652 }