ACMEv2 support for load-generator. (#3479)
This commit adds new ACMEv2 actions for the load-generator and an example configuration for load testing ACME v2 against a local boulder instance. The load-generator's existing V1 code remains but the two ACME versions can not be intermixed in the same load generator plan. The load generator should be making 100% ACME v1 action calls or 100% ACME v2 action calls. Follow-up work: * Adding a short load generator run for V1 and V2 to the integration tests/CI * Randomizing the # of names in pending orders - right now they are always 1 identifier orders. * Making it easier to save state on an initial run without needing to create an empty JSON file * DNS-01 support & Wildcard names I'm going to start chipping at the "Follow-up" items but wanted to get the meat of this PR up for review now. Resolves #3137
This commit is contained in:
parent
2956b0c938
commit
b99907d4a9
|
|
@ -20,12 +20,17 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/letsencrypt/boulder/core"
|
||||
"github.com/letsencrypt/boulder/probs"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// stringToOperation maps a configured plan action to a function that can
|
||||
// operate on a state/context. V2 and V1 operations can **not** be intermixed
|
||||
// in the same plan.
|
||||
stringToOperation = map[string]func(*State, *context) error{
|
||||
/* ACME v1 Operations */
|
||||
"newRegistration": newRegistration,
|
||||
"getRegistration": getRegistration,
|
||||
"newAuthorization": newAuthorization,
|
||||
|
|
@ -33,16 +38,167 @@ var (
|
|||
"solveTLSOne": solveTLSOne,
|
||||
"newCertificate": newCertificate,
|
||||
"revokeCertificate": revokeCertificate,
|
||||
|
||||
/* ACME v2 Operations */
|
||||
"newAccount": newAccount,
|
||||
"getAccount": getAccount,
|
||||
"newOrder": newOrder,
|
||||
"fulfillOrder": fulfillOrder,
|
||||
"finalizeOrder": finalizeOrder,
|
||||
}
|
||||
)
|
||||
|
||||
var plainReg = []byte(`{"resource":"new-reg"}`)
|
||||
// API path constants
|
||||
const (
|
||||
newAcctPath = "/acme/new-acct"
|
||||
newOrderPath = "/acme/new-order"
|
||||
newRegPath = "/acme/new-reg"
|
||||
newAuthzPath = "/acme/new-authz"
|
||||
challengePath = "/acme/challenge"
|
||||
newCertPath = "/acme/new-cert"
|
||||
revokeCertPath = "/acme/revoke-cert"
|
||||
)
|
||||
|
||||
var newRegPath = "/acme/new-reg"
|
||||
var challengePath = "/acme/challenge"
|
||||
var newCertPath = "/acme/new-cert"
|
||||
var revokeCertPath = "/acme/revoke-cert"
|
||||
// It's awkward to work with core.Order or corepb.Order when the API returns
|
||||
// a different object than either of these types can represent without
|
||||
// converting field values. The WFE uses an unexported `orderJSON` type for the
|
||||
// API results that contain an order. We duplicate it here instead of moving it
|
||||
// somewhere exported for this one utility.
|
||||
type OrderJSON struct {
|
||||
// The URL field isn't returned by the API, we populate it manually with the
|
||||
// `Location` header.
|
||||
URL string
|
||||
Status core.AcmeStatus `json:"status"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Identifiers []core.AcmeIdentifier `json:"identifiers"`
|
||||
Authorizations []string `json:"authorizations"`
|
||||
Finalize string `json:"finalize"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
Error *probs.ProblemDetails `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// getAccount takes a randomly selected v2 account from `state.accts` and puts it
|
||||
// into `ctx.acct`. The context `nonceSource` is also populated as convenience.
|
||||
func getAccount(s *State, ctx *context) error {
|
||||
s.rMu.RLock()
|
||||
defer s.rMu.RUnlock()
|
||||
|
||||
// There must be an existing v2 account in the state
|
||||
if len(s.accts) == 0 {
|
||||
return errors.New("no accounts to return")
|
||||
}
|
||||
|
||||
// Select a random account from the state and put it into the context
|
||||
ctx.acct = s.accts[mrand.Intn(len(s.accts))]
|
||||
ctx.ns = &nonceSource{s: s}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRegistration takes an existing v1 account from `state.regs` and puts it
|
||||
// into `ctx.reg`. The context `nonceSource` is also populated as convenience.
|
||||
func getRegistration(s *State, ctx *context) error {
|
||||
s.rMu.RLock()
|
||||
defer s.rMu.RUnlock()
|
||||
|
||||
// There must be an existing v1 registration in the state
|
||||
if len(s.regs) == 0 {
|
||||
return errors.New("no registrations to return")
|
||||
}
|
||||
|
||||
// Select a random registration from the state and put it into the context
|
||||
ctx.reg = s.regs[mrand.Intn(len(s.regs))]
|
||||
ctx.ns = &nonceSource{s: s}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newAccount puts a V2 account into the provided context. If the state provided
|
||||
// has too many accounts already (based on `state.NumAccts` and `state.maxRegs`)
|
||||
// then `newAccount` puts an existing account from the state into the context,
|
||||
// otherwise it creates a new account and puts it into both the state and the
|
||||
// context.
|
||||
func newAccount(s *State, ctx *context) error {
|
||||
// Check the max regs and if exceeded, just return an existing account instead
|
||||
// of creating a new one.
|
||||
if s.maxRegs != 0 && s.numAccts() >= s.maxRegs {
|
||||
return getAccount(s, ctx)
|
||||
}
|
||||
|
||||
// Create a random signing key
|
||||
signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.acct = &account{
|
||||
key: signKey,
|
||||
}
|
||||
ctx.ns = &nonceSource{s: s}
|
||||
|
||||
// Prepare an account registration message body
|
||||
reqBody := struct {
|
||||
ToSAgreed bool `json:"termsOfServiceAgreed"`
|
||||
Contact []string
|
||||
}{
|
||||
ToSAgreed: true,
|
||||
}
|
||||
// Set the account contact email if configured
|
||||
if s.email != "" {
|
||||
reqBody.Contact = []string{fmt.Sprintf("mailto:%s", s.email)}
|
||||
}
|
||||
reqBodyStr, err := json.Marshal(&reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sign the new account registration body using a JWS with an embedded JWK
|
||||
// because we do not have a key ID from the server yet.
|
||||
jws, err := ctx.signEmbeddedV2Request(reqBodyStr, fmt.Sprintf("%s%s", s.apiBase, newAcctPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyBuf := []byte(jws.FullSerialize())
|
||||
|
||||
// POST the account creation request to the server
|
||||
nStarted := time.Now()
|
||||
resp, err := s.post(fmt.Sprintf("%s%s", s.apiBase, newAcctPath), bodyBuf, ctx.ns)
|
||||
nFinished := time.Now()
|
||||
nState := "error"
|
||||
defer func() {
|
||||
s.callLatency.Add(
|
||||
fmt.Sprintf("POST %s", newAcctPath), nStarted, nFinished, nState)
|
||||
}()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, post failed: %s", newAcctPath, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// We expect that the result is a created account
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, bad response: %s", newAcctPath, body)
|
||||
}
|
||||
return fmt.Errorf("%s, bad response status %d: %s", newAcctPath, resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// Populate the context account's key ID with the Location header returned by
|
||||
// the server
|
||||
locHeader := resp.Header.Get("Location")
|
||||
if locHeader == "" {
|
||||
return fmt.Errorf("%s, bad response - no Location header with account ID", newAcctPath)
|
||||
}
|
||||
ctx.acct.id = locHeader
|
||||
|
||||
// Add the account to the state
|
||||
nState = "good"
|
||||
s.addAccount(ctx.acct)
|
||||
return nil
|
||||
}
|
||||
|
||||
// newRegistration puts a V1 registration into the provided context. If the
|
||||
// state provided has too many registrations already (based on `state.NumAccts`
|
||||
// and `state.maxRegs`) then `newRegistration` puts an existing registration
|
||||
// from the state into the context otherwise it creates a new registration and
|
||||
// puts it into both the state and the context.
|
||||
func newRegistration(s *State, ctx *context) error {
|
||||
// if we have generated the max number of registrations just become getRegistration
|
||||
if s.maxRegs != 0 && s.numRegs() >= s.maxRegs {
|
||||
|
|
@ -72,10 +228,10 @@ func newRegistration(s *State, ctx *context) error {
|
|||
if s.email != "" {
|
||||
regStr = []byte(fmt.Sprintf(`{"resource":"new-reg","contact":["mailto:%s"]}`, s.email))
|
||||
} else {
|
||||
regStr = plainReg
|
||||
regStr = []byte(`{"resource":"new-reg"}`)
|
||||
}
|
||||
// build the JWS object
|
||||
requestPayload, err := s.signWithNonce(newRegPath, true, regStr, signer)
|
||||
requestPayload, err := s.signWithNonce(regStr, signer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("/acme/new-reg, sign failed: %s", err)
|
||||
}
|
||||
|
|
@ -113,7 +269,7 @@ func newRegistration(s *State, ctx *context) error {
|
|||
regStr = []byte(fmt.Sprintf(`{"resource":"reg","agreement":"%s"}`, terms))
|
||||
|
||||
// build the JWS object
|
||||
requestPayload, err = s.signWithNonce("/acme/reg", false, regStr, signer)
|
||||
requestPayload, err = s.signWithNonce(regStr, signer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("/acme/reg, sign failed: %s", err)
|
||||
}
|
||||
|
|
@ -143,23 +299,102 @@ func newRegistration(s *State, ctx *context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func newAuthorization(s *State, ctx *context) error {
|
||||
// generate a random(-ish) domain name, will cause some multiples but not enough to make rate limits annoying!
|
||||
// randDomain generates a random(-ish) domain name as a subdomain of the
|
||||
// provided base domain.
|
||||
func randDomain(base string) string {
|
||||
// This approach will cause some repeat domains but not enough to make rate
|
||||
// limits annoying!
|
||||
n := time.Now().UnixNano()
|
||||
b := new(bytes.Buffer)
|
||||
binary.Write(b, binary.LittleEndian, n)
|
||||
randomDomain := fmt.Sprintf("%x.%s", sha1.Sum(b.Bytes()), s.domainBase)
|
||||
return fmt.Sprintf("%x.%s", sha1.Sum(b.Bytes()), base)
|
||||
}
|
||||
|
||||
// create the registration object
|
||||
// newOrder creates a new pending order object for a random set of domains using
|
||||
// the context's account.
|
||||
func newOrder(s *State, ctx *context) error {
|
||||
// generate a random(-ish) domain name, will cause some multiples but not enough to make rate limits annoying!
|
||||
randomDomain := randDomain(s.domainBase)
|
||||
|
||||
// create the new order request object
|
||||
initOrder := struct {
|
||||
Identifiers []core.AcmeIdentifier
|
||||
}{
|
||||
Identifiers: []core.AcmeIdentifier{
|
||||
{
|
||||
Type: core.IdentifierDNS,
|
||||
Value: randomDomain,
|
||||
},
|
||||
},
|
||||
}
|
||||
initOrderStr, err := json.Marshal(&initOrder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sign the new order request with the context account's key/key ID
|
||||
url := fmt.Sprintf("%s%s", s.apiBase, newOrderPath)
|
||||
jws, err := ctx.signKeyIDV2Request(initOrderStr, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyBuf := []byte(jws.FullSerialize())
|
||||
|
||||
// POST the new-order endpoint
|
||||
nStarted := time.Now()
|
||||
resp, err := s.post(url, bodyBuf, ctx.ns)
|
||||
nFinished := time.Now()
|
||||
nState := "error"
|
||||
defer func() {
|
||||
s.callLatency.Add(
|
||||
fmt.Sprintf("POST %s", newOrderPath), nStarted, nFinished, nState)
|
||||
}()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, post failed: %s", newOrderPath, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s, bad response: %s", newOrderPath, body)
|
||||
}
|
||||
|
||||
// We expect that the result is a created order
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return fmt.Errorf("%s, bad response status %d: %s", newOrderPath, resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// Unmarshal the Order object
|
||||
var orderJSON OrderJSON
|
||||
err = json.Unmarshal(body, &orderJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Populate the URL of the order from the Location header
|
||||
orderURL := resp.Header.Get("Location")
|
||||
if orderURL == "" {
|
||||
return fmt.Errorf("%s, bad response - no Location header with order ID", newOrderPath)
|
||||
}
|
||||
orderJSON.URL = orderURL
|
||||
|
||||
// Store the pending order in the context
|
||||
ctx.pendingOrders = append(ctx.pendingOrders, &orderJSON)
|
||||
nState = "good"
|
||||
return nil
|
||||
}
|
||||
|
||||
// newAuthorization creates a new authz for a random domain name using the
|
||||
// context's registration. The resulting pending authorization is stored in the
|
||||
// context's list of pending authorizations.
|
||||
func newAuthorization(s *State, ctx *context) error {
|
||||
// generate a random(-ish) domain name, will cause some multiples but not enough to make rate limits annoying!
|
||||
randomDomain := randDomain(s.domainBase)
|
||||
|
||||
// create the new-authz object
|
||||
initAuth := fmt.Sprintf(`{"resource":"new-authz","identifier":{"type":"dns","value":"%s"}}`, randomDomain)
|
||||
|
||||
// build the JWS object
|
||||
getNew := false
|
||||
// do a coin flip to decide whether to get a new nonce via HEAD
|
||||
if mrand.Intn(1) == 0 {
|
||||
getNew = true
|
||||
}
|
||||
requestPayload, err := s.signWithNonce("/acme/new-authz", getNew, []byte(initAuth), ctx.reg.signer)
|
||||
requestPayload, err := s.signWithNonce([]byte(initAuth), ctx.reg.signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -203,6 +438,194 @@ func newAuthorization(s *State, ctx *context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// popPendingOrder *removes* a random pendingOrder from the context, returning
|
||||
// it.
|
||||
func popPendingOrder(ctx *context) *OrderJSON {
|
||||
orderIndex := mrand.Intn(len(ctx.pendingOrders))
|
||||
order := ctx.pendingOrders[orderIndex]
|
||||
ctx.pendingOrders = append(ctx.pendingOrders[:orderIndex], ctx.pendingOrders[orderIndex+1:]...)
|
||||
return order
|
||||
}
|
||||
|
||||
// getAuthorization fetches an authorization by GETing the provided URL. It
|
||||
// records the latency and result of the GET operation in the state.
|
||||
func getAuthorization(s *State, url string) (*core.Authorization, error) {
|
||||
// GET the provided URL, tracking elapsed time
|
||||
aStarted := time.Now()
|
||||
resp, err := s.get(url)
|
||||
aFinished := time.Now()
|
||||
aState := "error"
|
||||
// Defer logging the latency and result
|
||||
defer func() {
|
||||
s.callLatency.Add("GET /acme/authz/{ID}", aStarted, aFinished, aState)
|
||||
}()
|
||||
// If there was an error, note the state and return
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s bad response: %s", url, err)
|
||||
}
|
||||
|
||||
// Read the response body
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal an authorization from the HTTP response body
|
||||
var authz core.Authorization
|
||||
err = json.Unmarshal(body, &authz)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s response: %s", url, body)
|
||||
}
|
||||
// The Authorization ID is not set in the response so we populate it based on
|
||||
// the URL
|
||||
paths := strings.Split(url, "/")
|
||||
authz.ID = paths[len(paths)-1]
|
||||
aState = "good"
|
||||
return &authz, nil
|
||||
}
|
||||
|
||||
// completeAuthorization processes a provided authorization by solving its
|
||||
// HTTP-01 challenge using the context's account and the state's challenge
|
||||
// server. Aftering POSTing the authorization's HTTP-01 challenge the
|
||||
// authorization will be polled waiting for a state change.
|
||||
func completeAuthorization(authz *core.Authorization, s *State, ctx *context) error {
|
||||
// Skip if the authz isn't pending
|
||||
if authz.Status != core.StatusPending {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find a challenge to solve from the pending authorization. For now, we only
|
||||
// process HTTP-01 challenges and must error if there isn't a HTTP-01
|
||||
// challenge to solve.
|
||||
var chalToSolve *core.Challenge
|
||||
for _, challenge := range authz.Challenges {
|
||||
if challenge.Type == core.ChallengeTypeHTTP01 {
|
||||
chalToSolve = &challenge
|
||||
break
|
||||
}
|
||||
}
|
||||
if chalToSolve == nil {
|
||||
return errors.New("no http-01 challenges to complete")
|
||||
}
|
||||
|
||||
// Compute the key authorization from the context account's key
|
||||
jwk := &jose.JSONWebKey{Key: &ctx.acct.key.PublicKey}
|
||||
thumbprint, err := jwk.Thumbprint(crypto.SHA256)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authStr := fmt.Sprintf("%s.%s", chalToSolve.Token, base64.RawURLEncoding.EncodeToString(thumbprint))
|
||||
|
||||
// Add the challenge response to the state's test server
|
||||
s.challSrv.addHTTPOneChallenge(chalToSolve.Token, authStr)
|
||||
// Clean up after we're done
|
||||
defer s.challSrv.deleteHTTPOneChallenge(chalToSolve.Token)
|
||||
|
||||
// Prepare the Challenge POST body
|
||||
update := fmt.Sprintf(`{"keyAuthorization":"%s"}`, authStr)
|
||||
jws, err := ctx.signKeyIDV2Request([]byte(update), chalToSolve.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestPayload := []byte(jws.FullSerialize())
|
||||
|
||||
// POST the challenge update to begin the challenge process
|
||||
cStarted := time.Now()
|
||||
resp, err := s.post(chalToSolve.URL, requestPayload, ctx.ns)
|
||||
cFinished := time.Now()
|
||||
cState := "error"
|
||||
// Record the final latency and state when finished
|
||||
defer func() {
|
||||
s.callLatency.Add("POST /acme/challenge/{ID}", cStarted, cFinished, cState)
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the response body and cleanup when finished
|
||||
defer resp.Body.Close()
|
||||
_, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The response code is expected to be Status OK
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("Unexpected HTTP response code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Poll the authorization waiting for the challenge response to be recorded in
|
||||
// a change of state. The polling may sleep and retry a few times if required
|
||||
pollAuthorization(authz, s, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The challenge is completed, the authz is valid
|
||||
cState = "good"
|
||||
return nil
|
||||
}
|
||||
|
||||
// pollAuthorization GETs a provided authorization up to three times, sleeping
|
||||
// in between attempts, waiting for the status of the returned authorization to
|
||||
// be valid. If the status is invalid, or if three GETs do not produce the
|
||||
// correct authorization state an error is returned. If no error is returned
|
||||
// then the authorization is valid and ready.
|
||||
func pollAuthorization(authz *core.Authorization, s *State, ctx *context) error {
|
||||
authzURL := fmt.Sprintf("%s/acme/authz/%s", s.apiBase, authz.ID)
|
||||
for i := 0; i < 3; i++ {
|
||||
// Fetch the authz by its URL
|
||||
authz, err := getAuthorization(s, authzURL)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// If the authz is invalid, abort with an error
|
||||
if authz.Status == "invalid" {
|
||||
return fmt.Errorf("Authorization %q failed challenge and is status invalid", authzURL)
|
||||
}
|
||||
// If the authz is valid, return with no error - the authz is ready to go!
|
||||
if authz.Status == "valid" {
|
||||
return nil
|
||||
}
|
||||
// Otherwise sleep and try again
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
return fmt.Errorf("Timed out polling authorization %q", authzURL)
|
||||
}
|
||||
|
||||
// fulfillOrder processes a pending order from the context, completing each
|
||||
// authorization's HTTP-01 challenge using the context's account, and finally
|
||||
// placing the now-ready-to-be-finalized order into the context's list of
|
||||
// fulfilled orders.
|
||||
func fulfillOrder(s *State, ctx *context) error {
|
||||
// There must be at least one pending order in the context to fulfill
|
||||
if len(ctx.pendingOrders) == 0 {
|
||||
return errors.New("no pending orders to fulfill")
|
||||
}
|
||||
|
||||
// Get an order to fulfill from the context
|
||||
order := popPendingOrder(ctx)
|
||||
|
||||
// Each of its authorizations need to be processed
|
||||
for _, url := range order.Authorizations {
|
||||
// Fetch the authz by its URL
|
||||
authz, err := getAuthorization(s, url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Complete the authorization by solving a challenge
|
||||
completeAuthorization(authz, s, ctx)
|
||||
}
|
||||
|
||||
// Once all of the authorizations have been fulfilled the order is fulfilled
|
||||
// and ready for future finalization.
|
||||
ctx.fulfilledOrders = append(ctx.fulfilledOrders, order.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// popPending **removes** a random pending authorization from the context,
|
||||
// returning it.
|
||||
func popPending(ctx *context) *core.Authorization {
|
||||
authzIndex := mrand.Intn(len(ctx.pendingAuthz))
|
||||
authz := ctx.pendingAuthz[authzIndex]
|
||||
|
|
@ -210,6 +633,8 @@ func popPending(ctx *context) *core.Authorization {
|
|||
return authz
|
||||
}
|
||||
|
||||
// solveHTTPOne solves a pending authorization's HTTP-01 challenge. It polls the
|
||||
// authorization waiting for the status to change to valid.
|
||||
func solveHTTPOne(s *State, ctx *context) error {
|
||||
if len(ctx.pendingAuthz) == 0 {
|
||||
return errors.New("no pending authorizations to complete")
|
||||
|
|
@ -236,7 +661,7 @@ func solveHTTPOne(s *State, ctx *context) error {
|
|||
defer s.challSrv.deleteHTTPOneChallenge(chall.Token)
|
||||
|
||||
update := fmt.Sprintf(`{"resource":"challenge","keyAuthorization":"%s"}`, authStr)
|
||||
requestPayload, err := s.signWithNonce(challengePath, false, []byte(update), ctx.reg.signer)
|
||||
requestPayload, err := s.signWithNonce([]byte(update), ctx.reg.signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -296,6 +721,9 @@ func solveHTTPOne(s *State, ctx *context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// solveTLSOne removes a pending authorization from the context and solves the
|
||||
// TLS-SNI-01 challenge associated with it, polling until the authorization
|
||||
// changes state.
|
||||
func solveTLSOne(s *State, ctx *context) error {
|
||||
if len(ctx.pendingAuthz) == 0 {
|
||||
return errors.New("no pending authorizations to complete")
|
||||
|
|
@ -320,7 +748,7 @@ func solveTLSOne(s *State, ctx *context) error {
|
|||
authStr := fmt.Sprintf("%s.%s", chall.Token, base64.RawURLEncoding.EncodeToString(thumbprint))
|
||||
|
||||
update := fmt.Sprintf(`{"resource":"challenge","keyAuthorization":"%s"}`, authStr)
|
||||
requestPayload, err := s.signWithNonce(challengePath, false, []byte(update), ctx.reg.signer)
|
||||
requestPayload, err := s.signWithNonce([]byte(update), ctx.reg.signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -380,6 +808,173 @@ func solveTLSOne(s *State, ctx *context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// getOrder GETs an order by URL, returning an OrderJSON object. It tracks the
|
||||
// latency of the GET operation in the provided state.
|
||||
func getOrder(s *State, url string) (*OrderJSON, error) {
|
||||
// GET the order URL
|
||||
aStarted := time.Now()
|
||||
resp, err := s.get(url)
|
||||
aFinished := time.Now()
|
||||
aState := "error"
|
||||
// Track the latency and result
|
||||
defer func() {
|
||||
s.callLatency.Add("GET /acme/order/{ID}", aStarted, aFinished, aState)
|
||||
}()
|
||||
// If there was an error, track that result
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s bad response: %s", url, err)
|
||||
}
|
||||
// Read the response body
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s, bad response: %s", newOrderPath, body)
|
||||
}
|
||||
|
||||
// We expect a HTTP status OK response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%s, bad response status %d: %s", newOrderPath, resp.StatusCode, body)
|
||||
}
|
||||
|
||||
// Unmarshal the Order object from the response body
|
||||
var orderJSON OrderJSON
|
||||
err = json.Unmarshal(body, &orderJSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate the order's URL based on the URL we fetched it from
|
||||
orderJSON.URL = url
|
||||
aState = "good"
|
||||
return &orderJSON, nil
|
||||
}
|
||||
|
||||
// pollOrderForCert polls a provided order, waiting for the status to change to
|
||||
// valid such that a certificate URL for the order is known. Three attempts are
|
||||
// made to check the order status, sleeping 3s between each. If these attempts
|
||||
// expire without the status becoming valid an error is returned.
|
||||
func pollOrderForCert(order *OrderJSON, s *State, ctx *context) (*OrderJSON, error) {
|
||||
for i := 0; i < 3; i++ {
|
||||
// Fetch the order by its URL
|
||||
order, err := getOrder(s, order.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the order is invalid, fail
|
||||
if order.Status == "invalid" {
|
||||
return nil, fmt.Errorf("Order %q failed and is status invalid", order.URL)
|
||||
}
|
||||
// If the order is valid, return with no error - the authz is ready to go!
|
||||
if order.Status == "valid" {
|
||||
return order, nil
|
||||
}
|
||||
// Otherwise sleep and try again
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
return nil, fmt.Errorf("Timed out polling order %q", order.URL)
|
||||
}
|
||||
|
||||
// popFulfilledOrder **removes** a fulfilled order from the context, returning
|
||||
// it. Fulfilled orders have all of their authorizations satisfied.
|
||||
func popFulfilledOrder(ctx *context) string {
|
||||
orderIndex := mrand.Intn(len(ctx.fulfilledOrders))
|
||||
order := ctx.fulfilledOrders[orderIndex]
|
||||
ctx.fulfilledOrders = append(ctx.fulfilledOrders[:orderIndex], ctx.fulfilledOrders[orderIndex+1:]...)
|
||||
return order
|
||||
}
|
||||
|
||||
// finalizeOrder removes a fulfilled order from the context and POSTs a CSR to
|
||||
// the order's finalization URL. The CSR's key is set from the state's
|
||||
// `certKey`. The order is then polled for the status to change to valid so that
|
||||
// the certificate URL can be added to the context. The context's `certs` list
|
||||
// is updated with the URL for the order's certificate.
|
||||
func finalizeOrder(s *State, ctx *context) error {
|
||||
// There must be at least one fulfilled order in the context
|
||||
if len(ctx.fulfilledOrders) < 1 {
|
||||
return fmt.Errorf("No fulfilled orders in the context ready to be finalized")
|
||||
}
|
||||
|
||||
// Pop a fulfilled order to process, and then GET its contents
|
||||
orderID := popFulfilledOrder(ctx)
|
||||
order, err := getOrder(s, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Mark down the finalization URL for the order
|
||||
finalizeURL := order.Finalize
|
||||
|
||||
// Pull the values from the order identifiers for use in the CSR
|
||||
dnsNames := make([]string, len(order.Identifiers))
|
||||
for i, ident := range order.Identifiers {
|
||||
dnsNames[i] = ident.Value
|
||||
}
|
||||
|
||||
// Create a CSR using the state's certKey
|
||||
csr, err := x509.CreateCertificateRequest(
|
||||
rand.Reader,
|
||||
&x509.CertificateRequest{DNSNames: dnsNames},
|
||||
s.certKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the finalization request body with the encoded CSR
|
||||
request := fmt.Sprintf(
|
||||
`{"csr":"%s"}`,
|
||||
base64.URLEncoding.EncodeToString(csr),
|
||||
)
|
||||
|
||||
// Sign the request body with the context's account key/keyID
|
||||
jws, err := ctx.signKeyIDV2Request([]byte(request), finalizeURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestPayload := []byte(jws.FullSerialize())
|
||||
|
||||
// POST the finalization URL for the order
|
||||
started := time.Now()
|
||||
resp, err := s.post(finalizeURL, requestPayload, ctx.ns)
|
||||
finished := time.Now()
|
||||
state := "error"
|
||||
// Track the latency and the result state
|
||||
defer func() {
|
||||
s.callLatency.Add("POST /acme/order/finalize", started, finished, state)
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad response, status %d", resp.StatusCode)
|
||||
}
|
||||
// Read the body to ensure there isn't an error. We don't need the actual
|
||||
// contents.
|
||||
_, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Poll the order waiting for the certificate to be ready
|
||||
completedOrder, err := pollOrderForCert(order, s, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The valid order should have a certificate URL
|
||||
certURL := completedOrder.Certificate
|
||||
if certURL == "" {
|
||||
return fmt.Errorf("Order %q was finalized but has no cert URL", order.URL)
|
||||
}
|
||||
|
||||
// Append the certificate URL into the context's list of certificates
|
||||
ctx.certs = append(ctx.certs, certURL)
|
||||
ctx.finalizedOrders = append(ctx.finalizedOrders, order.URL)
|
||||
state = "good"
|
||||
return nil
|
||||
}
|
||||
|
||||
// min returns the smaller of the two inputs
|
||||
func min(a, b int) int {
|
||||
if a > b {
|
||||
return b
|
||||
|
|
@ -387,6 +982,11 @@ func min(a, b int) int {
|
|||
return a
|
||||
}
|
||||
|
||||
// newCertificate POST's the v1 new-cert endpoint with a CSR for a random subset
|
||||
// of domains that have finalized authz's in the context (Up to
|
||||
// `state.maxNamesPerCert` domains). The CSR's private key is the
|
||||
// `state.certKey`. The context's `certs` list is updated with the URL of the
|
||||
// certificate produced.
|
||||
func newCertificate(s *State, ctx *context) error {
|
||||
authsLen := len(ctx.finalizedAuthz)
|
||||
num := min(mrand.Intn(authsLen), s.maxNamesPerCert)
|
||||
|
|
@ -409,7 +1009,7 @@ func newCertificate(s *State, ctx *context) error {
|
|||
)
|
||||
|
||||
// build the JWS object
|
||||
requestPayload, err := s.signWithNonce(newCertPath, false, []byte(request), ctx.reg.signer)
|
||||
requestPayload, err := s.signWithNonce([]byte(request), ctx.reg.signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -440,6 +1040,13 @@ func newCertificate(s *State, ctx *context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// revokeCertificate revokes a random certificate from the context's list of
|
||||
// certificates. Presently it always uses the context's registration and the V1
|
||||
// style of revocation. The certificate is removed from the context's `certs`
|
||||
// list.
|
||||
//
|
||||
// TODO(@cpu): Write a V2 version of `revokeCertificate` that uses the context's
|
||||
// account and a key ID JWS.
|
||||
func revokeCertificate(s *State, ctx *context) error {
|
||||
// randomly select a cert to revoke
|
||||
if len(ctx.certs) == 0 {
|
||||
|
|
@ -458,7 +1065,7 @@ func revokeCertificate(s *State, ctx *context) error {
|
|||
}
|
||||
|
||||
request := fmt.Sprintf(`{"resource":"revoke-cert","certificate":"%s"}`, base64.URLEncoding.EncodeToString(body))
|
||||
requestPayload, err := s.signWithNonce(revokeCertPath, false, []byte(request), ctx.reg.signer)
|
||||
requestPayload, err := s.signWithNonce([]byte(request), ctx.reg.signer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,18 +21,17 @@ type Config struct {
|
|||
RateDelta string // requests / s^2
|
||||
Runtime string // how long to run for
|
||||
}
|
||||
ExternalState string // path to file to load/save registrations etc to/from
|
||||
DontSaveState bool // don't save changes to external state
|
||||
APIBase string // ACME API address to send requests to
|
||||
DomainBase string // base domain name to create authorizations for
|
||||
ChallTypes []string // which challenges to complete, empty means use all
|
||||
HTTPOneAddr string // address to listen for http-01 validation requests on
|
||||
TLSOneAddr string // address to listen for tls-sni-01 validation requests on
|
||||
RealIP string // value of the Real-IP header to use when bypassing CDN
|
||||
CertKeySize int // size of the key to use when creating CSRs
|
||||
RegEmail string // email to use in registrations
|
||||
Results string // path to save metrics to
|
||||
MaxRegs int // maximum number of registrations to create
|
||||
ExternalState string // path to file to load/save registrations etc to/from
|
||||
DontSaveState bool // don't save changes to external state
|
||||
APIBase string // ACME API address to send requests to
|
||||
DomainBase string // base domain name to create authorizations for
|
||||
HTTPOneAddr string // address to listen for http-01 validation requests on
|
||||
TLSOneAddr string // address to listen for tls-sni-01 validation requests on
|
||||
RealIP string // value of the Real-IP header to use when bypassing CDN
|
||||
CertKeySize int // size of the key to use when creating CSRs
|
||||
RegEmail string // email to use in registrations
|
||||
Results string // path to save metrics to
|
||||
MaxRegs int // maximum number of registrations to create
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
@ -45,7 +44,7 @@ func main() {
|
|||
|
||||
configBytes, err := ioutil.ReadFile(*configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read wfe config file %q: %s\n", &configPath, err)
|
||||
fmt.Fprintf(os.Stderr, "Failed to read wfe config file %q: %s\n", *configPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var config Config
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
mrand "math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
|
@ -36,6 +35,7 @@ type RatePeriod struct {
|
|||
Rate int64
|
||||
}
|
||||
|
||||
// registration is an ACME v1 registration resource
|
||||
type registration struct {
|
||||
key *ecdsa.PrivateKey
|
||||
signer jose.Signer
|
||||
|
|
@ -44,6 +44,31 @@ type registration struct {
|
|||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// account is an ACME v2 account resource. It does not have a `jose.Signer`
|
||||
// because we need to set the Signer options per-request with the URL being
|
||||
// POSTed and must construct it on the fly from the `key`. Accounts are
|
||||
// protected by a `sync.Mutex` that must be held for updates (see
|
||||
// `account.Update`).
|
||||
type account struct {
|
||||
key *ecdsa.PrivateKey
|
||||
id string
|
||||
finalizedOrders []string
|
||||
certs []string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// update locks an account resource's mutex and sets the `finalizedOrders` and
|
||||
// `certs` fields to the provided values.
|
||||
func (acct *account) update(finalizedOrders, certs []string) {
|
||||
acct.mu.Lock()
|
||||
defer acct.mu.Unlock()
|
||||
|
||||
acct.finalizedOrders = append(acct.finalizedOrders, finalizedOrders...)
|
||||
acct.certs = append(acct.certs, certs...)
|
||||
}
|
||||
|
||||
// update locks a registration resource's mutx and sets the `finalizedAuthz` and
|
||||
// `certs` fields to the provided values.
|
||||
func (r *registration) update(finalizedAuthz, certs []string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
|
@ -53,11 +78,102 @@ func (r *registration) update(finalizedAuthz, certs []string) {
|
|||
}
|
||||
|
||||
type context struct {
|
||||
reg *registration
|
||||
pendingAuthz []*core.Authorization
|
||||
/* ACME V1 Context */
|
||||
// The current V1 registration (may be nil for V2 load generation)
|
||||
reg *registration
|
||||
// Pending authorizations waiting for challenge validation
|
||||
pendingAuthz []*core.Authorization
|
||||
// IDs of finalized authorizations in valid status
|
||||
finalizedAuthz []string
|
||||
certs []string
|
||||
ns *nonceSource
|
||||
|
||||
/* ACME V2 Context */
|
||||
// The current V2 account (may be nil for legacy load generation)
|
||||
acct *account
|
||||
// Pending orders waiting for authorization challenge validation
|
||||
pendingOrders []*OrderJSON
|
||||
// Fulfilled orders in a valid status waiting for finalization
|
||||
fulfilledOrders []string
|
||||
// Finalized orders that have certificates
|
||||
finalizedOrders []string
|
||||
|
||||
/* Shared Context */
|
||||
// A list of URLs for issued certificates
|
||||
certs []string
|
||||
// The nonce source for JWS signature nonce headers
|
||||
ns *nonceSource
|
||||
}
|
||||
|
||||
// signEmbeddedV2Request signs the provided request data using the context's
|
||||
// account's private key. The provided URL is set as a protected header per ACME
|
||||
// v2 JWS standards. The resulting JWS contains an **embedded** JWK - this makes
|
||||
// this function primarily applicable to new account requests where no key ID is
|
||||
// known.
|
||||
func (c *context) signEmbeddedV2Request(data []byte, url string) (*jose.JSONWebSignature, error) {
|
||||
// Create a signing key for the account's private key
|
||||
signingKey := jose.SigningKey{
|
||||
Key: c.acct.key,
|
||||
Algorithm: jose.ES256,
|
||||
}
|
||||
// Create a signer, setting the URL protected header
|
||||
signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{
|
||||
NonceSource: c.ns,
|
||||
EmbedJWK: true,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"url": url,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sign the data with the signer
|
||||
signed, err := signer.Sign(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
// signKeyIDV2Request signs the provided request data using the context's
|
||||
// account's private key. The provided URL is set as a protected header per ACME
|
||||
// v2 JWS standards. The resulting JWS contains a Key ID header that is
|
||||
// populated using the context's account's ID. This is the default JWS signing
|
||||
// style for ACME v2 requests and should be used everywhere but where the key ID
|
||||
// is unknown (e.g. new-account requests where an account doesn't exist yet).
|
||||
func (c *context) signKeyIDV2Request(data []byte, url string) (*jose.JSONWebSignature, error) {
|
||||
// Create a JWK with the account's private key and key ID
|
||||
jwk := &jose.JSONWebKey{
|
||||
Key: c.acct.key,
|
||||
Algorithm: "ECDSA",
|
||||
KeyID: c.acct.id,
|
||||
}
|
||||
|
||||
// Create a signing key with the JWK
|
||||
signerKey := jose.SigningKey{
|
||||
Key: jwk,
|
||||
Algorithm: jose.ES256,
|
||||
}
|
||||
|
||||
// Ensure the signer's nonce source and URL header will be set
|
||||
opts := &jose.SignerOptions{
|
||||
NonceSource: c.ns,
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"url": url,
|
||||
},
|
||||
}
|
||||
|
||||
// Construct the signer with the configured options
|
||||
signer, err := jose.NewSigner(signerKey, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sign the data with the signer
|
||||
signed, err := signer.Sign(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
type RateDelta struct {
|
||||
|
|
@ -88,8 +204,12 @@ type State struct {
|
|||
|
||||
operations []func(*State, *context) error
|
||||
|
||||
rMu sync.RWMutex
|
||||
rMu sync.RWMutex
|
||||
|
||||
// regs holds V1 registration objects
|
||||
regs []*registration
|
||||
// accts holds V2 account objects
|
||||
accts []*account
|
||||
|
||||
challSrv *challSrv
|
||||
callLatency latencyWriter
|
||||
|
|
@ -109,8 +229,22 @@ type rawRegistration struct {
|
|||
RawKey []byte `json:"rawKey"`
|
||||
}
|
||||
|
||||
type rawAccount struct {
|
||||
FinalizedOrders []string `json:"finalizedOrders"`
|
||||
Certs []string `json:"certs"`
|
||||
ID string `json:"id"`
|
||||
RawKey []byte `json:"rawKey"`
|
||||
}
|
||||
|
||||
type snapshot struct {
|
||||
Registrations []rawRegistration
|
||||
Accounts []rawAccount
|
||||
}
|
||||
|
||||
func (s *State) numAccts() int {
|
||||
s.rMu.RLock()
|
||||
defer s.rMu.RUnlock()
|
||||
return len(s.accts)
|
||||
}
|
||||
|
||||
func (s *State) numRegs() int {
|
||||
|
|
@ -119,9 +253,9 @@ func (s *State) numRegs() int {
|
|||
return len(s.regs)
|
||||
}
|
||||
|
||||
// Snapshot will save out generated registrations and certs (ignoring authorizations)
|
||||
// Snapshot will save out generated registrations and accounts
|
||||
func (s *State) Snapshot(filename string) error {
|
||||
fmt.Printf("[+] Saving registrations to %s\n", filename)
|
||||
fmt.Printf("[+] Saving registrations/accounts to %s\n", filename)
|
||||
snap := snapshot{}
|
||||
// assume rMu lock operations aren't happening right now
|
||||
for _, reg := range s.regs {
|
||||
|
|
@ -135,6 +269,18 @@ func (s *State) Snapshot(filename string) error {
|
|||
RawKey: k,
|
||||
})
|
||||
}
|
||||
for _, acct := range s.accts {
|
||||
k, err := x509.MarshalECPrivateKey(acct.key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snap.Accounts = append(snap.Accounts, rawAccount{
|
||||
Certs: acct.certs,
|
||||
FinalizedOrders: acct.finalizedOrders,
|
||||
ID: acct.id,
|
||||
RawKey: k,
|
||||
})
|
||||
}
|
||||
cont, err := json.Marshal(snap)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -142,9 +288,9 @@ func (s *State) Snapshot(filename string) error {
|
|||
return ioutil.WriteFile(filename, cont, os.ModePerm)
|
||||
}
|
||||
|
||||
// Restore previously generated registrations and certs
|
||||
// Restore previously generated registrations and accounts
|
||||
func (s *State) Restore(filename string) error {
|
||||
fmt.Printf("[+] Loading registrations from %s\n", filename)
|
||||
fmt.Printf("[+] Loading registrations/accounts from %s\n", filename)
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -175,6 +321,21 @@ func (s *State) Restore(filename string) error {
|
|||
certs: r.Certs,
|
||||
})
|
||||
}
|
||||
for _, a := range snap.Accounts {
|
||||
key, err := x509.ParseECPrivateKey(a.RawKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.accts = append(s.accts, &account{
|
||||
key: key,
|
||||
id: a.ID,
|
||||
finalizedOrders: a.FinalizedOrders,
|
||||
certs: a.Certs,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -381,9 +542,11 @@ func (s *State) get(path string) (*http.Response, error) {
|
|||
}
|
||||
|
||||
// Nonce utils, these methods are used to generate/store/retrieve the nonces
|
||||
// required for JWS
|
||||
// required for JWS in V1 ACME requests
|
||||
|
||||
func (s *State) signWithNonce(endpoint string, alwaysNew bool, payload []byte, signer jose.Signer) ([]byte, error) {
|
||||
// signWithNonce signs the provided message with the provided signer, returning
|
||||
// the raw JWS bytes or an error. signWithNonce is not compatible with ACME v2
|
||||
func (s *State) signWithNonce(payload []byte, signer jose.Signer) ([]byte, error) {
|
||||
jws, err := signer.Sign(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -438,6 +601,15 @@ func (ns *nonceSource) addNonce(nonce string) {
|
|||
ns.noncePool = append(ns.noncePool, nonce)
|
||||
}
|
||||
|
||||
// addAccount adds the provided account to the state's list of accts
|
||||
func (s *State) addAccount(acct *account) {
|
||||
s.rMu.Lock()
|
||||
defer s.rMu.Unlock()
|
||||
|
||||
s.accts = append(s.accts, acct)
|
||||
}
|
||||
|
||||
// addRegistration adds the provided registration to the state's list of regs
|
||||
func (s *State) addRegistration(reg *registration) {
|
||||
s.rMu.Lock()
|
||||
defer s.rMu.Unlock()
|
||||
|
|
@ -445,17 +617,6 @@ func (s *State) addRegistration(reg *registration) {
|
|||
s.regs = append(s.regs, reg)
|
||||
}
|
||||
|
||||
func getRegistration(s *State, ctx *context) error {
|
||||
s.rMu.RLock()
|
||||
defer s.rMu.RUnlock()
|
||||
|
||||
if len(s.regs) == 0 {
|
||||
return errors.New("no registrations to return")
|
||||
}
|
||||
ctx.reg = s.regs[mrand.Intn(len(s.regs))]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *State) sendCall() {
|
||||
defer s.wg.Done()
|
||||
ctx := &context{}
|
||||
|
|
@ -468,7 +629,13 @@ func (s *State) sendCall() {
|
|||
break
|
||||
}
|
||||
}
|
||||
// If the context's V1 registration
|
||||
if ctx.reg != nil {
|
||||
ctx.reg.update(ctx.finalizedAuthz, ctx.certs)
|
||||
}
|
||||
// If the context's V2 account isn't nil, update it based on the context's
|
||||
// finalizedOrders and certs.
|
||||
if ctx.acct != nil {
|
||||
ctx.acct.update(ctx.finalizedOrders, ctx.certs)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"plan": {
|
||||
"actions": [
|
||||
"newAccount",
|
||||
"newOrder",
|
||||
"fulfillOrder",
|
||||
"finalizeOrder"
|
||||
],
|
||||
"rate": 5,
|
||||
"runtime": "5m",
|
||||
"rateDelta": "5/1m"
|
||||
},
|
||||
"apiBase": "http://localhost:4001",
|
||||
"domainBase": "com",
|
||||
"httpOneAddr": "localhost:5002",
|
||||
"regKeySize": 2048,
|
||||
"certKeySize": 2048,
|
||||
"regEmail": "loadtesting@letsencrypt.org",
|
||||
"maxRegs": 20,
|
||||
"dontSaveState": true,
|
||||
"results": "v2-example-latency.json"
|
||||
}
|
||||
Loading…
Reference in New Issue