package notmain import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "fmt" "math/big" "os" "testing" "time" "github.com/jmhodges/clock" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" blog "github.com/letsencrypt/boulder/log" "github.com/letsencrypt/boulder/metrics" "github.com/letsencrypt/boulder/sa" sapb "github.com/letsencrypt/boulder/sa/proto" "github.com/letsencrypt/boulder/test" isa "github.com/letsencrypt/boulder/test/inmem/sa" "github.com/letsencrypt/boulder/test/vars" ) var ( regA *corepb.Registration regB *corepb.Registration regC *corepb.Registration regD *corepb.Registration ) const ( emailARaw = "test@example.com" emailBRaw = "example@example.com" emailCRaw = "test-example@example.com" telNum = "666-666-7777" ) func TestFindIDs(t *testing.T) { ctx := context.Background() testCtx := setup(t) defer testCtx.cleanUp() // Add some test registrations testCtx.addRegistrations(t) // Run findIDs - since no certificates have been added corresponding to // the above registrations, no IDs should be found. results, err := testCtx.c.findIDs(ctx) test.AssertNotError(t, err, "findIDs() produced error") test.AssertEquals(t, len(results), 0) // Now add some certificates testCtx.addCertificates(t) // Run findIDs - since there are three registrations with unexpired certs // we should get exactly three IDs back: RegA, RegC and RegD. RegB should // *not* be present since their certificate has already expired. Unlike // previous versions of this test RegD is not filtered out for having a `tel:` // contact field anymore - this is the duty of the notify-mailer. results, err = testCtx.c.findIDs(ctx) test.AssertNotError(t, err, "findIDs() produced error") test.AssertEquals(t, len(results), 3) for _, entry := range results { switch entry.ID { case regA.Id: case regC.Id: case regD.Id: default: t.Errorf("ID: %d not expected", entry.ID) } } // Allow a 1 year grace period testCtx.c.grace = 360 * 24 * time.Hour results, err = testCtx.c.findIDs(ctx) test.AssertNotError(t, err, "findIDs() produced error") // Now all four registration should be returned, including RegB since its // certificate expired within the grace period for _, entry := range results { switch entry.ID { case regA.Id: case regB.Id: case regC.Id: case regD.Id: default: t.Errorf("ID: %d not expected", entry.ID) } } } func TestFindIDsWithExampleHostnames(t *testing.T) { ctx := context.Background() testCtx := setup(t) defer testCtx.cleanUp() // Add some test registrations testCtx.addRegistrations(t) // Run findIDsWithExampleHostnames - since no certificates have been // added corresponding to the above registrations, no IDs should be // found. results, err := testCtx.c.findIDsWithExampleHostnames(ctx) test.AssertNotError(t, err, "findIDs() produced error") test.AssertEquals(t, len(results), 0) // Now add some certificates testCtx.addCertificates(t) // Run findIDsWithExampleHostnames - since there are three // registrations with unexpired certs we should get exactly three // IDs back: RegA, RegC and RegD. RegB should *not* be present since // their certificate has already expired. results, err = testCtx.c.findIDsWithExampleHostnames(ctx) test.AssertNotError(t, err, "findIDs() produced error") test.AssertEquals(t, len(results), 3) for _, entry := range results { switch entry.ID { case regA.Id: test.AssertEquals(t, entry.Hostname, "example-a.com") case regC.Id: test.AssertEquals(t, entry.Hostname, "example-c.com") case regD.Id: test.AssertEquals(t, entry.Hostname, "example-d.com") default: t.Errorf("ID: %d not expected", entry.ID) } } // Allow a 1 year grace period testCtx.c.grace = 360 * 24 * time.Hour results, err = testCtx.c.findIDsWithExampleHostnames(ctx) test.AssertNotError(t, err, "findIDs() produced error") // Now all four registrations should be returned, including RegB // since it expired within the grace period test.AssertEquals(t, len(results), 4) for _, entry := range results { switch entry.ID { case regA.Id: test.AssertEquals(t, entry.Hostname, "example-a.com") case regB.Id: test.AssertEquals(t, entry.Hostname, "example-b.com") case regC.Id: test.AssertEquals(t, entry.Hostname, "example-c.com") case regD.Id: test.AssertEquals(t, entry.Hostname, "example-d.com") default: t.Errorf("ID: %d not expected", entry.ID) } } } func TestFindIDsForHostnames(t *testing.T) { ctx := context.Background() testCtx := setup(t) defer testCtx.cleanUp() // Add some test registrations testCtx.addRegistrations(t) // Run findIDsForHostnames - since no certificates have been added corresponding to // the above registrations, no IDs should be found. results, err := testCtx.c.findIDsForHostnames(ctx, []string{"example-a.com", "example-b.com", "example-c.com", "example-d.com"}) test.AssertNotError(t, err, "findIDs() produced error") test.AssertEquals(t, len(results), 0) // Now add some certificates testCtx.addCertificates(t) results, err = testCtx.c.findIDsForHostnames(ctx, []string{"example-a.com", "example-b.com", "example-c.com", "example-d.com"}) test.AssertNotError(t, err, "findIDsForHostnames() failed") test.AssertEquals(t, len(results), 3) for _, entry := range results { switch entry.ID { case regA.Id: case regC.Id: case regD.Id: default: t.Errorf("ID: %d not expected", entry.ID) } } } func TestWriteToFile(t *testing.T) { expected := `[{"id":1},{"id":2},{"id":3}]` mockResults := idExporterResults{{ID: 1}, {ID: 2}, {ID: 3}} dir := os.TempDir() f, err := os.CreateTemp(dir, "ids_test") test.AssertNotError(t, err, "os.CreateTemp produced an error") // Writing the result to an outFile should produce the correct results err = mockResults.writeToFile(f.Name()) test.AssertNotError(t, err, fmt.Sprintf("writeIDs produced an error writing to %s", f.Name())) contents, err := os.ReadFile(f.Name()) test.AssertNotError(t, err, fmt.Sprintf("os.ReadFile produced an error reading from %s", f.Name())) test.AssertEquals(t, string(contents), expected+"\n") } func Test_unmarshalHostnames(t *testing.T) { testDir := os.TempDir() testFile, err := os.CreateTemp(testDir, "ids_test") test.AssertNotError(t, err, "os.CreateTemp produced an error") // Non-existent hostnamesFile _, err = unmarshalHostnames("file_does_not_exist") test.AssertError(t, err, "expected error for non-existent file") // Empty hostnamesFile err = os.WriteFile(testFile.Name(), []byte(""), 0644) test.AssertNotError(t, err, "os.WriteFile produced an error") _, err = unmarshalHostnames(testFile.Name()) test.AssertError(t, err, "expected error for file containing 0 entries") // One hostname present in the hostnamesFile err = os.WriteFile(testFile.Name(), []byte("example-a.com"), 0644) test.AssertNotError(t, err, "os.WriteFile produced an error") results, err := unmarshalHostnames(testFile.Name()) test.AssertNotError(t, err, "error when unmarshalling hostnamesFile with a single hostname") test.AssertEquals(t, len(results), 1) // Two hostnames present in the hostnamesFile err = os.WriteFile(testFile.Name(), []byte("example-a.com\nexample-b.com"), 0644) test.AssertNotError(t, err, "os.WriteFile produced an error") results, err = unmarshalHostnames(testFile.Name()) test.AssertNotError(t, err, "error when unmarshalling hostnamesFile with a two hostnames") test.AssertEquals(t, len(results), 2) // Three hostnames present in the hostnamesFile but two are separated only by a space err = os.WriteFile(testFile.Name(), []byte("example-a.com\nexample-b.com example-c.com"), 0644) test.AssertNotError(t, err, "os.WriteFile produced an error") _, err = unmarshalHostnames(testFile.Name()) test.AssertError(t, err, "error when unmarshalling hostnamesFile with three space separated domains") } type testCtx struct { c idExporter ssa sapb.StorageAuthorityClient cleanUp func() } func (tc testCtx) addRegistrations(t *testing.T) { emailA := "mailto:" + emailARaw emailB := "mailto:" + emailBRaw emailC := "mailto:" + emailCRaw tel := "tel:" + telNum // Every registration needs a unique JOSE key jsonKeyA := []byte(`{ "kty":"RSA", "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", "e":"AQAB" }`) jsonKeyB := []byte(`{ "kty":"RSA", "n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w", "e":"AAEAAQ" }`) jsonKeyC := []byte(`{ "kty":"RSA", "n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-sCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ", "e":"AQAB" }`) jsonKeyD := []byte(`{ "kty":"RSA", "n":"rFH5kUBZrlPj73epjJjyCxzVzZuV--JjKgapoqm9pOuOt20BUTdHqVfC2oDclqM7HFhkkX9OSJMTHgZ7WaVqZv9u1X2yjdx9oVmMLuspX7EytW_ZKDZSzL-FCOFCuQAuYKkLbsdcA3eHBK_lwc4zwdeHFMKIulNvLqckkqYB9s8GpgNXBDIQ8GjR5HuJke_WUNjYHSd8jY1LU9swKWsLQe2YoQUz_ekQvBvBCoaFEtrtRaSJKNLIVDObXFr2TLIiFiM0Em90kK01-eQ7ZiruZTKomll64bRFPoNo4_uwubddg3xTqur2vdF3NyhTrYdvAgTem4uC0PFjEQ1bK_djBQ", "e":"AQAB" }`) // Regs A through C have `mailto:` contact ACME URL's regA = &corepb.Registration{ Id: 1, Contact: []string{emailA}, Key: jsonKeyA, } regB = &corepb.Registration{ Id: 2, Contact: []string{emailB}, Key: jsonKeyB, } regC = &corepb.Registration{ Id: 3, Contact: []string{emailC}, Key: jsonKeyC, } // Reg D has a `tel:` contact ACME URL regD = &corepb.Registration{ Id: 4, Contact: []string{tel}, Key: jsonKeyD, } // Add the four test registrations ctx := context.Background() var err error regA, err = tc.ssa.NewRegistration(ctx, regA) test.AssertNotError(t, err, "Couldn't store regA") regB, err = tc.ssa.NewRegistration(ctx, regB) test.AssertNotError(t, err, "Couldn't store regB") regC, err = tc.ssa.NewRegistration(ctx, regC) test.AssertNotError(t, err, "Couldn't store regC") regD, err = tc.ssa.NewRegistration(ctx, regD) test.AssertNotError(t, err, "Couldn't store regD") } func (tc testCtx) addCertificates(t *testing.T) { ctx := context.Background() serial1 := big.NewInt(1336) serial1String := core.SerialToString(serial1) serial2 := big.NewInt(1337) serial2String := core.SerialToString(serial2) serial3 := big.NewInt(1338) serial3String := core.SerialToString(serial3) serial4 := big.NewInt(1339) serial4String := core.SerialToString(serial4) key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) test.AssertNotError(t, err, "creating test key") fc := clock.NewFake() // Add one cert for RegA that expires in 30 days rawCertA := x509.Certificate{ Subject: pkix.Name{ CommonName: "happy A", }, NotAfter: fc.Now().Add(30 * 24 * time.Hour), DNSNames: []string{"example-a.com"}, SerialNumber: serial1, } certDerA, _ := x509.CreateCertificate(rand.Reader, &rawCertA, &rawCertA, key.Public(), key) certA := &core.Certificate{ RegistrationID: regA.Id, Serial: serial1String, Expires: rawCertA.NotAfter, DER: certDerA, } err = tc.c.dbMap.Insert(ctx, certA) test.AssertNotError(t, err, "Couldn't add certA") _, err = tc.c.dbMap.ExecContext( ctx, "INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)", "com.example-a", serial1String, ) test.AssertNotError(t, err, "Couldn't add issued name for certA") // Add one cert for RegB that already expired 30 days ago rawCertB := x509.Certificate{ Subject: pkix.Name{ CommonName: "happy B", }, NotAfter: fc.Now().Add(-30 * 24 * time.Hour), DNSNames: []string{"example-b.com"}, SerialNumber: serial2, } certDerB, _ := x509.CreateCertificate(rand.Reader, &rawCertB, &rawCertB, key.Public(), key) certB := &core.Certificate{ RegistrationID: regB.Id, Serial: serial2String, Expires: rawCertB.NotAfter, DER: certDerB, } err = tc.c.dbMap.Insert(ctx, certB) test.AssertNotError(t, err, "Couldn't add certB") _, err = tc.c.dbMap.ExecContext( ctx, "INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)", "com.example-b", serial2String, ) test.AssertNotError(t, err, "Couldn't add issued name for certB") // Add one cert for RegC that expires in 30 days rawCertC := x509.Certificate{ Subject: pkix.Name{ CommonName: "happy C", }, NotAfter: fc.Now().Add(30 * 24 * time.Hour), DNSNames: []string{"example-c.com"}, SerialNumber: serial3, } certDerC, _ := x509.CreateCertificate(rand.Reader, &rawCertC, &rawCertC, key.Public(), key) certC := &core.Certificate{ RegistrationID: regC.Id, Serial: serial3String, Expires: rawCertC.NotAfter, DER: certDerC, } err = tc.c.dbMap.Insert(ctx, certC) test.AssertNotError(t, err, "Couldn't add certC") _, err = tc.c.dbMap.ExecContext( ctx, "INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)", "com.example-c", serial3String, ) test.AssertNotError(t, err, "Couldn't add issued name for certC") // Add one cert for RegD that expires in 30 days rawCertD := x509.Certificate{ Subject: pkix.Name{ CommonName: "happy D", }, NotAfter: fc.Now().Add(30 * 24 * time.Hour), DNSNames: []string{"example-d.com"}, SerialNumber: serial4, } certDerD, _ := x509.CreateCertificate(rand.Reader, &rawCertD, &rawCertD, key.Public(), key) certD := &core.Certificate{ RegistrationID: regD.Id, Serial: serial4String, Expires: rawCertD.NotAfter, DER: certDerD, } err = tc.c.dbMap.Insert(ctx, certD) test.AssertNotError(t, err, "Couldn't add certD") _, err = tc.c.dbMap.ExecContext( ctx, "INSERT INTO issuedNames (reversedName, serial, notBefore) VALUES (?,?,0)", "com.example-d", serial4String, ) test.AssertNotError(t, err, "Couldn't add issued name for certD") } func setup(t *testing.T) testCtx { log := blog.UseMock() fc := clock.NewFake() // Using DBConnSAFullPerms to be able to insert registrations and certificates dbMap, err := sa.DBMapForTest(vars.DBConnSAFullPerms) if err != nil { t.Fatalf("Couldn't connect the database: %s", err) } cleanUp := test.ResetBoulderTestDatabase(t) ssa, err := sa.NewSQLStorageAuthority(dbMap, dbMap, nil, 1, 0, fc, log, metrics.NoopRegisterer) if err != nil { t.Fatalf("unable to create SQLStorageAuthority: %s", err) } return testCtx{ c: idExporter{ dbMap: dbMap, log: log, clk: fc, }, ssa: isa.SA{Impl: ssa}, cleanUp: cleanUp, } }