Skip to content

Commit 318c1f6

Browse files
committed
This implements more sleeps when clients are expected to poll
Specifically this adds two additional delay points: - Between completing validation but before updating the order state - After signing the certificate and before marking it as ready It also completely refactors how these sleeps are controlled, by replacing PEBBLE_VA_* with a single PEBBLE_SLEEPTIME environment variable.
1 parent 99dd605 commit 318c1f6

6 files changed

Lines changed: 257 additions & 67 deletions

File tree

README.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,17 @@ services:
129129
- 14000:14000 # ACME port
130130
- 15000:15000 # Management port
131131
environment:
132-
- PEBBLE_VA_NOSLEEP=1
132+
- PEBBLE_SLEEPTIME=0
133133
volumes:
134134
- ./my-pebble-config.json:/test/my-pebble-config.json
135135
```
136136
137137
With a Docker command:
138138
139139
```bash
140-
docker run -e "PEBBLE_VA_NOSLEEP=1" letsencrypt/pebble
140+
docker run -e "PEBBLE_SLEEPTIME=0" letsencrypt/pebble
141141
# or
142-
docker run -e "PEBBLE_VA_NOSLEEP=1" --mount src=$(pwd)/my-pebble-config.json,target=/test/my-pebble-config.json,type=bind letsencrypt/pebble pebble -config /test/my-pebble-config.json
142+
docker run -e "PEBBLE_SLEEPTIME=0" --mount src=$(pwd)/my-pebble-config.json,target=/test/my-pebble-config.json,type=bind letsencrypt/pebble pebble -config /test/my-pebble-config.json
143143
```
144144

145145
**Note**: The Pebble dockerfile uses [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build/) and requires Docker CE 17.05.0-ce or newer.
@@ -197,19 +197,28 @@ for more information.
197197

198198
### Testing at full speed
199199

200-
By default Pebble will sleep a random number of seconds (from 0 to 15) between
201-
individual challenge validation attempts. This ensures clients don't make
202-
assumptions about when the challenge is solved from the CA side by observing
203-
a single request for a challenge response. Instead clients must poll the
204-
challenge to observe the state since the CA may send many validation requests.
200+
By default, Pebble will sleep a random number of seconds (from 0 to 5) during
201+
specific stages of the workflow. This ensures clients don't make assumptions
202+
about the state of the system based on observing external behavior. Instead,
203+
clients must poll at the appropriate time.
205204

206-
To test issuance "at full speed" with no artificial sleeps set the environment
207-
variable `PEBBLE_VA_NOSLEEP` to `1`. E.g.
205+
These sleeps occur at the following 3 points:
208206

209-
`PEBBLE_VA_NOSLEEP=1 pebble -config ./test/config/pebble-config.json`
207+
1. Before attempting to perform a validation
208+
2. Between validating and updating the order state
209+
3. After creating the certificate and marking it as ready
210210

211-
The maximal number of seconds to sleep can be configured by defining
212-
`PEBBLE_VA_SLEEPTIME`. It must be set to a positive integer.
211+
The maximum sleep time can be configured through the `PEBBLE_SLEEPTIME` environment
212+
variable. To test "at full speed" with no artificial sleeps, set the environment
213+
variable to `0`.
214+
215+
For example, to test polling code by forcing a large delay:
216+
217+
`PEBBLE_SLEEPTIME=60 pebble -config ./test/config/pebble-config.json`
218+
219+
and to disable sleeping entirely:
220+
221+
`PEBBLE_SLEEPTIME=0 pebble -config ./test/config/pebble-config.json`
213222

214223
### Skipping Validation
215224

ca/ca.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package ca
22

33
import (
44
"crypto"
5-
"crypto/rand"
5+
crand "crypto/rand"
66
"crypto/rsa"
77
"crypto/sha1"
88
"crypto/x509"
@@ -13,6 +13,7 @@ import (
1313
"log"
1414
"math"
1515
"math/big"
16+
mrand "math/rand"
1617
"net"
1718
"strings"
1819
"time"
@@ -31,6 +32,7 @@ type CAImpl struct {
3132
log *log.Logger
3233
db *db.MemoryStore
3334
ocspResponderURL string
35+
sleepTime int
3436

3537
chains []*chain
3638
}
@@ -57,7 +59,7 @@ type issuer struct {
5759
}
5860

5961
func makeSerial() *big.Int {
60-
serial, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
62+
serial, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
6163
if err != nil {
6264
panic(fmt.Sprintf("unable to create random serial number: %s", err.Error()))
6365
}
@@ -92,7 +94,7 @@ func makeSubjectKeyID(key crypto.PublicKey) ([]byte, error) {
9294

9395
// makeKey creates a new 2048 bit RSA private key and a Subject Key Identifier
9496
func makeKey() (*rsa.PrivateKey, []byte, error) {
95-
key, err := rsa.GenerateKey(rand.Reader, 2048)
97+
key, err := rsa.GenerateKey(crand.Reader, 2048)
9698
if err != nil {
9799
return nil, nil, err
98100
}
@@ -133,7 +135,7 @@ func (ca *CAImpl) makeRootCert(
133135
parent = template
134136
}
135137

136-
der, err := x509.CreateCertificate(rand.Reader, template, parent, subjectKey.Public(), signerKey)
138+
der, err := x509.CreateCertificate(crand.Reader, template, parent, subjectKey.Public(), signerKey)
137139
if err != nil {
138140
return nil, err
139141
}
@@ -307,7 +309,7 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
307309
template.OCSPServer = []string{ca.ocspResponderURL}
308310
}
309311

310-
der, err := x509.CreateCertificate(rand.Reader, template, issuer.cert.Cert, key, issuer.key)
312+
der, err := x509.CreateCertificate(crand.Reader, template, issuer.cert.Cert, key, issuer.key)
311313
if err != nil {
312314
return nil, err
313315
}
@@ -340,10 +342,11 @@ func (ca *CAImpl) newCertificate(domains []string, ips []net.IP, key crypto.Publ
340342
return newCert, nil
341343
}
342344

343-
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternateRoots int, chainLength int) *CAImpl {
345+
func New(log *log.Logger, db *db.MemoryStore, ocspResponderURL string, alternateRoots int, chainLength int, sleepTime int) *CAImpl {
344346
ca := &CAImpl{
345-
log: log,
346-
db: db,
347+
log: log,
348+
db: db,
349+
sleepTime: sleepTime,
347350
}
348351

349352
if ocspResponderURL != "" {
@@ -398,6 +401,12 @@ func (ca *CAImpl) CompleteOrder(order *core.Order) {
398401
}
399402
ca.log.Printf("Issued certificate serial %s for order %s\n", cert.ID, order.ID)
400403

404+
if ca.sleepTime > 0 {
405+
sleepLen := time.Duration(mrand.Intn(ca.sleepTime))
406+
ca.log.Printf("Sleeping for %s seconds before marking order %s complete", time.Second*sleepLen, order.ID)
407+
time.Sleep(time.Second * sleepLen)
408+
}
409+
401410
// Lock and update the order to store the issued certificate
402411
order.Lock()
403412
order.CertificateObject = cert

cmd/pebble/main.go

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"os"
88
"strconv"
9+
"strings"
910

1011
"github.com/letsencrypt/pebble/ca"
1112
"github.com/letsencrypt/pebble/cmd"
@@ -31,6 +32,24 @@ type config struct {
3132
}
3233
}
3334

35+
const (
36+
// vaNoSleepEnvVar and vaSleepTimeEnvVar are deprecated environment variable
37+
// names used to control sleeping in the VA. Now sleepTimeEnvVar is the proper
38+
// method to manage it. Exists for deprecation warning purposes.
39+
vaNoSleepEnvVar = "PEBBLE_VA_NOSLEEP"
40+
vaSleepTimeEnvVar = "PEBBLE_VA_SLEEPTIME"
41+
42+
// sleepTimeEnvVar is the variable used to control if and how much the process
43+
// sleeps during specific stages of the workflow. 0 disables sleeping, negative
44+
// and non-integers will be ignored, and positive integers are the maximum
45+
// number of seconds to sleep for.
46+
sleepTimeEnvVar = "PEBBLE_SLEEPTIME"
47+
48+
// defaultSleepTime defines the default sleep time (in seconds) at various
49+
// stages in the workflow.
50+
defaultSleepTime = 5
51+
)
52+
3453
func main() {
3554
configFile := flag.String(
3655
"config",
@@ -69,9 +88,16 @@ func main() {
6988
chainLength = int(val)
7089
}
7190

91+
sleepTime := getEnvSleepTime(logger, os.Getenv(sleepTimeEnvVar), os.Getenv(vaNoSleepEnvVar), os.Getenv(vaSleepTimeEnvVar))
92+
if sleepTime == 0 {
93+
logger.Print("Disabled random sleeps")
94+
} else {
95+
logger.Printf("Setting maximum random sleep interval to %d seconds", sleepTime)
96+
}
97+
7298
db := db.NewMemoryStore()
73-
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength)
74-
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress)
99+
ca := ca.New(logger, db, c.Pebble.OCSPResponderURL, alternateRoots, chainLength, sleepTime)
100+
va := va.New(logger, c.Pebble.HTTPPort, c.Pebble.TLSPort, *strictMode, *resolverAddress, sleepTime)
75101

76102
for keyID, key := range c.Pebble.ExternalAccountMACKeys {
77103
err := db.AddExternalAccountKeyByID(keyID, key)
@@ -117,3 +143,72 @@ func main() {
117143
muxHandler)
118144
cmd.FailOnError(err, "Calling ListenAndServeTLS()")
119145
}
146+
147+
// getEnvSleepTime is the abstraction to decide how long to randomly sleep for. This is
148+
// relatively verbose, due to handling the deprecated PEBBLE_VA_* sleep methods. Most
149+
// of this can be removed after a suitable transition period. The deprecated path is not
150+
// documented.
151+
func getEnvSleepTime(logger *log.Logger, envSleepTime string, envVaNoSleep string, envVaSleepTime string) int {
152+
sleepTime, vaSleepTime, vaNoSleep := parseSleepEnvironment(logger, envSleepTime, envVaNoSleep, envVaSleepTime)
153+
154+
if sleepTime < 0 && !vaNoSleep && vaSleepTime < 0 { // 1
155+
return defaultSleepTime
156+
} else if sleepTime >= 0 && !vaNoSleep && vaSleepTime < 0 { // 2
157+
return sleepTime
158+
} else if sleepTime < 0 && !vaNoSleep && vaSleepTime >= 0 { // 3
159+
return vaSleepTime
160+
} else if sleepTime >= 0 && !vaNoSleep && vaSleepTime >= 0 { // 4
161+
logger.Printf("WARNING: %s is present with %s, ignoring %s", sleepTimeEnvVar, vaSleepTimeEnvVar, vaSleepTimeEnvVar)
162+
return sleepTime
163+
} else if sleepTime < 0 && vaNoSleep && vaSleepTime < 0 { // 5
164+
return 0
165+
} else if sleepTime >= 0 && vaNoSleep && vaSleepTime < 0 { // 6
166+
logger.Printf("WARNING: %s is present with %s, ignoring %s", sleepTimeEnvVar, vaNoSleepEnvVar, vaNoSleepEnvVar)
167+
return sleepTime
168+
} else if sleepTime < 0 && vaNoSleep && vaSleepTime >= 0 { // 7
169+
logger.Printf("WARNING: %s is present with %s, ignoring %s", vaNoSleepEnvVar, vaSleepTimeEnvVar, vaSleepTimeEnvVar)
170+
return 0
171+
} else /*if sleepTime >= 0 && vaNoSleep && vaSleepTime >= 0*/ { // 8
172+
logger.Printf("WARNING: %s is present with %s and %s, ignoring %s and %s", sleepTimeEnvVar, vaNoSleepEnvVar, vaSleepTimeEnvVar, vaNoSleepEnvVar, vaSleepTimeEnvVar)
173+
return sleepTime
174+
}
175+
}
176+
177+
// parseSleepEnvironment exists to make the linter happy. When PEBBLE_VA_* is removed,
178+
// whatever remains of this function should be merged with getEnvSleepTime.
179+
func parseSleepEnvironment(logger *log.Logger, envSleepTime string, envVaNoSleep string, envVaSleepTime string) (int, int, bool) {
180+
sleepTime := -1
181+
if envSleepTime != "" {
182+
i, err := strconv.ParseUint(envSleepTime, 10, 64) // Uint parse makes negatives fail
183+
if err != nil {
184+
logger.Printf("WARNING: Failed to parse %s", sleepTimeEnvVar)
185+
} else {
186+
sleepTime = int(i)
187+
}
188+
}
189+
190+
vaSleepTime := -1
191+
if envVaSleepTime != "" {
192+
logger.Printf("WARNING: %s is deprecated, use %s=%s", vaSleepTimeEnvVar, sleepTimeEnvVar, envVaSleepTime)
193+
i, err := strconv.ParseUint(envVaSleepTime, 10, 64) // Uint parse makes negatives fail
194+
if err != nil {
195+
logger.Printf("WARNING: Failed to parse %s", vaSleepTimeEnvVar)
196+
} else {
197+
vaSleepTime = int(i)
198+
}
199+
}
200+
201+
vaNoSleep := false
202+
if envVaNoSleep != "" {
203+
logger.Printf("WARNING: %s is deprecated, use %s=0", vaNoSleepEnvVar, sleepTimeEnvVar)
204+
}
205+
switch strings.ToLower(envVaNoSleep) {
206+
case "1", "true":
207+
vaNoSleep = true
208+
case "0", "false", "":
209+
// no action
210+
default:
211+
logger.Printf("WARNING: Failed to parse %s", vaNoSleepEnvVar)
212+
}
213+
return sleepTime, vaSleepTime, vaNoSleep
214+
}

cmd/pebble/main_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,105 @@
11
package main
2+
3+
import (
4+
"bytes"
5+
"log"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func newLoggerAndBuffer(testName string) (*log.Logger, *bytes.Buffer) {
11+
buf := &bytes.Buffer{}
12+
return log.New(buf, testName+" ", 0), buf
13+
}
14+
15+
func validateLogs(t *testing.T, logs string, expected []string) {
16+
t.Helper()
17+
18+
if len(expected) == 0 && len(logs) != 0 {
19+
t.Errorf("got logs when none were expected: %s", logs)
20+
} else if strings.Count(logs, "\n") != len(expected) {
21+
t.Errorf("unexpected log count actual=%d, expected=%d", strings.Count(logs, "\n"), len(expected))
22+
}
23+
24+
for _, e := range expected {
25+
if !strings.Contains(logs, e) {
26+
t.Errorf("expected log message [%s] not present", e)
27+
}
28+
}
29+
}
30+
31+
func TestGetEnvSleepTime(t *testing.T) {
32+
// Summary from https://github.com/letsencrypt/pebble/pull/347#issuecomment-861634804:
33+
//
34+
// a. PEBBLE_VA_* missing, PEBBLE_SLEEPTIME missing, 5s delay on both
35+
// b. PEBBLE_VA_* present, PEBBLE_SLEEPTIME missing, VA delay (possibly 0 if NOSLEEP) on both, emit a deprecated message on startup
36+
// c. PEBBLE_VA_* missing, PEBBLE_SLEEPTIME present, specified delay on both
37+
// d. PEBBLE_VA_* present, PEBBLE_SLEEPTIME present, use PEBBLE_SLEEPTIME, emit a warning message on startup that both are present and one is deprecated.
38+
//
39+
// Invalid or negative integers are treated as missing.
40+
//
41+
// This expands to:
42+
// 1. PEBBLE_SLEEPTIME invalid, PEBBLE_VA_NOSLEEP clear, PEBBLE_VA_SLEEPTIME invalid - a
43+
// 2. PEBBLE_SLEEPTIME valid, PEBBLE_VA_NOSLEEP clear, PEBBLE_VA_SLEEPTIME invalid - c
44+
// 3. PEBBLE_SLEEPTIME invalid, PEBBLE_VA_NOSLEEP clear, PEBBLE_VA_SLEEPTIME valid - b (deprecated)
45+
// 4. PEBBLE_SLEEPTIME valid, PEBBLE_VA_NOSLEEP clear, PEBBLE_VA_SLEEPTIME valid - d (deprecated, conflict)
46+
// 5. PEBBLE_SLEEPTIME invalid, PEBBLE_VA_NOSLEEP set, PEBBLE_VA_SLEEPTIME invalid - b (deprecated)
47+
// 6. PEBBLE_SLEEPTIME valid, PEBBLE_VA_NOSLEEP set, PEBBLE_VA_SLEEPTIME invalid - d (deprecated, conflict)
48+
// 7. PEBBLE_SLEEPTIME invalid, PEBBLE_VA_NOSLEEP set, PEBBLE_VA_SLEEPTIME valid - b (deprecated, conflict)
49+
// 8. PEBBLE_SLEEPTIME valid, PEBBLE_VA_NOSLEEP set, PEBBLE_VA_SLEEPTIME valid - d (deprecated, conflict)
50+
t.Parallel()
51+
52+
const (
53+
ignVaSleep = "ignoring PEBBLE_VA_SLEEPTIME"
54+
ignVaNoSleep = "ignoring PEBBLE_VA_NOSLEEP"
55+
ignVaBoth = "ignoring PEBBLE_VA_NOSLEEP and PEBBLE_VA_SLEEPTIME"
56+
depVaSleep = "PEBBLE_VA_SLEEPTIME is deprecated"
57+
depVaNoSleep = "PEBBLE_VA_NOSLEEP is deprecated"
58+
parseSleep = "parse PEBBLE_SLEEPTIME"
59+
parseVaSleep = "parse PEBBLE_VA_SLEEPTIME"
60+
parseVaNoSleep = "parse PEBBLE_VA_NOSLEEP"
61+
)
62+
63+
tests := []struct {
64+
name string
65+
envSleepTime string
66+
envVaNoSleep string
67+
envVaSleepTime string
68+
expected int
69+
expectedLogs []string
70+
}{
71+
// happy paths
72+
{"defaults", "", "", "", defaultSleepTime, nil}, // 1
73+
{"sleep", "3", "", "", 3, nil}, // 2
74+
{"vasleep", "", "", "3", 3, []string{depVaSleep}}, // 3
75+
{"sleep+vasleep", "3", "", "6", 3, []string{depVaSleep, ignVaSleep}}, // 4
76+
{"nosleep", "", "1", "", 0, []string{depVaNoSleep}}, // 5
77+
{"sleep+nosleep", "3", "1", "", 3, []string{depVaNoSleep, ignVaNoSleep}}, // 6
78+
{"nosleep+vasleep", "", "1", "3", 0, []string{depVaSleep, depVaNoSleep, ignVaSleep}}, // 7
79+
{"sleep+vasleep+nosleep", "3", "1", "6", 3, []string{depVaSleep, depVaNoSleep, ignVaBoth}}, // 8
80+
81+
// parse failures
82+
{"sleep-inv", "f", "", "", defaultSleepTime, []string{parseSleep}},
83+
{"sleep-inv+vasleep+nosleep", "f", "1", "6", 0, []string{parseSleep, depVaSleep, depVaNoSleep, ignVaSleep}},
84+
{"vasleep-inv", "", "", "f", defaultSleepTime, []string{depVaSleep, parseVaSleep}},
85+
{"sleep+vasleep-inv+nosleep", "", "1", "f", 0, []string{depVaSleep, parseVaSleep, depVaNoSleep}},
86+
{"nosleep-inv", "", "f", "", defaultSleepTime, []string{depVaNoSleep, parseVaNoSleep}},
87+
}
88+
89+
for _, test := range tests {
90+
t.Run(test.name, func(t *testing.T) {
91+
logger, logBuffer := newLoggerAndBuffer(test.name)
92+
if actual := getEnvSleepTime(logger, test.envSleepTime, test.envVaNoSleep, test.envVaSleepTime); actual != test.expected {
93+
t.Errorf("envSleepTime=[%s], envVaNoSleep=[%s], envVaSleepTime=[%s], expected=%d, actual=%d",
94+
test.envSleepTime,
95+
test.envVaNoSleep,
96+
test.envVaSleepTime,
97+
test.expected,
98+
actual,
99+
)
100+
}
101+
validateLogs(t, logBuffer.String(), test.expectedLogs)
102+
t.Logf("captured logs:\n%s", logBuffer.String())
103+
})
104+
}
105+
}

0 commit comments

Comments
 (0)