1 package notmain
2
3 import (
4 "context"
5 "crypto/rand"
6 "fmt"
7 "html/template"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12
13 "github.com/jmhodges/clock"
14 "github.com/letsencrypt/boulder/core"
15 "github.com/letsencrypt/boulder/db"
16 blog "github.com/letsencrypt/boulder/log"
17 "github.com/letsencrypt/boulder/mocks"
18 rapb "github.com/letsencrypt/boulder/ra/proto"
19 "github.com/letsencrypt/boulder/sa"
20 "github.com/letsencrypt/boulder/test"
21 "github.com/letsencrypt/boulder/test/vars"
22 "github.com/prometheus/client_golang/prometheus"
23 "google.golang.org/grpc"
24 "google.golang.org/protobuf/types/known/emptypb"
25 )
26
27 func randHash(t *testing.T) []byte {
28 t.Helper()
29 h := make([]byte, 32)
30 _, err := rand.Read(h)
31 test.AssertNotError(t, err, "failed to read rand")
32 return h
33 }
34
35 func insertBlockedRow(t *testing.T, dbMap *db.WrappedMap, fc clock.Clock, hash []byte, by int64, checked bool) {
36 t.Helper()
37 _, err := dbMap.ExecContext(context.Background(), `INSERT INTO blockedKeys
38 (keyHash, added, source, revokedBy, extantCertificatesChecked)
39 VALUES
40 (?, ?, ?, ?, ?)`,
41 hash,
42 fc.Now(),
43 1,
44 by,
45 checked,
46 )
47 test.AssertNotError(t, err, "failed to add test row")
48 }
49
50 func TestSelectUncheckedRows(t *testing.T) {
51 ctx := context.Background()
52
53 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
54 test.AssertNotError(t, err, "failed setting up db client")
55 defer test.ResetBoulderTestDatabase(t)()
56
57 fc := clock.NewFake()
58
59 bkr := &badKeyRevoker{
60 dbMap: dbMap,
61 logger: blog.NewMock(),
62 clk: fc,
63 }
64
65 hashA, hashB, hashC := randHash(t), randHash(t), randHash(t)
66 insertBlockedRow(t, dbMap, fc, hashA, 1, true)
67 count, err := bkr.countUncheckedKeys(ctx)
68 test.AssertNotError(t, err, "countUncheckedKeys failed")
69 test.AssertEquals(t, count, 0)
70 _, err = bkr.selectUncheckedKey(ctx)
71 test.AssertError(t, err, "selectUncheckedKey didn't fail with no rows to process")
72 test.Assert(t, db.IsNoRows(err), "returned error is not sql.ErrNoRows")
73 insertBlockedRow(t, dbMap, fc, hashB, 1, false)
74 insertBlockedRow(t, dbMap, fc, hashC, 1, false)
75 count, err = bkr.countUncheckedKeys(ctx)
76 test.AssertNotError(t, err, "countUncheckedKeys failed")
77 test.AssertEquals(t, count, 2)
78 row, err := bkr.selectUncheckedKey(ctx)
79 test.AssertNotError(t, err, "selectUncheckKey failed")
80 test.AssertByteEquals(t, row.KeyHash, hashB)
81 test.AssertEquals(t, row.RevokedBy, int64(1))
82 }
83
84 func insertRegistration(t *testing.T, dbMap *db.WrappedMap, fc clock.Clock, addrs ...string) int64 {
85 t.Helper()
86 jwkHash := make([]byte, 32)
87 _, err := rand.Read(jwkHash)
88 test.AssertNotError(t, err, "failed to read rand")
89 contactStr := "[]"
90 if len(addrs) > 0 {
91 contacts := []string{}
92 for _, addr := range addrs {
93 contacts = append(contacts, fmt.Sprintf(`"mailto:%s"`, addr))
94 }
95 contactStr = fmt.Sprintf("[%s]", strings.Join(contacts, ","))
96 }
97 res, err := dbMap.ExecContext(
98 context.Background(),
99 "INSERT INTO registrations (jwk, jwk_sha256, contact, agreement, initialIP, createdAt, status, LockCol) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
100 []byte{},
101 fmt.Sprintf("%x", jwkHash),
102 contactStr,
103 "yes",
104 []byte{},
105 fc.Now(),
106 string(core.StatusValid),
107 0,
108 )
109 test.AssertNotError(t, err, "failed to insert test registrations row")
110 regID, err := res.LastInsertId()
111 test.AssertNotError(t, err, "failed to get registration ID")
112 return regID
113 }
114
115 type ExpiredStatus bool
116
117 const (
118 Expired = ExpiredStatus(true)
119 Unexpired = ExpiredStatus(false)
120 Revoked = core.OCSPStatusRevoked
121 Unrevoked = core.OCSPStatusGood
122 )
123
124 func insertGoodCert(t *testing.T, dbMap *db.WrappedMap, fc clock.Clock, keyHash []byte, serial string, regID int64) {
125 insertCert(t, dbMap, fc, keyHash, serial, regID, Unexpired, Unrevoked)
126 }
127
128 func insertCert(t *testing.T, dbMap *db.WrappedMap, fc clock.Clock, keyHash []byte, serial string, regID int64, expiredStatus ExpiredStatus, status core.OCSPStatus) {
129 t.Helper()
130 ctx := context.Background()
131
132 expiresOffset := 0 * time.Second
133 if !expiredStatus {
134 expiresOffset = 90*24*time.Hour - 1*time.Second
135 }
136
137 _, err := dbMap.ExecContext(
138 ctx,
139 `INSERT IGNORE INTO keyHashToSerial
140 (keyHash, certNotAfter, certSerial) VALUES
141 (?, ?, ?)`,
142 keyHash,
143 fc.Now().Add(expiresOffset),
144 serial,
145 )
146 test.AssertNotError(t, err, "failed to insert test keyHashToSerial row")
147
148 _, err = dbMap.ExecContext(
149 ctx,
150 "INSERT INTO certificateStatus (serial, status, isExpired, ocspLastUpdated, revokedDate, revokedReason, lastExpirationNagSent) VALUES (?, ?, ?, ?, ?, ?, ?)",
151 serial,
152 status,
153 expiredStatus,
154 fc.Now(),
155 time.Time{},
156 0,
157 time.Time{},
158 )
159 test.AssertNotError(t, err, "failed to insert test certificateStatus row")
160
161 _, err = dbMap.ExecContext(
162 ctx,
163 "INSERT INTO precertificates (serial, registrationID, der, issued, expires) VALUES (?, ?, ?, ?, ?)",
164 serial,
165 regID,
166 []byte{1, 2, 3},
167 fc.Now(),
168 fc.Now().Add(expiresOffset),
169 )
170 test.AssertNotError(t, err, "failed to insert test certificateStatus row")
171
172 _, err = dbMap.ExecContext(
173 ctx,
174 "INSERT INTO certificates (serial, registrationID, der, digest, issued, expires) VALUES (?, ?, ?, ?, ?, ?)",
175 serial,
176 regID,
177 []byte{1, 2, 3},
178 []byte{},
179 fc.Now(),
180 fc.Now().Add(expiresOffset),
181 )
182 test.AssertNotError(t, err, "failed to insert test certificates row")
183 }
184
185
186
187
188 func TestFindUnrevokedNoRows(t *testing.T) {
189 ctx := context.Background()
190
191 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
192 test.AssertNotError(t, err, "failed setting up db client")
193 defer test.ResetBoulderTestDatabase(t)()
194
195 fc := clock.NewFake()
196
197 hashA := randHash(t)
198 _, err = dbMap.ExecContext(
199 ctx,
200 "INSERT INTO keyHashToSerial (keyHash, certNotAfter, certSerial) VALUES (?, ?, ?)",
201 hashA,
202 fc.Now().Add(90*24*time.Hour-1*time.Second),
203 "zz",
204 )
205 test.AssertNotError(t, err, "failed to insert test keyHashToSerial row")
206
207 bkr := &badKeyRevoker{dbMap: dbMap, serialBatchSize: 1, maxRevocations: 10, clk: fc}
208 _, err = bkr.findUnrevoked(ctx, uncheckedBlockedKey{KeyHash: hashA})
209 test.Assert(t, db.IsNoRows(err), "expected NoRows error")
210 }
211
212 func TestFindUnrevoked(t *testing.T) {
213 ctx := context.Background()
214
215 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
216 test.AssertNotError(t, err, "failed setting up db client")
217 defer test.ResetBoulderTestDatabase(t)()
218
219 fc := clock.NewFake()
220
221 regID := insertRegistration(t, dbMap, fc)
222
223 bkr := &badKeyRevoker{dbMap: dbMap, serialBatchSize: 1, maxRevocations: 10, clk: fc}
224
225 hashA := randHash(t)
226
227 insertCert(t, dbMap, fc, hashA, "ff", regID, Unexpired, Unrevoked)
228
229 insertCert(t, dbMap, fc, hashA, "ff", regID, Unexpired, Unrevoked)
230
231 insertCert(t, dbMap, fc, hashA, "ee", regID, Expired, Unrevoked)
232
233 insertCert(t, dbMap, fc, hashA, "dd", regID, Unexpired, Revoked)
234
235 rows, err := bkr.findUnrevoked(ctx, uncheckedBlockedKey{KeyHash: hashA})
236 test.AssertNotError(t, err, "findUnrevoked failed")
237 test.AssertEquals(t, len(rows), 1)
238 test.AssertEquals(t, rows[0].Serial, "ff")
239 test.AssertEquals(t, rows[0].RegistrationID, int64(1))
240 test.AssertByteEquals(t, rows[0].DER, []byte{1, 2, 3})
241
242 bkr.maxRevocations = 0
243 _, err = bkr.findUnrevoked(ctx, uncheckedBlockedKey{KeyHash: hashA})
244 test.AssertError(t, err, "findUnrevoked didn't fail with 0 maxRevocations")
245 test.AssertEquals(t, err.Error(), fmt.Sprintf("too many certificates to revoke associated with %x: got 1, max 0", hashA))
246 }
247
248 func TestResolveContacts(t *testing.T) {
249 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
250 test.AssertNotError(t, err, "failed setting up db client")
251 defer test.ResetBoulderTestDatabase(t)()
252
253 fc := clock.NewFake()
254
255 bkr := &badKeyRevoker{dbMap: dbMap, clk: fc}
256
257 regIDA := insertRegistration(t, dbMap, fc)
258 regIDB := insertRegistration(t, dbMap, fc, "example.com", "example-2.com")
259 regIDC := insertRegistration(t, dbMap, fc, "example.com")
260 regIDD := insertRegistration(t, dbMap, fc, "example-2.com")
261
262 idToEmail, err := bkr.resolveContacts(context.Background(), []int64{regIDA, regIDB, regIDC, regIDD})
263 test.AssertNotError(t, err, "resolveContacts failed")
264 test.AssertDeepEquals(t, idToEmail, map[int64][]string{
265 regIDA: {""},
266 regIDB: {"example.com", "example-2.com"},
267 regIDC: {"example.com"},
268 regIDD: {"example-2.com"},
269 })
270 }
271
272 var testTemplate = template.Must(template.New("testing").Parse("{{range .}}{{.}}\n{{end}}"))
273
274 func TestSendMessage(t *testing.T) {
275 mm := &mocks.Mailer{}
276 fc := clock.NewFake()
277 bkr := &badKeyRevoker{mailer: mm, emailSubject: "testing", emailTemplate: testTemplate, clk: fc}
278
279 maxSerials = 2
280 err := bkr.sendMessage("example.com", []string{"a", "b", "c"})
281 test.AssertNotError(t, err, "sendMessages failed")
282 test.AssertEquals(t, len(mm.Messages), 1)
283 test.AssertEquals(t, mm.Messages[0].To, "example.com")
284 test.AssertEquals(t, mm.Messages[0].Subject, bkr.emailSubject)
285 test.AssertEquals(t, mm.Messages[0].Body, "a\nb\nand 1 more certificates.\n")
286
287 }
288
289 type mockRevoker struct {
290 revoked int
291 mu sync.Mutex
292 }
293
294 func (mr *mockRevoker) AdministrativelyRevokeCertificate(ctx context.Context, in *rapb.AdministrativelyRevokeCertificateRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) {
295 mr.mu.Lock()
296 defer mr.mu.Unlock()
297 mr.revoked++
298 return nil, nil
299 }
300
301 func TestRevokeCerts(t *testing.T) {
302 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
303 test.AssertNotError(t, err, "failed setting up db client")
304 defer test.ResetBoulderTestDatabase(t)()
305
306 fc := clock.NewFake()
307 mm := &mocks.Mailer{}
308 mr := &mockRevoker{}
309 bkr := &badKeyRevoker{dbMap: dbMap, raClient: mr, mailer: mm, emailSubject: "testing", emailTemplate: testTemplate, clk: fc}
310
311 err = bkr.revokeCerts([]string{"revoker@example.com", "revoker-b@example.com"}, map[string][]unrevokedCertificate{
312 "revoker@example.com": {{ID: 0, Serial: "ff"}},
313 "revoker-b@example.com": {{ID: 0, Serial: "ff"}},
314 "other@example.com": {{ID: 1, Serial: "ee"}},
315 })
316 test.AssertNotError(t, err, "revokeCerts failed")
317 test.AssertEquals(t, len(mm.Messages), 1)
318 test.AssertEquals(t, mm.Messages[0].To, "other@example.com")
319 test.AssertEquals(t, mm.Messages[0].Subject, bkr.emailSubject)
320 test.AssertEquals(t, mm.Messages[0].Body, "ee\n")
321 }
322
323 func TestCertificateAbsent(t *testing.T) {
324 ctx := context.Background()
325
326 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
327 test.AssertNotError(t, err, "failed setting up db client")
328 defer test.ResetBoulderTestDatabase(t)()
329
330 fc := clock.NewFake()
331
332
333 regIDA := insertRegistration(t, dbMap, fc, "example.com")
334 hashA := randHash(t)
335 insertBlockedRow(t, dbMap, fc, hashA, regIDA, false)
336
337
338
339 _, err = dbMap.ExecContext(
340 ctx,
341 "INSERT INTO keyHashToSerial (keyHash, certNotAfter, certSerial) VALUES (?, ?, ?)",
342 hashA,
343 fc.Now().Add(90*24*time.Hour-1*time.Second),
344 "ffaaee",
345 )
346 test.AssertNotError(t, err, "failed to insert test keyHashToSerial row")
347
348 bkr := &badKeyRevoker{
349 dbMap: dbMap,
350 maxRevocations: 1,
351 serialBatchSize: 1,
352 raClient: &mockRevoker{},
353 mailer: &mocks.Mailer{},
354 emailSubject: "testing",
355 emailTemplate: testTemplate,
356 logger: blog.NewMock(),
357 clk: fc,
358 }
359 _, err = bkr.invoke(ctx)
360 test.AssertError(t, err, "expected error when row in keyHashToSerial didn't have a matching cert")
361 }
362
363 func TestInvoke(t *testing.T) {
364 ctx := context.Background()
365
366 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
367 test.AssertNotError(t, err, "failed setting up db client")
368 defer test.ResetBoulderTestDatabase(t)()
369
370 fc := clock.NewFake()
371
372 mm := &mocks.Mailer{}
373 mr := &mockRevoker{}
374 bkr := &badKeyRevoker{
375 dbMap: dbMap,
376 maxRevocations: 10,
377 serialBatchSize: 1,
378 raClient: mr,
379 mailer: mm,
380 emailSubject: "testing",
381 emailTemplate: testTemplate,
382 logger: blog.NewMock(),
383 clk: fc,
384 }
385
386
387 regIDA := insertRegistration(t, dbMap, fc, "example.com")
388 regIDB := insertRegistration(t, dbMap, fc, "example.com")
389 regIDC := insertRegistration(t, dbMap, fc, "other.example.com", "uno.example.com")
390 regIDD := insertRegistration(t, dbMap, fc)
391 hashA := randHash(t)
392 insertBlockedRow(t, dbMap, fc, hashA, regIDC, false)
393 insertGoodCert(t, dbMap, fc, hashA, "ff", regIDA)
394 insertGoodCert(t, dbMap, fc, hashA, "ee", regIDB)
395 insertGoodCert(t, dbMap, fc, hashA, "dd", regIDC)
396 insertGoodCert(t, dbMap, fc, hashA, "cc", regIDD)
397
398 noWork, err := bkr.invoke(ctx)
399 test.AssertNotError(t, err, "invoke failed")
400 test.AssertEquals(t, noWork, false)
401 test.AssertEquals(t, mr.revoked, 4)
402 test.AssertEquals(t, len(mm.Messages), 1)
403 test.AssertEquals(t, mm.Messages[0].To, "example.com")
404 test.AssertMetricWithLabelsEquals(t, keysToProcess, prometheus.Labels{}, 1)
405
406 var checked struct {
407 ExtantCertificatesChecked bool
408 }
409 err = dbMap.SelectOne(ctx, &checked, "SELECT extantCertificatesChecked FROM blockedKeys WHERE keyHash = ?", hashA)
410 test.AssertNotError(t, err, "failed to select row from blockedKeys")
411 test.AssertEquals(t, checked.ExtantCertificatesChecked, true)
412
413
414 hashB := randHash(t)
415 insertBlockedRow(t, dbMap, fc, hashB, regIDC, false)
416 insertCert(t, dbMap, fc, hashB, "bb", regIDA, Expired, Revoked)
417
418 noWork, err = bkr.invoke(ctx)
419 test.AssertNotError(t, err, "invoke failed")
420 test.AssertEquals(t, noWork, false)
421
422 checked.ExtantCertificatesChecked = false
423 err = dbMap.SelectOne(ctx, &checked, "SELECT extantCertificatesChecked FROM blockedKeys WHERE keyHash = ?", hashB)
424 test.AssertNotError(t, err, "failed to select row from blockedKeys")
425 test.AssertEquals(t, checked.ExtantCertificatesChecked, true)
426
427 noWork, err = bkr.invoke(ctx)
428 test.AssertNotError(t, err, "invoke failed")
429 test.AssertEquals(t, noWork, true)
430 }
431
432 func TestInvokeRevokerHasNoExtantCerts(t *testing.T) {
433
434
435
436
437
438 dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
439 test.AssertNotError(t, err, "failed setting up db client")
440 defer test.ResetBoulderTestDatabase(t)()
441
442 fc := clock.NewFake()
443
444 mm := &mocks.Mailer{}
445 mr := &mockRevoker{}
446 bkr := &badKeyRevoker{dbMap: dbMap,
447 maxRevocations: 10,
448 serialBatchSize: 1,
449 raClient: mr,
450 mailer: mm,
451 emailSubject: "testing",
452 emailTemplate: testTemplate,
453 logger: blog.NewMock(),
454 clk: fc,
455 }
456
457
458 regIDA := insertRegistration(t, dbMap, fc, "a@example.com")
459 regIDB := insertRegistration(t, dbMap, fc, "a@example.com")
460 regIDC := insertRegistration(t, dbMap, fc, "b@example.com")
461
462 hashA := randHash(t)
463
464 insertBlockedRow(t, dbMap, fc, hashA, regIDA, false)
465
466 insertGoodCert(t, dbMap, fc, hashA, "ee", regIDB)
467 insertGoodCert(t, dbMap, fc, hashA, "dd", regIDB)
468 insertGoodCert(t, dbMap, fc, hashA, "cc", regIDC)
469 insertGoodCert(t, dbMap, fc, hashA, "bb", regIDC)
470
471 noWork, err := bkr.invoke(context.Background())
472 test.AssertNotError(t, err, "invoke failed")
473 test.AssertEquals(t, noWork, false)
474 test.AssertEquals(t, mr.revoked, 4)
475 test.AssertEquals(t, len(mm.Messages), 1)
476 test.AssertEquals(t, mm.Messages[0].To, "b@example.com")
477 }
478
479 func TestBackoffPolicy(t *testing.T) {
480 fc := clock.NewFake()
481 mocklog := blog.NewMock()
482 bkr := &badKeyRevoker{
483 clk: fc,
484 backoffIntervalMax: time.Second * 60,
485 backoffIntervalBase: time.Second * 1,
486 backoffFactor: 1.3,
487 logger: mocklog,
488 }
489
490
491 bkr.backoff()
492 resultLog := mocklog.GetAllMatching("INFO: backoff trying again in")
493 if len(resultLog) == 0 {
494 t.Fatalf("no backoff loglines found")
495 }
496
497
498 bkr.backoffReset()
499 test.AssertEquals(t, bkr.backoffTicker, 0)
500 }
501
View as plain text