load-generator: remove acme v1 support. (#4132)

We don't intend to load test the legacy WFE implementation in the future
and if we need to we can always revive this code from git. Removing it
will make refactoring the ACME v2 code to be closer to RFC 8555 easier.
This commit is contained in:
Daniel McCarney 2019-03-25 12:22:18 -04:00 committed by GitHub
parent f61242e751
commit de30d22303
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 17 additions and 551 deletions

View File

@ -889,17 +889,12 @@ def run_chisel(test_case_filter):
value()
def run_loadtest():
"""Run the load generator for v1 and v2."""
"""Run the ACME v2 load generator."""
latency_data_file = "%s/integration-test-latency.json" % tempdir
run("./bin/load-generator \
-config test/load-generator/config/integration-test-config.json\
-results %s" % latency_data_file)
latency_data_file = "%s/v2-integration-test-latency.json" % tempdir
run("./bin/load-generator \
-config test/load-generator/config/v2-integration-test-config.json\
-results %s" % latency_data_file)
def check_balance():
"""Verify that gRPC load balancing across backends is working correctly.

View File

@ -2,4 +2,4 @@
![](https://i.imgur.com/58ZQjyH.gif)
`load-generator` is a load generator for the Boulder WFE which emulates user workflows.
`load-generator` is a load generator for RFC 8555 which emulates user workflows.

View File

@ -27,18 +27,8 @@ import (
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.
// operate on a state/context.
stringToOperation = map[string]func(*State, *context) error{
/* ACME v1 Operations */
"newRegistration": newRegistration,
"getRegistration": getRegistration,
"newAuthorization": newAuthorization,
"solveHTTPOne": solveHTTPOne,
"newCertificate": newCertificate,
"revokeCertificate": revokeCertificate,
/* ACME v2 Operations */
"newAccount": newAccount,
"getAccount": getAccount,
"newOrder": newOrder,
@ -93,23 +83,6 @@ func getAccount(s *State, ctx *context) error {
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,
@ -193,111 +166,6 @@ func newAccount(s *State, ctx *context) error {
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 {
return getRegistration(s, ctx)
}
signKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return err
}
ns := &nonceSource{s: s}
ctx.ns = ns
signer, err := jose.NewSigner(
jose.SigningKey{
Key: signKey,
Algorithm: jose.ES256,
},
&jose.SignerOptions{
NonceSource: ns,
EmbedJWK: true,
})
if err != nil {
return err
}
// create the registration object
var regStr []byte
if s.email != "" {
regStr = []byte(fmt.Sprintf(`{"resource":"new-reg","contact":["mailto:%s"]}`, s.email))
} else {
regStr = []byte(`{"resource":"new-reg"}`)
}
// build the JWS object
requestPayload, err := s.signWithNonce(regStr, signer)
if err != nil {
return fmt.Errorf("/acme/new-reg, sign failed: %s", err)
}
nStarted := time.Now()
resp, err := s.post(fmt.Sprintf("%s%s", s.apiBase, newRegPath), requestPayload, ctx.ns)
nFinished := time.Now()
nState := "good"
defer func() { s.callLatency.Add("POST /acme/new-reg", nStarted, nFinished, nState) }()
if err != nil {
nState = "error"
return fmt.Errorf("/acme/new-reg, post failed: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
body, err := ioutil.ReadAll(resp.Body)
nState = "error"
if err != nil {
return fmt.Errorf("/acme/new-reg, bad response: %s", body)
}
return fmt.Errorf("/acme/new-reg, bad response status %d: %s", resp.StatusCode, body)
}
// get terms
links := resp.Header[http.CanonicalHeaderKey("link")]
terms := ""
for _, l := range links {
if strings.HasSuffix(l, ">;rel=\"terms-of-service\"") {
terms = l[1 : len(l)-len(">;rel=\"terms-of-service\"")]
break
}
}
// agree to terms
regStr = []byte(fmt.Sprintf(`{"resource":"reg","agreement":"%s"}`, terms))
// build the JWS object
requestPayload, err = s.signWithNonce(regStr, signer)
if err != nil {
return fmt.Errorf("/acme/reg, sign failed: %s", err)
}
tStarted := time.Now()
resp, err = s.post(resp.Header.Get("Location"), requestPayload, ctx.ns)
tFinished := time.Now()
tState := "good"
defer func() { s.callLatency.Add("POST /acme/reg/{ID}", tStarted, tFinished, tState) }()
if err != nil {
tState = "error"
return fmt.Errorf("/acme/reg, post failed: %s", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
tState = "error"
return err
}
tState = "error"
return fmt.Errorf("/acme/reg, bad response status %d: %s", resp.StatusCode, body)
}
ctx.reg = &registration{key: signKey, signer: signer}
s.addRegistration(ctx.reg)
return nil
}
// randDomain generates a random(-ish) domain name as a subdomain of the
// provided base domain.
func randDomain(base string) string {
@ -387,61 +255,6 @@ func newOrder(s *State, ctx *context) error {
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
requestPayload, err := s.signWithNonce([]byte(initAuth), ctx.reg.signer)
if err != nil {
return err
}
started := time.Now()
resp, err := s.post(fmt.Sprintf("%s/acme/new-authz", s.apiBase), requestPayload, ctx.ns)
finished := time.Now()
state := "good"
defer func() { s.callLatency.Add("POST /acme/new-authz", started, finished, state) }()
if err != nil {
state = "error"
return err
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
state = "error"
return err
}
state = "error"
return fmt.Errorf("bad response, status %d: %s", resp.StatusCode, body)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
state = "error"
return err
}
var authz core.Authorization
err = json.Unmarshal(body, &authz)
if err != nil {
state = "error"
return err
}
// populate authz ID from location header because we strip it
paths := strings.Split(resp.Header.Get("Location"), "/")
authz.ID = paths[len(paths)-1]
ctx.pendingAuthz = append(ctx.pendingAuthz, &authz)
return nil
}
// popPendingOrder *removes* a random pendingOrder from the context, returning
// it.
func popPendingOrder(ctx *context) *OrderJSON {
@ -628,103 +441,6 @@ func fulfillOrder(s *State, ctx *context) error {
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]
ctx.pendingAuthz = append(ctx.pendingAuthz[:authzIndex], ctx.pendingAuthz[authzIndex+1:]...)
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")
}
authz := popPending(ctx)
var chall *core.Challenge
for _, c := range authz.Challenges {
if c.Type == "http-01" {
chall = &c
break
}
}
if chall == nil {
return errors.New("no http-01 challenges to complete")
}
jwk := &jose.JSONWebKey{Key: &ctx.reg.key.PublicKey}
thumbprint, err := jwk.Thumbprint(crypto.SHA256)
if err != nil {
return err
}
authStr := fmt.Sprintf("%s.%s", chall.Token, base64.RawURLEncoding.EncodeToString(thumbprint))
s.challSrv.AddHTTPOneChallenge(chall.Token, authStr)
defer s.challSrv.DeleteHTTPOneChallenge(chall.Token)
update := fmt.Sprintf(`{"resource":"challenge","keyAuthorization":"%s"}`, authStr)
requestPayload, err := s.signWithNonce([]byte(update), ctx.reg.signer)
if err != nil {
return err
}
cStarted := time.Now()
resp, err := s.post(chall.URI, requestPayload, ctx.ns)
cFinished := time.Now()
cState := "good"
defer func() { s.callLatency.Add("POST /acme/challenge/{ID}", cStarted, cFinished, cState) }()
if err != nil {
cState = "error"
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
cState = "error"
return fmt.Errorf("Unexpected error code")
}
// Sit and spin until status valid or invalid, replicating Certbot behavior
ident := ""
for i := 0; i < 3; i++ {
aStarted := time.Now()
resp, err = s.get(fmt.Sprintf("%s/acme/authz/%s", s.apiBase, authz.ID))
aFinished := time.Now()
aState := "good"
defer func() { s.callLatency.Add("GET /acme/authz/{ID}", aStarted, aFinished, aState) }()
if err != nil {
aState = "error"
return fmt.Errorf("/acme/authz bad response: %s", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
aState = "error"
return err
}
var newAuthz core.Authorization
err = json.Unmarshal(body, &newAuthz)
if err != nil {
aState = "error"
return fmt.Errorf("/acme/authz bad response: %s", body)
}
if newAuthz.Status == "valid" {
ident = newAuthz.Identifier.Value
break
}
if newAuthz.Status == "invalid" {
return fmt.Errorf("HTTP-01 challenge invalid: %s", string(body))
}
time.Sleep(3 * time.Second)
}
if ident == "" {
return errors.New("HTTP-01 challenge validation timed out")
}
ctx.finalizedAuthz = append(ctx.finalizedAuthz, ident)
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) {
@ -898,114 +614,3 @@ 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)
dnsNames := []string{}
for i := 0; i <= num; i++ {
dnsNames = append(dnsNames, ctx.finalizedAuthz[mrand.Intn(authsLen)])
}
csr, err := x509.CreateCertificateRequest(
rand.Reader,
&x509.CertificateRequest{DNSNames: dnsNames},
s.certKey,
)
if err != nil {
return err
}
request := fmt.Sprintf(
`{"resource":"new-cert","csr":"%s"}`,
base64.URLEncoding.EncodeToString(csr),
)
// build the JWS object
requestPayload, err := s.signWithNonce([]byte(request), ctx.reg.signer)
if err != nil {
return err
}
started := time.Now()
resp, err := s.post(fmt.Sprintf("%s%s", s.apiBase, newCertPath), requestPayload, ctx.ns)
finished := time.Now()
state := "good"
defer func() { s.callLatency.Add("POST /acme/new-cert", started, finished, state) }()
if err != nil {
state = "error"
return err
}
defer resp.Body.Close()
if resp.StatusCode != 201 {
state = "error"
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("bad response, status %d: %s", resp.StatusCode, body)
}
if certLoc := resp.Header.Get("Location"); certLoc != "" {
ctx.certs = append(ctx.certs, certLoc)
}
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 {
return errors.New("no certificates to revoke")
}
index := mrand.Intn(len(ctx.certs))
resp, err := s.get(ctx.certs[index])
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
request := fmt.Sprintf(`{"resource":"revoke-cert","certificate":"%s"}`, base64.URLEncoding.EncodeToString(body))
requestPayload, err := s.signWithNonce([]byte(request), ctx.reg.signer)
if err != nil {
return err
}
started := time.Now()
resp, err = s.post(fmt.Sprintf("%s%s", s.apiBase, revokeCertPath), requestPayload, ctx.ns)
finished := time.Now()
state := "good"
defer func() { s.callLatency.Add("POST /acme/revoke-cert", started, finished, state) }()
if err != nil {
state = "error"
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
state = "error"
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("bad response, status %d: %s", resp.StatusCode, body)
}
ctx.certs = append(ctx.certs[:index], ctx.certs[index+1:]...)
return nil
}

View File

@ -1,21 +0,0 @@
{
"plan": {
"actions": [
"newRegistration",
"newAuthorization",
"solveHTTPOne",
"newCertificate"
],
"rate": 5,
"runtime": "5m",
"rateDelta": "5/1m"
},
"apiBase": "http://localhost:4000",
"domainBase": "com",
"httpOneAddr": "localhost:5002",
"tlsOneAddr": "localhost:5001",
"regKeySize": 2048,
"certKeySize": 2048,
"regEmail": "loadtesting@letsencrypt.org",
"results": "example-latency.json"
}

View File

@ -1,20 +1,22 @@
{
"plan": {
"actions": [
"newRegistration",
"newAuthorization",
"solveHTTPOne",
"newCertificate"
"newAccount",
"newOrder",
"fulfillOrder",
"finalizeOrder"
],
"rate": 1,
"runtime": "10s",
"rateDelta": "5/1m"
},
"apiBase": "http://boulder:4000",
"apiBase": "http://boulder:4001",
"domainBase": "com",
"httpOneAddr": "localhost:5002",
"tlsOneAddr": "localhost:5001",
"regKeySize": 2048,
"certKeySize": 2048,
"regEmail": "loadtesting@letsencrypt.org"
"regEmail": "loadtesting@letsencrypt.org",
"maxRegs": 20,
"maxNamesPerCert": 20,
"dontSaveState": true
}

View File

@ -1,22 +0,0 @@
{
"plan": {
"actions": [
"newAccount",
"newOrder",
"fulfillOrder",
"finalizeOrder"
],
"rate": 1,
"runtime": "10s",
"rateDelta": "5/1m"
},
"apiBase": "http://boulder:4001",
"domainBase": "com",
"httpOneAddr": "localhost:5002",
"regKeySize": 2048,
"certKeySize": 2048,
"regEmail": "loadtesting@letsencrypt.org",
"maxRegs": 20,
"maxNamesPerCert": 20,
"dontSaveState": true
}

View File

@ -27,7 +27,6 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/challtestsrv"
)
@ -37,15 +36,6 @@ type RatePeriod struct {
Rate int64
}
// registration is an ACME v1 registration resource
type registration struct {
key *ecdsa.PrivateKey
signer jose.Signer
finalizedAuthz []string
certs []string
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
@ -69,26 +59,7 @@ func (acct *account) update(finalizedOrders, certs []string) {
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()
r.finalizedAuthz = append(r.finalizedAuthz, finalizedAuthz...)
r.certs = append(r.certs, certs...)
}
type context struct {
/* 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
/* ACME V2 Context */
// The current V2 account (may be nil for legacy load generation)
acct *account
// Pending orders waiting for authorization challenge validation
@ -98,7 +69,6 @@ type context struct {
// 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
@ -208,8 +178,6 @@ type State struct {
rMu sync.RWMutex
// regs holds V1 registration objects
regs []*registration
// accts holds V2 account objects
accts []*account
@ -225,12 +193,6 @@ type State struct {
wg *sync.WaitGroup
}
type rawRegistration struct {
Certs []string `json:"certs"`
FinalizedAuthz []string `json:"finalizedAuthz"`
RawKey []byte `json:"rawKey"`
}
type rawAccount struct {
FinalizedOrders []string `json:"finalizedOrders"`
Certs []string `json:"certs"`
@ -239,8 +201,7 @@ type rawAccount struct {
}
type snapshot struct {
Registrations []rawRegistration
Accounts []rawAccount
Accounts []rawAccount
}
func (s *State) numAccts() int {
@ -249,28 +210,10 @@ func (s *State) numAccts() int {
return len(s.accts)
}
func (s *State) numRegs() int {
s.rMu.RLock()
defer s.rMu.RUnlock()
return len(s.regs)
}
// Snapshot will save out generated registrations and accounts
// Snapshot will save out generated accounts
func (s *State) Snapshot(filename string) error {
fmt.Printf("[+] Saving registrations/accounts to %s\n", filename)
fmt.Printf("[+] Saving accounts to %s\n", filename)
snap := snapshot{}
// assume rMu lock operations aren't happening right now
for _, reg := range s.regs {
k, err := x509.MarshalECPrivateKey(reg.key)
if err != nil {
return err
}
snap.Registrations = append(snap.Registrations, rawRegistration{
Certs: reg.certs,
FinalizedAuthz: reg.finalizedAuthz,
RawKey: k,
})
}
for _, acct := range s.accts {
k, err := x509.MarshalECPrivateKey(acct.key)
if err != nil {
@ -290,9 +233,9 @@ func (s *State) Snapshot(filename string) error {
return ioutil.WriteFile(filename, cont, os.ModePerm)
}
// Restore previously generated registrations and accounts
// Restore previously generated accounts
func (s *State) Restore(filename string) error {
fmt.Printf("[+] Loading registrations/accounts from %s\n", filename)
fmt.Printf("[+] Loading accounts from %s\n", filename)
content, err := ioutil.ReadFile(filename)
if err != nil {
return err
@ -302,27 +245,6 @@ func (s *State) Restore(filename string) error {
if err != nil {
return err
}
for _, r := range snap.Registrations {
key, err := x509.ParseECPrivateKey(r.RawKey)
if err != nil {
continue
}
signer, err := jose.NewSigner(jose.SigningKey{
Key: key,
Algorithm: jose.RS256,
}, &jose.SignerOptions{
NonceSource: &nonceSource{s: s},
EmbedJWK: true,
})
if err != nil {
continue
}
s.regs = append(s.regs, &registration{
key: key,
signer: signer,
certs: r.Certs,
})
}
for _, a := range snap.Accounts {
key, err := x509.ParseECPrivateKey(a.RawKey)
if err != nil {
@ -571,9 +493,6 @@ func (s *State) get(path string) (*http.Response, error) {
return resp, nil
}
// Nonce utils, these methods are used to generate/store/retrieve the nonces
// required for JWS in V1 ACME requests
// 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) {
@ -639,14 +558,6 @@ func (s *State) addAccount(acct *account) {
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()
s.regs = append(s.regs, reg)
}
func (s *State) sendCall() {
defer s.wg.Done()
ctx := &context{}
@ -659,10 +570,6 @@ 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 {