Skip to content

Commit a8586d0

Browse files
authored
Add integration test for precertificate OCSP. (letsencrypt#4417)
This test adds support in ct-test-srv for rejecting precertificates by hostname, in order to artificially generate a condition where a precertificate is issued but no final certificate can be issued. Right now the final check in the test is temporarily disabled until the feature is fixed. Also, as our first Go-based integration test, this pulls in the eggsampler/acme Go client, and adds some suport in integration-test.py. This also refactors ct-test-srv slightly to use a ServeMux, and fixes a couple of cases of not returning immediately on error.
1 parent b905691 commit a8586d0

File tree

27 files changed

+2195
-59
lines changed

27 files changed

+2195
-59
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/beeker1121/goque v0.0.0-20170321141813-4044bc29b280
99
github.com/beorn7/perks v0.0.0-20160229213445-3ac7bf7a47d1 // indirect
1010
github.com/cloudflare/cfssl v0.0.0-20190716004220-2185c182e6ba
11+
github.com/eggsampler/acme/v2 v2.0.1
1112
github.com/go-gorp/gorp v2.0.0+incompatible // indirect
1213
github.com/go-sql-driver/mysql v0.0.0-20170715192408-3955978caca4
1314
github.com/golang/mock v1.2.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ github.com/cloudflare/cfssl v0.0.0-20190726000631-633726f6bcb7 h1:Puu1hUwfps3+1C
2121
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2222
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2323
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24+
github.com/eggsampler/acme/v2 v2.0.1 h1:SfhaHP/6jCWOEMdzWI/9pmNDm2yuCHd4agW7u29fEJY=
25+
github.com/eggsampler/acme/v2 v2.0.1/go.mod h1:kMR4S+ZCJtXb0WCg8MJkUKFQ0pyLEMQ9l5JA+CHvK2Y=
2426
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
2527
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
2628
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=

test/ct-test-srv/main.go

Lines changed: 128 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -27,83 +27,146 @@ type ctSubmissionRequest struct {
2727

2828
type integrationSrv struct {
2929
sync.Mutex
30-
submissions map[string]int64
30+
submissions map[string]int64
31+
// Hostnames where we refuse to provide an SCT. This is to exercise the code
32+
// path where all CT servers fail.
33+
rejectHosts map[string]bool
34+
// A list of entries that we rejected based on rejectHosts.
35+
rejected []string
3136
key *ecdsa.PrivateKey
3237
latencySchedule []float64
3338
latencyItem int
3439
}
3540

36-
func (is *integrationSrv) handler(w http.ResponseWriter, r *http.Request) {
37-
switch r.URL.Path {
38-
case "/ct/v1/add-pre-chain":
39-
fallthrough
40-
case "/ct/v1/add-chain":
41-
if r.Method != "POST" {
42-
http.NotFound(w, r)
43-
return
44-
}
45-
bodyBytes, err := ioutil.ReadAll(r.Body)
46-
if err != nil {
47-
http.Error(w, err.Error(), http.StatusBadRequest)
48-
}
41+
func readJSON(w http.ResponseWriter, r *http.Request, output interface{}) error {
42+
if r.Method != "POST" {
43+
return fmt.Errorf("incorrect method; only POST allowed")
44+
}
45+
bodyBytes, err := ioutil.ReadAll(r.Body)
46+
if err != nil {
47+
return err
48+
}
4949

50-
var addChainReq ctSubmissionRequest
51-
err = json.Unmarshal(bodyBytes, &addChainReq)
52-
if err != nil {
53-
http.Error(w, err.Error(), http.StatusBadRequest)
54-
}
55-
if len(addChainReq.Chain) == 0 {
56-
w.WriteHeader(400)
57-
return
58-
}
50+
err = json.Unmarshal(bodyBytes, output)
51+
if err != nil {
52+
return err
53+
}
54+
return nil
55+
}
5956

60-
precert := false
61-
if r.URL.Path == "/ct/v1/add-pre-chain" {
62-
precert = true
63-
}
57+
func (is *integrationSrv) addChain(w http.ResponseWriter, r *http.Request) {
58+
is.addChainOrPre(w, r, false)
59+
}
6460

65-
b, err := base64.StdEncoding.DecodeString(addChainReq.Chain[0])
66-
if err != nil {
67-
w.WriteHeader(400)
68-
return
69-
}
70-
cert, err := x509.ParseCertificate(b)
71-
if err != nil {
72-
w.WriteHeader(400)
73-
return
74-
}
75-
hostnames := strings.Join(cert.DNSNames, ",")
61+
// addRejectHost takes a JSON POST with a "host" field; any subsequent
62+
// submissions for that host will get a 400 error.
63+
func (is *integrationSrv) addRejectHost(w http.ResponseWriter, r *http.Request) {
64+
var rejectHostReq struct {
65+
Host string
66+
}
67+
err := readJSON(w, r, &rejectHostReq)
68+
if err != nil {
69+
http.Error(w, err.Error(), http.StatusBadRequest)
70+
return
71+
}
7672

77-
is.Lock()
78-
is.submissions[hostnames]++
79-
is.Unlock()
73+
is.Lock()
74+
defer is.Unlock()
75+
is.rejectHosts[rejectHostReq.Host] = true
76+
w.Write([]byte{})
77+
}
78+
79+
// getRejections returns a JSON array containing strings; those strings are
80+
// base64 encodings of certificates or precertificates that were rejected due to
81+
// the rejectHosts mechanism.
82+
func (is *integrationSrv) getRejections(w http.ResponseWriter, r *http.Request) {
83+
is.Lock()
84+
defer is.Unlock()
85+
output, err := json.Marshal(is.rejected)
86+
if err != nil {
87+
http.Error(w, err.Error(), http.StatusBadRequest)
88+
return
89+
}
8090

81-
if is.latencySchedule != nil {
82-
is.Lock()
83-
sleepTime := time.Duration(is.latencySchedule[is.latencyItem%len(is.latencySchedule)]) * time.Second
84-
is.latencyItem++
91+
w.WriteHeader(http.StatusOK)
92+
w.Write(output)
93+
}
94+
95+
func (is *integrationSrv) addPreChain(w http.ResponseWriter, r *http.Request) {
96+
is.addChainOrPre(w, r, true)
97+
}
98+
99+
func (is *integrationSrv) addChainOrPre(w http.ResponseWriter, r *http.Request, precert bool) {
100+
if r.Method != "POST" {
101+
http.NotFound(w, r)
102+
return
103+
}
104+
bodyBytes, err := ioutil.ReadAll(r.Body)
105+
if err != nil {
106+
http.Error(w, err.Error(), http.StatusBadRequest)
107+
return
108+
}
109+
110+
var addChainReq ctSubmissionRequest
111+
err = json.Unmarshal(bodyBytes, &addChainReq)
112+
if err != nil {
113+
http.Error(w, err.Error(), http.StatusBadRequest)
114+
return
115+
}
116+
if len(addChainReq.Chain) == 0 {
117+
w.WriteHeader(400)
118+
return
119+
}
120+
121+
b, err := base64.StdEncoding.DecodeString(addChainReq.Chain[0])
122+
if err != nil {
123+
w.WriteHeader(400)
124+
return
125+
}
126+
cert, err := x509.ParseCertificate(b)
127+
if err != nil {
128+
w.WriteHeader(400)
129+
return
130+
}
131+
hostnames := strings.Join(cert.DNSNames, ",")
132+
133+
is.Lock()
134+
for _, h := range cert.DNSNames {
135+
if is.rejectHosts[h] {
85136
is.Unlock()
86-
time.Sleep(sleepTime)
87-
}
88-
w.WriteHeader(http.StatusOK)
89-
w.Write(publisher.CreateTestingSignedSCT(addChainReq.Chain, is.key, precert, time.Now()))
90-
case "/submissions":
91-
if r.Method != "GET" {
92-
http.NotFound(w, r)
137+
is.rejected = append(is.rejected, addChainReq.Chain[0])
138+
w.WriteHeader(400)
93139
return
94140
}
141+
}
142+
143+
is.submissions[hostnames]++
144+
is.Unlock()
95145

146+
if is.latencySchedule != nil {
96147
is.Lock()
97-
hostnames := r.URL.Query().Get("hostnames")
98-
submissions := is.submissions[hostnames]
148+
sleepTime := time.Duration(is.latencySchedule[is.latencyItem%len(is.latencySchedule)]) * time.Second
149+
is.latencyItem++
99150
is.Unlock()
151+
time.Sleep(sleepTime)
152+
}
153+
w.WriteHeader(http.StatusOK)
154+
w.Write(publisher.CreateTestingSignedSCT(addChainReq.Chain, is.key, precert, time.Now()))
155+
}
100156

101-
w.WriteHeader(http.StatusOK)
102-
fmt.Fprintf(w, "%d", submissions)
103-
default:
157+
func (is *integrationSrv) getSubmissions(w http.ResponseWriter, r *http.Request) {
158+
if r.Method != "GET" {
104159
http.NotFound(w, r)
105160
return
106161
}
162+
163+
is.Lock()
164+
hostnames := r.URL.Query().Get("hostnames")
165+
submissions := is.submissions[hostnames]
166+
is.Unlock()
167+
168+
w.WriteHeader(http.StatusOK)
169+
fmt.Fprintf(w, "%d", submissions)
107170
}
108171

109172
type config struct {
@@ -139,10 +202,17 @@ func runPersonality(p Personality) {
139202
key: key,
140203
latencySchedule: p.LatencySchedule,
141204
submissions: make(map[string]int64),
205+
rejectHosts: make(map[string]bool),
142206
}
207+
m := http.NewServeMux()
208+
m.HandleFunc("/submissions", is.getSubmissions)
209+
m.HandleFunc("/ct/v1/add-pre-chain", is.addPreChain)
210+
m.HandleFunc("/ct/v1/add-chain", is.addChain)
211+
m.HandleFunc("/add-reject-host", is.addRejectHost)
212+
m.HandleFunc("/get-rejections", is.getRejections)
143213
srv := &http.Server{
144214
Addr: p.Addr,
145-
Handler: http.HandlerFunc(is.handler),
215+
Handler: m,
146216
}
147217
log.Printf("ct-test-srv on %s with pubkey %s", p.Addr,
148218
base64.StdEncoding.EncodeToString(pubKeyBytes))

test/integration-test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def run_client_tests():
3838
cmd = os.path.join(root, 'tests', 'boulder-integration.sh')
3939
run(cmd, cwd=root)
4040

41+
def run_go_tests():
42+
run("go test -tags integration -count=1 ./test/integration")
43+
4144
def run_expired_authz_purger():
4245
# Note: This test must be run after all other tests that depend on
4346
# authorizations added to the database during setup
@@ -256,6 +259,8 @@ def main():
256259
if args.run_certbot:
257260
run_client_tests()
258261

262+
run_go_tests()
263+
259264
if args.custom:
260265
run(args.custom)
261266

0 commit comments

Comments
 (0)