...

Source file src/github.com/letsencrypt/boulder/test/load-generator/boulder-calls.go

Documentation: github.com/letsencrypt/boulder/test/load-generator

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

View as plain text