...

Source file src/github.com/letsencrypt/boulder/cmd/bad-key-revoker/main_test.go

Documentation: github.com/letsencrypt/boulder/cmd/bad-key-revoker

     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 // 90 days exclusive
   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  // Test that we produce an error when a serial from the keyHashToSerial table
   186  // does not have a corresponding entry in the certificateStatus and
   187  // precertificates table.
   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), // 90 days exclusive
   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  	// insert valid, unexpired
   227  	insertCert(t, dbMap, fc, hashA, "ff", regID, Unexpired, Unrevoked)
   228  	// insert valid, unexpired, duplicate
   229  	insertCert(t, dbMap, fc, hashA, "ff", regID, Unexpired, Unrevoked)
   230  	// insert valid, expired
   231  	insertCert(t, dbMap, fc, hashA, "ee", regID, Expired, Unrevoked)
   232  	// insert revoked
   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  	// populate DB with all the test data
   333  	regIDA := insertRegistration(t, dbMap, fc, "example.com")
   334  	hashA := randHash(t)
   335  	insertBlockedRow(t, dbMap, fc, hashA, regIDA, false)
   336  
   337  	// Add an entry to keyHashToSerial but not to certificateStatus or certificate
   338  	// status, and expect an error.
   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), // 90 days exclusive
   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  	// populate DB with all the test data
   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  	// add a row with no associated valid certificates
   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  	// This test checks that when the user who revoked the initial
   434  	// certificate that added the row to blockedKeys doesn't have any
   435  	// extant certificates themselves their contact email is still
   436  	// resolved and we avoid sending any emails to accounts that
   437  	// share the same email.
   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  	// populate DB with all the test data
   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  	// Backoff once. Check to make sure the backoff is logged.
   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  	// Make sure `backoffReset` resets the ticker.
   498  	bkr.backoffReset()
   499  	test.AssertEquals(t, bkr.backoffTicker, 0)
   500  }
   501  

View as plain text