github.com/letsencrypt/boulder@v0.20251208.0/cmd/bad-key-revoker/main_test.go (about)

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