1 package notmain
2
3 import (
4 "context"
5 "crypto/ecdsa"
6 "crypto/elliptic"
7 "crypto/rand"
8 "crypto/x509"
9 "errors"
10 "fmt"
11 "math/big"
12 "net"
13 "strings"
14 "testing"
15 "text/template"
16 "time"
17
18 "github.com/jmhodges/clock"
19 "github.com/letsencrypt/boulder/core"
20 corepb "github.com/letsencrypt/boulder/core/proto"
21 "github.com/letsencrypt/boulder/db"
22 berrors "github.com/letsencrypt/boulder/errors"
23 blog "github.com/letsencrypt/boulder/log"
24 bmail "github.com/letsencrypt/boulder/mail"
25 "github.com/letsencrypt/boulder/metrics"
26 "github.com/letsencrypt/boulder/mocks"
27 "github.com/letsencrypt/boulder/sa"
28 sapb "github.com/letsencrypt/boulder/sa/proto"
29 "github.com/letsencrypt/boulder/sa/satest"
30 "github.com/letsencrypt/boulder/test"
31 isa "github.com/letsencrypt/boulder/test/inmem/sa"
32 "github.com/letsencrypt/boulder/test/vars"
33 "github.com/prometheus/client_golang/prometheus"
34 io_prometheus_client "github.com/prometheus/client_model/go"
35 "google.golang.org/grpc"
36 )
37
38 type fakeRegStore struct {
39 RegByID map[int64]*corepb.Registration
40 }
41
42 func (f fakeRegStore) GetRegistration(ctx context.Context, req *sapb.RegistrationID, _ ...grpc.CallOption) (*corepb.Registration, error) {
43 r, ok := f.RegByID[req.Id]
44 if !ok {
45 return r, berrors.NotFoundError("no registration found for %q", req.Id)
46 }
47 return r, nil
48 }
49
50 func newFakeRegStore() fakeRegStore {
51 return fakeRegStore{RegByID: make(map[int64]*corepb.Registration)}
52 }
53
54 const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`
55 const testEmailSubject = `email subject for test`
56 const emailARaw = "rolandshoemaker@gmail.com"
57 const emailBRaw = "test@gmail.com"
58
59 var (
60 emailA = "mailto:" + emailARaw
61 emailB = "mailto:" + emailBRaw
62 jsonKeyA = []byte(`{
63 "kty":"RSA",
64 "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
65 "e":"AQAB"
66 }`)
67 jsonKeyB = []byte(`{
68 "kty":"RSA",
69 "n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
70 "e":"AAEAAQ"
71 }`)
72 jsonKeyC = []byte(`{
73 "kty":"RSA",
74 "n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ",
75 "e":"AQAB"
76 }`)
77 tmpl = template.Must(template.New("expiry-email").Parse(testTmpl))
78 subjTmpl = template.Must(template.New("expiry-email-subject").Parse("Testing: " + defaultExpirationSubject))
79 )
80
81 func TestSendNagsManyCerts(t *testing.T) {
82 mc := mocks.Mailer{}
83 rs := newFakeRegStore()
84 fc := clock.NewFake()
85
86 staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
87 tmpl := template.Must(template.New("expiry-email").Parse(
88 `cert for DNS names {{.TruncatedDNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`))
89
90 m := mailer{
91 log: blog.NewMock(),
92 mailer: &mc,
93 emailTemplate: tmpl,
94 addressLimiter: &limiter{clk: fc, limit: 4},
95
96 subjectTemplate: staticTmpl,
97 rs: rs,
98 clk: fc,
99 stats: initStats(metrics.NoopRegisterer),
100 }
101
102 var certs []*x509.Certificate
103 for i := 0; i < 101; i++ {
104 certs = append(certs, &x509.Certificate{
105 SerialNumber: big.NewInt(0x0304),
106 NotAfter: fc.Now().AddDate(0, 0, 2),
107 DNSNames: []string{fmt.Sprintf("example-%d.com", i)},
108 })
109 }
110
111 conn, err := m.mailer.Connect()
112 test.AssertNotError(t, err, "connecting SMTP")
113 err = m.sendNags(conn, []string{emailA}, certs)
114 test.AssertNotError(t, err, "sending mail")
115
116 test.AssertEquals(t, len(mc.Messages), 1)
117 if len(strings.Split(mc.Messages[0].Body, "\n")) > 100 {
118 t.Errorf("Expected mailed message to truncate after 100 domains, got: %q", mc.Messages[0].Body)
119 }
120 }
121
122 func TestSendNags(t *testing.T) {
123 mc := mocks.Mailer{}
124 rs := newFakeRegStore()
125 fc := clock.NewFake()
126
127 staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
128
129 log := blog.NewMock()
130 m := mailer{
131 log: log,
132 mailer: &mc,
133 emailTemplate: tmpl,
134 addressLimiter: &limiter{clk: fc, limit: 4},
135
136 subjectTemplate: staticTmpl,
137 rs: rs,
138 clk: fc,
139 stats: initStats(metrics.NoopRegisterer),
140 }
141
142 cert := &x509.Certificate{
143 SerialNumber: big.NewInt(0x0304),
144 NotAfter: fc.Now().AddDate(0, 0, 2),
145 DNSNames: []string{"example.com"},
146 }
147
148 conn, err := m.mailer.Connect()
149 test.AssertNotError(t, err, "connecting SMTP")
150 err = m.sendNags(conn, []string{emailA}, []*x509.Certificate{cert})
151 test.AssertNotError(t, err, "Failed to send warning messages")
152 test.AssertEquals(t, len(mc.Messages), 1)
153 test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{
154 To: emailARaw,
155 Subject: testEmailSubject,
156 Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
157 })
158
159 mc.Clear()
160 conn, err = m.mailer.Connect()
161 test.AssertNotError(t, err, "connecting SMTP")
162 err = m.sendNags(conn, []string{emailA, emailB}, []*x509.Certificate{cert})
163 test.AssertNotError(t, err, "Failed to send warning messages")
164 test.AssertEquals(t, len(mc.Messages), 2)
165 test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{
166 To: emailARaw,
167 Subject: testEmailSubject,
168 Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
169 })
170 test.AssertEquals(t, mc.Messages[1], mocks.MailerMessage{
171 To: emailBRaw,
172 Subject: testEmailSubject,
173 Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
174 })
175
176 mc.Clear()
177 conn, err = m.mailer.Connect()
178 test.AssertNotError(t, err, "connecting SMTP")
179 err = m.sendNags(conn, []string{}, []*x509.Certificate{cert})
180 test.AssertNotError(t, err, "Not an error to pass no email contacts")
181 test.AssertEquals(t, len(mc.Messages), 0)
182
183 sendLogs := log.GetAllMatching("INFO: attempting send JSON=.*")
184 if len(sendLogs) != 2 {
185 t.Errorf("expected 2 'attempting send' log line, got %d: %s", len(sendLogs), strings.Join(sendLogs, "\n"))
186 }
187 if !strings.Contains(sendLogs[0], `"Rcpt":["rolandshoemaker@gmail.com"]`) {
188 t.Errorf("expected first 'attempting send' log line to have one address, got %q", sendLogs[0])
189 }
190 if !strings.Contains(sendLogs[0], `"TruncatedSerials":["000000000000000000000000000000000304"]`) {
191 t.Errorf("expected first 'attempting send' log line to have one serial, got %q", sendLogs[0])
192 }
193 if !strings.Contains(sendLogs[0], `"DaysToExpiration":2`) {
194 t.Errorf("expected first 'attempting send' log line to have 2 days to expiration, got %q", sendLogs[0])
195 }
196 if !strings.Contains(sendLogs[0], `"TruncatedDNSNames":["example.com"]`) {
197 t.Errorf("expected first 'attempting send' log line to have 1 domain, 'example.com', got %q", sendLogs[0])
198 }
199 }
200
201 func TestSendNagsAddressLimited(t *testing.T) {
202 mc := mocks.Mailer{}
203 rs := newFakeRegStore()
204 fc := clock.NewFake()
205
206 staticTmpl := template.Must(template.New("expiry-email-subject-static").Parse(testEmailSubject))
207
208 log := blog.NewMock()
209 m := mailer{
210 log: log,
211 mailer: &mc,
212 emailTemplate: tmpl,
213 addressLimiter: &limiter{clk: fc, limit: 1},
214
215 subjectTemplate: staticTmpl,
216 rs: rs,
217 clk: fc,
218 stats: initStats(metrics.NoopRegisterer),
219 }
220
221 m.addressLimiter.inc(emailARaw)
222
223 cert := &x509.Certificate{
224 SerialNumber: big.NewInt(0x0304),
225 NotAfter: fc.Now().AddDate(0, 0, 2),
226 DNSNames: []string{"example.com"},
227 }
228
229 conn, err := m.mailer.Connect()
230 test.AssertNotError(t, err, "connecting SMTP")
231
232
233 err = m.sendNags(conn, []string{emailA}, []*x509.Certificate{cert})
234 test.AssertNotError(t, err, "sending warning messages")
235
236 test.AssertEquals(t, len(mc.Messages), 0)
237
238
239
240 err = m.sendNags(conn, []string{emailA, emailB}, []*x509.Certificate{cert})
241 test.AssertNotError(t, err, "sending warning messages to two addresses")
242 test.AssertEquals(t, len(mc.Messages), 1)
243 test.AssertEquals(t, mc.Messages[0], mocks.MailerMessage{
244 To: emailBRaw,
245 Subject: testEmailSubject,
246 Body: fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter.Format(time.DateOnly)),
247 })
248 }
249
250 var serial1 = big.NewInt(0x1336)
251 var serial2 = big.NewInt(0x1337)
252 var serial3 = big.NewInt(0x1338)
253 var serial4 = big.NewInt(0x1339)
254 var serial4String = core.SerialToString(serial4)
255 var serial5 = big.NewInt(0x1340)
256 var serial5String = core.SerialToString(serial5)
257 var serial6 = big.NewInt(0x1341)
258 var serial7 = big.NewInt(0x1342)
259 var serial8 = big.NewInt(0x1343)
260 var serial9 = big.NewInt(0x1344)
261
262 var testKey *ecdsa.PrivateKey
263
264 func init() {
265 var err error
266 testKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
267 if err != nil {
268 panic(err)
269 }
270 }
271
272 func TestProcessCerts(t *testing.T) {
273 expiresIn := time.Hour * 24 * 7
274 testCtx := setup(t, []time.Duration{expiresIn})
275
276 certs := addExpiringCerts(t, testCtx)
277 err := testCtx.m.processCerts(context.Background(), certs, expiresIn)
278 test.AssertNotError(t, err, "processing certs")
279
280
281
282 if len(testCtx.log.GetAllMatching("UPDATE certificateStatus.*000000000000000000000000000000001339")) != 1 {
283 t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s",
284 strings.Join(testCtx.log.GetAll(), "\n"))
285 }
286 }
287
288
289
290 func TestNoContactCertIsNotRenewed(t *testing.T) {
291 expiresIn := time.Hour * 24 * 7
292 testCtx := setup(t, []time.Duration{expiresIn})
293
294 reg, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, nil)
295 test.AssertNotError(t, err, "Couldn't store regA")
296
297 cert, err := makeCertificate(
298 reg.Id,
299 serial1,
300 []string{"example-a.com"},
301 23*time.Hour,
302 testCtx.fc)
303 test.AssertNotError(t, err, "creating cert A")
304
305 err = insertCertificate(cert, time.Time{})
306 test.AssertNotError(t, err, "inserting certificate")
307
308 err = testCtx.m.findExpiringCertificates(context.Background())
309 test.AssertNotError(t, err, "finding expired certificates")
310
311
312 test.AssertEquals(t, len(testCtx.mc.Messages), 0)
313
314
315 certsExamined := testCtx.m.stats.certificatesExamined
316 test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
317
318 certsAlreadyRenewed := testCtx.m.stats.certificatesAlreadyRenewed
319 test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 0.0)
320
321
322
323 err = testCtx.m.findExpiringCertificates(context.Background())
324 test.AssertNotError(t, err, "finding expired certificates")
325 test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
326 test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 0.0)
327 }
328
329
330
331 func TestNoContactCertIsRenewed(t *testing.T) {
332 ctx := context.Background()
333
334 testCtx := setup(t, []time.Duration{time.Hour * 24 * 7})
335
336 reg, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{})
337 test.AssertNotError(t, err, "Couldn't store regA")
338
339 names := []string{"example-a.com"}
340 cert, err := makeCertificate(
341 reg.Id,
342 serial1,
343 names,
344 23*time.Hour,
345 testCtx.fc)
346 test.AssertNotError(t, err, "creating cert A")
347
348 expires := testCtx.fc.Now().Add(23 * time.Hour)
349
350 err = insertCertificate(cert, time.Time{})
351 test.AssertNotError(t, err, "inserting certificate")
352
353 setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
354 test.AssertNotError(t, err, "setting up DB")
355 err = setupDBMap.Insert(ctx, &core.FQDNSet{
356 SetHash: core.HashNames(names),
357 Serial: core.SerialToString(serial2),
358 Issued: testCtx.fc.Now().Add(time.Hour),
359 Expires: expires.Add(time.Hour),
360 })
361 test.AssertNotError(t, err, "inserting FQDNSet for renewal")
362
363 err = testCtx.m.findExpiringCertificates(ctx)
364 test.AssertNotError(t, err, "finding expired certificates")
365
366
367 certsExamined := testCtx.m.stats.certificatesExamined
368 test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
369
370 certsAlreadyRenewed := testCtx.m.stats.certificatesAlreadyRenewed
371 test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 1.0)
372
373
374
375 err = testCtx.m.findExpiringCertificates(ctx)
376 test.AssertNotError(t, err, "finding expired certificates")
377 test.AssertMetricWithLabelsEquals(t, certsExamined, prometheus.Labels{}, 1.0)
378 test.AssertMetricWithLabelsEquals(t, certsAlreadyRenewed, prometheus.Labels{}, 1.0)
379 }
380
381 func TestProcessCertsParallel(t *testing.T) {
382 expiresIn := time.Hour * 24 * 7
383 testCtx := setup(t, []time.Duration{expiresIn})
384
385 testCtx.m.parallelSends = 2
386 certs := addExpiringCerts(t, testCtx)
387 err := testCtx.m.processCerts(context.Background(), certs, expiresIn)
388 test.AssertNotError(t, err, "processing certs")
389
390
391
392 if len(testCtx.log.GetAllMatching("UPDATE certificateStatus.*000000000000000000000000000000001339")) != 1 {
393 t.Errorf("Expected an update to certificateStatus, got these log lines:\n%s",
394 strings.Join(testCtx.log.GetAll(), "\n"))
395 }
396 }
397
398 type erroringMailClient struct{}
399
400 func (e erroringMailClient) Connect() (bmail.Conn, error) {
401 return nil, errors.New("whoopsie-doo")
402 }
403
404 func TestProcessCertsConnectError(t *testing.T) {
405 expiresIn := time.Hour * 24 * 7
406 testCtx := setup(t, []time.Duration{expiresIn})
407
408 testCtx.m.mailer = erroringMailClient{}
409 certs := addExpiringCerts(t, testCtx)
410
411 err := testCtx.m.processCerts(context.Background(), certs, expiresIn)
412 test.AssertError(t, err, "processing certs")
413 }
414
415 func TestFindExpiringCertificates(t *testing.T) {
416 testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7})
417
418 addExpiringCerts(t, testCtx)
419
420 err := testCtx.m.findExpiringCertificates(context.Background())
421 test.AssertNotError(t, err, "Failed on no certificates")
422 test.AssertEquals(t, len(testCtx.log.GetAllMatching("Searching for certificates that expire between.*")), 3)
423
424 err = testCtx.m.findExpiringCertificates(context.Background())
425 test.AssertNotError(t, err, "Failed to find expiring certs")
426
427 if len(testCtx.mc.Messages) != 2 {
428 builder := new(strings.Builder)
429 for _, m := range testCtx.mc.Messages {
430 fmt.Fprintf(builder, "%s\n", m)
431 }
432 t.Fatalf("Expected two messages when finding expiring certificates, got:\n%s",
433 builder.String())
434 }
435
436 test.AssertEquals(t, testCtx.mc.Messages[0], mocks.MailerMessage{
437 To: emailARaw,
438
439
440 Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\"",
441 Body: "hi, cert for DNS names example-a.com is going to expire in 0 days (1970-01-01)",
442 })
443 test.AssertEquals(t, testCtx.mc.Messages[1], mocks.MailerMessage{
444 To: emailBRaw,
445
446
447 Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"another.example-c.com\" (and 1 more)",
448 Body: "hi, cert for DNS names another.example-c.com\nexample-c.com is going to expire in 7 days (1970-01-08)",
449 })
450
451
452 test.AssertEquals(t, len(testCtx.log.GetAllMatching("no certs given to send nags for")), 0)
453
454
455 testCtx.mc.Clear()
456 err = testCtx.m.findExpiringCertificates(context.Background())
457 test.AssertNotError(t, err, "Failed to find expiring certs")
458 test.AssertEquals(t, len(testCtx.mc.Messages), 0)
459 test.AssertMetricWithLabelsEquals(t, testCtx.m.stats.sendDelay, prometheus.Labels{"nag_group": "48h0m0s"}, 90000)
460 test.AssertMetricWithLabelsEquals(t, testCtx.m.stats.sendDelay, prometheus.Labels{"nag_group": "192h0m0s"}, 82800)
461 }
462
463 func makeRegistration(sac sapb.StorageAuthorityClient, id int64, jsonKey []byte, contacts []string) (*corepb.Registration, error) {
464 var ip [4]byte
465 _, err := rand.Reader.Read(ip[:])
466 if err != nil {
467 return nil, err
468 }
469 ipText, err := net.IP(ip[:]).MarshalText()
470 if err != nil {
471 return nil, fmt.Errorf("formatting IP address: %s", err)
472 }
473 reg, err := sac.NewRegistration(context.Background(), &corepb.Registration{
474 Id: id,
475 Contact: contacts,
476 Key: jsonKey,
477 InitialIP: ipText,
478 })
479 if err != nil {
480 return nil, fmt.Errorf("storing registration: %s", err)
481 }
482 return reg, nil
483 }
484
485 func makeCertificate(regID int64, serial *big.Int, dnsNames []string, expires time.Duration, fc clock.FakeClock) (certDERWithRegID, error) {
486
487 template := &x509.Certificate{
488 NotAfter: fc.Now().Add(expires),
489 DNSNames: dnsNames,
490 SerialNumber: serial,
491 }
492 certDer, err := x509.CreateCertificate(rand.Reader, template, template, &testKey.PublicKey, testKey)
493 if err != nil {
494 return certDERWithRegID{}, err
495 }
496 return certDERWithRegID{
497 RegID: regID,
498 DER: certDer,
499 }, nil
500 }
501
502 func insertCertificate(cert certDERWithRegID, lastNagSent time.Time) error {
503 ctx := context.Background()
504
505 parsedCert, err := x509.ParseCertificate(cert.DER)
506 if err != nil {
507 return err
508 }
509
510 setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
511 if err != nil {
512 return err
513 }
514 err = setupDBMap.Insert(ctx, &core.Certificate{
515 RegistrationID: cert.RegID,
516 Serial: core.SerialToString(parsedCert.SerialNumber),
517 Issued: parsedCert.NotBefore,
518 Expires: parsedCert.NotAfter,
519 DER: cert.DER,
520 })
521 if err != nil {
522 return fmt.Errorf("inserting certificate: %w", err)
523 }
524
525 return setupDBMap.Insert(ctx, &core.CertificateStatus{
526 Serial: core.SerialToString(parsedCert.SerialNumber),
527 LastExpirationNagSent: lastNagSent,
528 Status: core.OCSPStatusGood,
529 NotAfter: parsedCert.NotAfter,
530 OCSPLastUpdated: time.Time{},
531 RevokedDate: time.Time{},
532 RevokedReason: 0,
533 })
534 }
535
536 func addExpiringCerts(t *testing.T, ctx *testCtx) []certDERWithRegID {
537
538 regA, err := makeRegistration(ctx.ssa, 1, jsonKeyA, []string{emailA})
539 test.AssertNotError(t, err, "Couldn't store regA")
540 regB, err := makeRegistration(ctx.ssa, 2, jsonKeyB, []string{emailB})
541 test.AssertNotError(t, err, "Couldn't store regB")
542 regC, err := makeRegistration(ctx.ssa, 3, jsonKeyC, []string{emailB})
543 test.AssertNotError(t, err, "Couldn't store regC")
544
545
546 certA, err := makeCertificate(
547 regA.Id,
548 serial1,
549 []string{"example-a.com"},
550 23*time.Hour,
551 ctx.fc)
552 test.AssertNotError(t, err, "creating cert A")
553
554
555 certB, err := makeCertificate(
556 regA.Id,
557 serial2,
558 []string{"example-b.com"},
559 72*time.Hour,
560 ctx.fc)
561 test.AssertNotError(t, err, "creating cert B")
562
563
564 certC, err := makeCertificate(
565 regB.Id,
566 serial3,
567 []string{"example-c.com", "another.example-c.com"},
568 (7*24+1)*time.Hour,
569 ctx.fc)
570 test.AssertNotError(t, err, "creating cert C")
571
572
573 certDNames := []string{"example-d.com"}
574 certD, err := makeCertificate(
575 regC.Id,
576 serial4,
577 certDNames,
578 72*time.Hour,
579 ctx.fc)
580 test.AssertNotError(t, err, "creating cert D")
581
582 fqdnStatusD := &core.FQDNSet{
583 SetHash: core.HashNames(certDNames),
584 Serial: serial4String,
585 Issued: ctx.fc.Now().AddDate(0, 0, -87),
586 Expires: ctx.fc.Now().AddDate(0, 0, 3),
587 }
588 fqdnStatusDRenewed := &core.FQDNSet{
589 SetHash: core.HashNames(certDNames),
590 Serial: serial5String,
591 Issued: ctx.fc.Now().AddDate(0, 0, -3),
592 Expires: ctx.fc.Now().AddDate(0, 0, 87),
593 }
594
595 err = insertCertificate(certA, ctx.fc.Now().Add(-72*time.Hour))
596 test.AssertNotError(t, err, "inserting certA")
597 err = insertCertificate(certB, ctx.fc.Now().Add(-36*time.Hour))
598 test.AssertNotError(t, err, "inserting certB")
599 err = insertCertificate(certC, ctx.fc.Now().Add(-36*time.Hour))
600 test.AssertNotError(t, err, "inserting certC")
601 err = insertCertificate(certD, ctx.fc.Now().Add(-36*time.Hour))
602 test.AssertNotError(t, err, "inserting certD")
603
604 setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
605 test.AssertNotError(t, err, "setting up DB")
606 err = setupDBMap.Insert(context.Background(), fqdnStatusD)
607 test.AssertNotError(t, err, "Couldn't add fqdnStatusD")
608 err = setupDBMap.Insert(context.Background(), fqdnStatusDRenewed)
609 test.AssertNotError(t, err, "Couldn't add fqdnStatusDRenewed")
610 return []certDERWithRegID{certA, certB, certC, certD}
611 }
612
613 func countGroupsAtCapacity(group string, counter *prometheus.GaugeVec) int {
614 ch := make(chan prometheus.Metric, 10)
615 counter.With(prometheus.Labels{"nag_group": group}).Collect(ch)
616 m := <-ch
617 var iom io_prometheus_client.Metric
618 _ = m.Write(&iom)
619 return int(iom.Gauge.GetValue())
620 }
621
622 func TestFindCertsAtCapacity(t *testing.T) {
623 testCtx := setup(t, []time.Duration{time.Hour * 24})
624
625 addExpiringCerts(t, testCtx)
626
627
628 testCtx.m.certificatesPerTick = 1
629
630 err := testCtx.m.findExpiringCertificates(context.Background())
631 test.AssertNotError(t, err, "Failed to find expiring certs")
632 test.AssertEquals(t, len(testCtx.mc.Messages), 1)
633
634
635
636
637
638
639 test.AssertEquals(t, countGroupsAtCapacity("48h0m0s", testCtx.m.stats.nagsAtCapacity), 1)
640
641
642 testCtx.mc.Clear()
643 err = testCtx.m.findExpiringCertificates(context.Background())
644 test.AssertNotError(t, err, "Failed to find expiring certs")
645 test.AssertEquals(t, len(testCtx.mc.Messages), 0)
646
647
648 test.AssertEquals(t, countGroupsAtCapacity("48h0m0s", testCtx.m.stats.nagsAtCapacity), 0)
649 }
650
651 func TestCertIsRenewed(t *testing.T) {
652 testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7})
653
654 reg := satest.CreateWorkingRegistration(t, testCtx.ssa)
655
656 testCerts := []*struct {
657 Serial *big.Int
658 stringSerial string
659 DNS []string
660 NotBefore time.Time
661 NotAfter time.Time
662
663 IsRenewed bool
664 }{
665 {
666 Serial: serial1,
667 DNS: []string{"a.example.com", "a2.example.com"},
668 NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour),
669 NotAfter: testCtx.fc.Now().Add((89 * 24) * time.Hour),
670 IsRenewed: true,
671 },
672 {
673 Serial: serial2,
674 DNS: []string{"a.example.com", "a2.example.com"},
675 NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour),
676 NotAfter: testCtx.fc.Now().Add((90 * 24) * time.Hour),
677 IsRenewed: false,
678 },
679 {
680 Serial: serial3,
681 DNS: []string{"b.example.net"},
682 NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour),
683 NotAfter: testCtx.fc.Now().Add((90 * 24) * time.Hour),
684 IsRenewed: false,
685 },
686 {
687 Serial: serial4,
688 DNS: []string{"c.example.org"},
689 NotBefore: testCtx.fc.Now().Add((-100 * 24) * time.Hour),
690 NotAfter: testCtx.fc.Now().Add((-10 * 24) * time.Hour),
691 IsRenewed: true,
692 },
693 {
694 Serial: serial5,
695 DNS: []string{"c.example.org"},
696 NotBefore: testCtx.fc.Now().Add((-80 * 24) * time.Hour),
697 NotAfter: testCtx.fc.Now().Add((10 * 24) * time.Hour),
698 IsRenewed: true,
699 },
700 {
701 Serial: serial6,
702 DNS: []string{"c.example.org"},
703 NotBefore: testCtx.fc.Now().Add((-75 * 24) * time.Hour),
704 NotAfter: testCtx.fc.Now().Add((15 * 24) * time.Hour),
705 IsRenewed: true,
706 },
707 {
708 Serial: serial7,
709 DNS: []string{"c.example.org"},
710 NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour),
711 NotAfter: testCtx.fc.Now().Add((89 * 24) * time.Hour),
712 IsRenewed: false,
713 },
714 {
715 Serial: serial8,
716 DNS: []string{"d.example.com", "d2.example.com"},
717 NotBefore: testCtx.fc.Now().Add((-1 * 24) * time.Hour),
718 NotAfter: testCtx.fc.Now().Add((89 * 24) * time.Hour),
719 IsRenewed: false,
720 },
721 {
722 Serial: serial9,
723 DNS: []string{"d.example.com", "d2.example.com", "d3.example.com"},
724 NotBefore: testCtx.fc.Now().Add((0 * 24) * time.Hour),
725 NotAfter: testCtx.fc.Now().Add((90 * 24) * time.Hour),
726 IsRenewed: false,
727 },
728 }
729
730 setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
731 if err != nil {
732 t.Fatal(err)
733 }
734
735 for _, testData := range testCerts {
736 testData.stringSerial = core.SerialToString(testData.Serial)
737
738 rawCert := x509.Certificate{
739 NotBefore: testData.NotBefore,
740 NotAfter: testData.NotAfter,
741 DNSNames: testData.DNS,
742 SerialNumber: testData.Serial,
743 }
744
745 certDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey)
746 if err != nil {
747 t.Fatal(err)
748 }
749 fqdnStatus := &core.FQDNSet{
750 SetHash: core.HashNames(testData.DNS),
751 Serial: testData.stringSerial,
752 Issued: testData.NotBefore,
753 Expires: testData.NotAfter,
754 }
755
756 err = insertCertificate(certDERWithRegID{DER: certDer, RegID: reg.Id}, time.Time{})
757 test.AssertNotError(t, err, fmt.Sprintf("Couldn't add cert %s", testData.stringSerial))
758
759 err = setupDBMap.Insert(context.Background(), fqdnStatus)
760 test.AssertNotError(t, err, fmt.Sprintf("Couldn't add fqdnStatus %s", testData.stringSerial))
761 }
762
763 for _, testData := range testCerts {
764 renewed, err := testCtx.m.certIsRenewed(context.Background(), testData.DNS, testData.NotBefore)
765 if err != nil {
766 t.Errorf("error checking renewal state for %s: %v", testData.stringSerial, err)
767 continue
768 }
769 if renewed != testData.IsRenewed {
770 t.Errorf("for %s: got %v, expected %v", testData.stringSerial, renewed, testData.IsRenewed)
771 }
772 }
773 }
774
775 func TestLifetimeOfACert(t *testing.T) {
776 testCtx := setup(t, []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7})
777 defer testCtx.cleanUp()
778
779 regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{emailA})
780 test.AssertNotError(t, err, "Couldn't store regA")
781
782 certA, err := makeCertificate(
783 regA.Id,
784 serial1,
785 []string{"example-a.com"},
786 0,
787 testCtx.fc)
788 test.AssertNotError(t, err, "making certificate")
789
790 err = insertCertificate(certA, time.Time{})
791 test.AssertNotError(t, err, "unable to insert Certificate")
792
793 type lifeTest struct {
794 timeLeft time.Duration
795 numMsgs int
796 context string
797 }
798 tests := []lifeTest{
799 {
800 timeLeft: 9 * 24 * time.Hour,
801
802 numMsgs: 0,
803 context: "Expected no emails sent because we are more than 7 days out.",
804 },
805 {
806 (7*24 + 12) * time.Hour,
807 1,
808 "Sent 1 for 7 day notice.",
809 },
810 {
811 7 * 24 * time.Hour,
812 1,
813 "The 7 day email was already sent.",
814 },
815 {
816 (4*24 - 1) * time.Hour,
817 2,
818 "Sent 1 for the 7 day notice, and 1 for the 4 day notice.",
819 },
820 {
821 36 * time.Hour,
822 3,
823 "Sent 1 for the 7 day notice, 1 for the 4 day notice, and 1 for the 1 day notice.",
824 },
825 {
826 12 * time.Hour,
827 3,
828 "The 1 day before email was already sent.",
829 },
830 {
831 -2 * 24 * time.Hour,
832 3,
833 "No expiration warning emails are sent after expiration",
834 },
835 }
836
837 for _, tt := range tests {
838 testCtx.fc.Add(-tt.timeLeft)
839 err = testCtx.m.findExpiringCertificates(context.Background())
840 test.AssertNotError(t, err, "error calling findExpiringCertificates")
841 if len(testCtx.mc.Messages) != tt.numMsgs {
842 t.Errorf(tt.context+" number of messages: expected %d, got %d", tt.numMsgs, len(testCtx.mc.Messages))
843 }
844 testCtx.fc.Add(tt.timeLeft)
845 }
846 }
847
848 func TestDontFindRevokedCert(t *testing.T) {
849 expiresIn := 24 * time.Hour
850 testCtx := setup(t, []time.Duration{expiresIn})
851
852 regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{"mailto:one@mail.com"})
853 test.AssertNotError(t, err, "Couldn't store regA")
854 certA, err := makeCertificate(
855 regA.Id,
856 serial1,
857 []string{"example-a.com"},
858 expiresIn,
859 testCtx.fc)
860 test.AssertNotError(t, err, "making certificate")
861
862 err = insertCertificate(certA, time.Time{})
863 test.AssertNotError(t, err, "inserting certificate")
864
865 ctx := context.Background()
866
867 setupDBMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms)
868 test.AssertNotError(t, err, "sa.NewDbMap failed")
869 _, err = setupDBMap.ExecContext(ctx, "UPDATE certificateStatus SET status = ? WHERE serial = ?",
870 string(core.OCSPStatusRevoked), core.SerialToString(serial1))
871 test.AssertNotError(t, err, "revoking certificate")
872
873 err = testCtx.m.findExpiringCertificates(ctx)
874 test.AssertNotError(t, err, "err from findExpiringCertificates")
875
876 if len(testCtx.mc.Messages) != 0 {
877 t.Errorf("no emails should have been sent, but sent %d", len(testCtx.mc.Messages))
878 }
879 }
880
881 func TestDedupOnRegistration(t *testing.T) {
882 expiresIn := 96 * time.Hour
883 testCtx := setup(t, []time.Duration{expiresIn})
884
885 regA, err := makeRegistration(testCtx.ssa, 1, jsonKeyA, []string{emailA})
886 test.AssertNotError(t, err, "Couldn't store regA")
887 certA, err := makeCertificate(
888 regA.Id,
889 serial1,
890 []string{"example-a.com", "shared-example.com"},
891 72*time.Hour,
892 testCtx.fc)
893 test.AssertNotError(t, err, "making certificate")
894 err = insertCertificate(certA, time.Time{})
895 test.AssertNotError(t, err, "inserting certificate")
896
897 certB, err := makeCertificate(
898 regA.Id,
899 serial2,
900 []string{"example-b.com", "shared-example.com"},
901 48*time.Hour,
902 testCtx.fc)
903 test.AssertNotError(t, err, "making certificate")
904 err = insertCertificate(certB, time.Time{})
905 test.AssertNotError(t, err, "inserting certificate")
906
907 expires := testCtx.fc.Now().Add(48 * time.Hour)
908
909 err = testCtx.m.findExpiringCertificates(context.Background())
910 test.AssertNotError(t, err, "error calling findExpiringCertificates")
911 if len(testCtx.mc.Messages) > 1 {
912 t.Errorf("num of messages, want %d, got %d", 1, len(testCtx.mc.Messages))
913 }
914 if len(testCtx.mc.Messages) == 0 {
915 t.Fatalf("no messages sent")
916 }
917 domains := "example-a.com\nexample-b.com\nshared-example.com"
918 test.AssertEquals(t, testCtx.mc.Messages[0], mocks.MailerMessage{
919 To: emailARaw,
920
921
922 Subject: "Testing: Let's Encrypt certificate expiration notice for domain \"example-a.com\" (and 2 more)",
923 Body: fmt.Sprintf(`hi, cert for DNS names %s is going to expire in 2 days (%s)`,
924 domains,
925 expires.Format(time.DateOnly)),
926 })
927 }
928
929 type testCtx struct {
930 dbMap *db.WrappedMap
931 ssa sapb.StorageAuthorityClient
932 mc *mocks.Mailer
933 fc clock.FakeClock
934 m *mailer
935 log *blog.Mock
936 cleanUp func()
937 }
938
939 func setup(t *testing.T, nagTimes []time.Duration) *testCtx {
940 log := blog.NewMock()
941
942
943
944 dbMap, err := sa.DBMapForTestWithLog(vars.DBConnSAFullPerms, log)
945 if err != nil {
946 t.Fatalf("Couldn't connect the database: %s", err)
947 }
948
949 fc := clock.NewFake()
950 ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, fc, log, metrics.NoopRegisterer)
951 if err != nil {
952 t.Fatalf("unable to create SQLStorageAuthority: %s", err)
953 }
954 cleanUp := test.ResetBoulderTestDatabase(t)
955
956 mc := &mocks.Mailer{}
957
958 offsetNags := make([]time.Duration, len(nagTimes))
959 for i, t := range nagTimes {
960 offsetNags[i] = t + 24*time.Hour
961 }
962
963 m := &mailer{
964 log: log,
965 mailer: mc,
966 emailTemplate: tmpl,
967 subjectTemplate: subjTmpl,
968 dbMap: dbMap,
969 rs: isa.SA{Impl: ssa},
970 nagTimes: offsetNags,
971 addressLimiter: &limiter{clk: fc, limit: 4},
972 certificatesPerTick: 100,
973 clk: fc,
974 stats: initStats(metrics.NoopRegisterer),
975 }
976 return &testCtx{
977 dbMap: dbMap,
978 ssa: isa.SA{Impl: ssa},
979 mc: mc,
980 fc: fc,
981 m: m,
982 log: log,
983 cleanUp: cleanUp,
984 }
985 }
986
987 func TestLimiter(t *testing.T) {
988 clk := clock.NewFake()
989 lim := &limiter{clk: clk, limit: 4}
990 fooAtExample := "foo@example.com"
991 lim.inc(fooAtExample)
992 test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
993 lim.inc(fooAtExample)
994 test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
995 lim.inc(fooAtExample)
996 test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
997 lim.inc(fooAtExample)
998 test.AssertError(t, lim.check(fooAtExample), "expected an error")
999
1000 clk.Sleep(time.Hour)
1001 test.AssertError(t, lim.check(fooAtExample), "expected an error")
1002
1003
1004 clk.Sleep(24 * time.Hour)
1005 test.AssertNotError(t, lim.check(fooAtExample), "expected no error")
1006 }
1007
View as plain text