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 }