diff --git a/README.md b/README.md index c2ca0ce6..7b7fb393 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,21 @@ The endpoint returns the information as a JSON object: "Status": "Revoked" } +#### Updates to configuration at runtime + +A couple of Pebble's configurables can be modified after Pebble has started by sending +a `PATCH` request to `https://localhost:15000/runtimeConfig` whose request body is a +json object with one or more of the following properties: + +| Property | Type | Analogous Env Variable | +|-------------------|----------------|------------------------- +| AuthzReusePercent | integer, 0-100 | PEBBLE_AUTHZREUSE | +| NonceErrPercent | integer, 0-100 | PEBBLE_WFE_NONCEREJECT | + +The endpoint returns HTTP 202 Accepted if the request was successful. + +Changing these percentages can simplify creating test suites where certain tests exercise +authorization reuse or nonce error handling. ### OCSP Responder URL diff --git a/wfe/wfe.go b/wfe/wfe.go index 52790c20..0d8b2716 100644 --- a/wfe/wfe.go +++ b/wfe/wfe.go @@ -11,6 +11,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "io/ioutil" "log" "math/big" @@ -23,6 +24,7 @@ import ( "sort" "strconv" "strings" + "sync/atomic" "time" "unicode" @@ -61,6 +63,7 @@ const ( intermediateCertPath = "/intermediates/" intermediateKeyPath = "/intermediate-keys/" certStatusBySerial = "/cert-status-by-serial/" + runtimeConfig = "/runtimeConfig" // How long do pending authorizations last before expiring? pendingAuthzExpire = time.Hour @@ -104,7 +107,7 @@ const ( // The default value when PEBBLE_WFE_AUTHZREUSE is not set, how often to try // and reuse valid authorizations. - defaultAuthzReuse = 50 + defaultAuthzReuse = int32(50) // ordersPerPageEnvVar defines the environment variable name used to provide // the number of orders to show per page. To have the WFE show 15 orders per @@ -148,8 +151,8 @@ type WebFrontEndImpl struct { log *log.Logger db *db.MemoryStore nonce *nonceMap - nonceErrPercent int - authzReusePercent int + nonceErrPercent int32 + authzReusePercent int32 ordersPerPage int va *va.VAImpl ca *ca.CAImpl @@ -168,12 +171,12 @@ func New( // Read the % of good nonces that should be rejected as bad nonces from the // environment nonceErrPercentVal := os.Getenv(badNonceEnvVar) - var nonceErrPercent int + var nonceErrPercent int32 // Parse the env var value as a base 10 int - if there isn't an error, use it // as the wfe nonceErrPercent if val, err := strconv.ParseInt(nonceErrPercentVal, 10, 0); err == nil { - nonceErrPercent = int(val) + nonceErrPercent = int32(val) } else { // Otherwise just use the default nonceErrPercent = defaultNonceReject @@ -191,7 +194,7 @@ func New( authzReusePercent := defaultAuthzReuse if val, err := strconv.ParseInt(os.Getenv(authzReuseEnvVar), 10, 0); err == nil && val >= 0 && val <= 100 { - authzReusePercent = int(val) + authzReusePercent = int32(val) } log.Printf("Configured to attempt authz reuse for each identifier %d%% of the time", authzReusePercent) @@ -484,6 +487,42 @@ func (wfe *WebFrontEndImpl) handleCertStatusBySerial( } } +func (wfe *WebFrontEndImpl) updateRuntimeConfig( + ctx context.Context, + response http.ResponseWriter, + request *http.Request) { + + type requestSchema struct { + AuthzReusePercent *uint8 `json:",omitempty"` + NonceErrPercent *uint8 `json:",omitempty"` + } + + if request.Body == nil { + response.WriteHeader(http.StatusBadRequest) + return + } + defer request.Body.Close() + + var maxBodyBytes int64 = 2048 + d := json.NewDecoder(io.LimitReader(request.Body, maxBodyBytes)) + requestObj := &requestSchema{} + err := d.Decode(requestObj) + if err != nil { + response.WriteHeader(http.StatusBadRequest) + _, _ = response.Write([]byte(err.Error())) + return + } + + if requestObj.AuthzReusePercent != nil { + atomic.StoreInt32(&wfe.authzReusePercent, int32(*requestObj.AuthzReusePercent)) + } + if requestObj.NonceErrPercent != nil { + atomic.StoreInt32(&wfe.nonceErrPercent, int32(*requestObj.NonceErrPercent)) + } + + response.WriteHeader(http.StatusAccepted) +} + func (wfe *WebFrontEndImpl) Handler() http.Handler { m := http.NewServeMux() // GET & POST handlers @@ -517,6 +556,7 @@ func (wfe *WebFrontEndImpl) ManagementHandler() http.Handler { wfe.HandleManagementFunc(m, intermediateCertPath, wfe.handleCert(wfe.ca.GetIntermediateCert, intermediateCertPath)) wfe.HandleManagementFunc(m, intermediateKeyPath, wfe.handleKey(wfe.ca.GetIntermediateKey, intermediateKeyPath)) wfe.HandleManagementFunc(m, certStatusBySerial, wfe.handleCertStatusBySerial) + wfe.HandleManagementFunc(m, runtimeConfig, wfe.updateRuntimeConfig) return m } @@ -889,10 +929,10 @@ func (wfe *WebFrontEndImpl) verifyJWS( } // Roll a random number between 0 and 100. - nonceRoll := rand.Intn(100) + nonceRoll := rand.Int31n(100) // If the nonce is not valid OR if the nonceRoll was less than the // nonceErrPercent, fail with an error - if !wfe.nonce.validNonce(nonce) || nonceRoll < wfe.nonceErrPercent { + if !wfe.nonce.validNonce(nonce) || nonceRoll < atomic.LoadInt32(&wfe.nonceErrPercent) { return nil, acme.BadNonceProblem(fmt.Sprintf( "JWS has an invalid anti-replay nonce: %s", nonce)) } @@ -1486,7 +1526,7 @@ func (wfe *WebFrontEndImpl) makeAuthorizations(order *core.Order, request *http. // If there is an existing valid authz for this identifier, we can reuse it authz := wfe.db.FindValidAuthorization(order.AccountID, ident) // Otherwise create a new pending authz (and randomly not) - if authz == nil || rand.Intn(100) > wfe.authzReusePercent { + if authz == nil || rand.Int31n(100) > atomic.LoadInt32(&wfe.authzReusePercent) { authz = &core.Authorization{ ID: newToken(), ExpiresDate: expires,