boulder/cmd/id-exporter/main_test.go

462 lines
15 KiB
Go

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,
}
}