1 package notmain
2
3 import (
4 "bufio"
5 "context"
6 "crypto"
7 "crypto/sha256"
8 "crypto/x509"
9 "errors"
10 "flag"
11 "fmt"
12 "io"
13 "os"
14 "os/user"
15 "sort"
16 "strconv"
17 "sync"
18
19 "github.com/jmhodges/clock"
20 "github.com/letsencrypt/boulder/cmd"
21 "github.com/letsencrypt/boulder/core"
22 "github.com/letsencrypt/boulder/db"
23 berrors "github.com/letsencrypt/boulder/errors"
24 "github.com/letsencrypt/boulder/features"
25 bgrpc "github.com/letsencrypt/boulder/grpc"
26 blog "github.com/letsencrypt/boulder/log"
27 "github.com/letsencrypt/boulder/metrics"
28 "github.com/letsencrypt/boulder/privatekey"
29 rapb "github.com/letsencrypt/boulder/ra/proto"
30 "github.com/letsencrypt/boulder/revocation"
31 "github.com/letsencrypt/boulder/sa"
32 sapb "github.com/letsencrypt/boulder/sa/proto"
33 "google.golang.org/protobuf/types/known/timestamppb"
34 )
35
36 const usageString = `
37 usage:
38 list-reasons -config <path>
39 serial-revoke -config <path> <serial> <reason-code>
40 malformed-revoke -config <path> <serial> <reason-code>
41 batched-serial-revoke -config <path> <serial-file-path> <reason-code> <parallelism>
42 incident-table-revoke -config <path> <table-name> <reason-code> <parallelism>
43 reg-revoke -config <path> <registration-id> <reason-code>
44 private-key-block -config <path> -comment="<string>" -dry-run=<bool> <priv-key-path>
45 private-key-revoke -config <path> -comment="<string>" -dry-run=<bool> <priv-key-path>
46 clear-email -config <path> <email-address>
47
48
49 descriptions:
50 list-reasons List all revocation reason codes.
51 serial-revoke Revoke a single certificate by the hex serial number.
52 malformed-revoke Revoke a single certificate by the hex serial number. Works even
53 if the certificate cannot be parsed from the database.
54 Note: This does not purge the Akamai cache.
55 Note: This cannot be used to revoke for key compromise.
56 batched-serial-revoke Revoke all certificates contained in a file of hex serial numbers.
57 incident-table-revoke Revoke all certificates in the provided incident table.
58 reg-revoke Revoke all certificates associated with a registration ID.
59 private-key-block Adds the SPKI hash, derived from the provided private key, to the
60 blocked keys table. <priv-key-path> is expected to be the path
61 to a PEM formatted file containing an RSA or ECDSA private key.
62 private-key-revoke Revoke all certificates matching the SPKI hash derived from the
63 provided private key. Then adds the hash to the blocked keys
64 table. <priv-key-path> is expected to be the path to a PEM
65 formatted file containing an RSA or ECDSA private key.
66 clear-email Delete all instances of a given email from all accounts (slow).
67
68 flags:
69 all:
70 -config File path to the configuration file for this service (required)
71
72 private-key-block | private-key-revoke:
73 -dry-run true (default): only queries for affected certificates. false: will
74 perform the requested block or revoke action. Only implemented for
75 private-key-block and private-key-revoke.
76 -comment Comment to include in the blocked keys table entry. (default: "")
77 `
78
79 type Config struct {
80 Revoker struct {
81 DB cmd.DBConfig
82
83
84
85 TLS cmd.TLSConfig
86
87 RAService *cmd.GRPCClientConfig
88 SAService *cmd.GRPCClientConfig
89
90 Features map[string]bool
91 }
92
93 Syslog cmd.SyslogConfig
94 }
95
96 type revoker struct {
97 rac rapb.RegistrationAuthorityClient
98 sac sapb.StorageAuthorityClient
99 dbMap *db.WrappedMap
100 clk clock.Clock
101 log blog.Logger
102 }
103
104 func newRevoker(c Config) *revoker {
105 logger := cmd.NewLogger(c.Syslog)
106 logger.Info(cmd.VersionString())
107
108
109 tlsConfig, err := c.Revoker.TLS.Load(metrics.NoopRegisterer)
110 cmd.FailOnError(err, "TLS config")
111
112 clk := cmd.Clock()
113
114 raConn, err := bgrpc.ClientSetup(c.Revoker.RAService, tlsConfig, metrics.NoopRegisterer, clk)
115 cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
116 rac := rapb.NewRegistrationAuthorityClient(raConn)
117
118 dbMap, err := sa.InitWrappedDb(c.Revoker.DB, nil, logger)
119 cmd.FailOnError(err, "While initializing dbMap")
120
121 saConn, err := bgrpc.ClientSetup(c.Revoker.SAService, tlsConfig, metrics.NoopRegisterer, clk)
122 cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
123 sac := sapb.NewStorageAuthorityClient(saConn)
124
125 return &revoker{
126 rac: rac,
127 sac: sac,
128 dbMap: dbMap,
129 clk: clk,
130 log: logger,
131 }
132 }
133
134 func (r *revoker) revokeCertificate(ctx context.Context, certObj core.Certificate, reasonCode revocation.Reason, skipBlockKey bool) error {
135 if reasonCode < 0 || reasonCode == 7 || reasonCode > 10 {
136 panic(fmt.Sprintf("Invalid reason code: %d", reasonCode))
137 }
138 u, err := user.Current()
139 if err != nil {
140 return err
141 }
142
143 var req *rapb.AdministrativelyRevokeCertificateRequest
144 if certObj.DER != nil {
145 cert, err := x509.ParseCertificate(certObj.DER)
146 if err != nil {
147 return err
148 }
149 req = &rapb.AdministrativelyRevokeCertificateRequest{
150 Cert: cert.Raw,
151 Serial: core.SerialToString(cert.SerialNumber),
152 Code: int64(reasonCode),
153 AdminName: u.Username,
154 SkipBlockKey: skipBlockKey,
155 }
156 } else {
157 req = &rapb.AdministrativelyRevokeCertificateRequest{
158 Serial: certObj.Serial,
159 Code: int64(reasonCode),
160 AdminName: u.Username,
161 SkipBlockKey: skipBlockKey,
162 }
163 }
164 _, err = r.rac.AdministrativelyRevokeCertificate(ctx, req)
165 if err != nil {
166 return err
167 }
168 r.log.Infof("Revoked certificate %s with reason '%s'", certObj.Serial, revocation.ReasonToString[reasonCode])
169 return nil
170 }
171
172 func (r *revoker) revokeBySerial(ctx context.Context, serial string, reasonCode revocation.Reason, skipBlockKey bool) error {
173 certObj, err := sa.SelectPrecertificate(ctx, r.dbMap, serial)
174 if err != nil {
175 if db.IsNoRows(err) {
176 return berrors.NotFoundError("precertificate with serial %q not found", serial)
177 }
178 return err
179 }
180 return r.revokeCertificate(ctx, certObj, reasonCode, skipBlockKey)
181 }
182
183 func (r *revoker) revokeSerialBatchFile(ctx context.Context, serialPath string, reasonCode revocation.Reason, parallelism int) error {
184 file, err := os.Open(serialPath)
185 if err != nil {
186 return err
187 }
188
189 scanner := bufio.NewScanner(file)
190 if err != nil {
191 return err
192 }
193
194 wg := new(sync.WaitGroup)
195 work := make(chan string, parallelism)
196 for i := 0; i < parallelism; i++ {
197 wg.Add(1)
198 go func() {
199 defer wg.Done()
200 for serial := range work {
201
202 if serial == "" {
203 continue
204 }
205 err := r.revokeBySerial(ctx, serial, reasonCode, false)
206 if err != nil {
207 r.log.Errf("failed to revoke %q: %s", serial, err)
208 }
209 }
210 }()
211 }
212
213 for scanner.Scan() {
214 serial := scanner.Text()
215 if serial == "" {
216 continue
217 }
218 work <- serial
219 }
220 close(work)
221 wg.Wait()
222
223 return nil
224 }
225
226
227
228 func (r *revoker) clearEmailAddress(ctx context.Context, email string) error {
229 r.log.AuditInfof("Scanning database for accounts with email addresses matching %q in order to clear the email addresses.", email)
230 regIDs, err := r.getRegIDsMatchingEmail(ctx, email)
231 if err != nil {
232 return err
233 }
234
235 r.log.Infof("Found %d registration IDs matching email %q.", len(regIDs), email)
236
237 failures := 0
238 for _, regID := range regIDs {
239 err := sa.ClearEmail(ctx, r.dbMap, regID, email)
240 if err != nil {
241
242
243 r.log.AuditErrf("failed to clear email %q for registration ID %d: %s", email, regID, err)
244 failures++
245 } else {
246 r.log.AuditInfof("cleared email %q for registration ID %d: %s", email, regID, err)
247 }
248 }
249 if failures > 0 {
250 return fmt.Errorf("failed to clear email for %d out of %d registration IDs", failures, len(regIDs))
251 }
252 return nil
253 }
254
255 func (r *revoker) revokeIncidentTableSerials(ctx context.Context, tableName string, reasonCode revocation.Reason, parallelism int) error {
256 wg := new(sync.WaitGroup)
257 work := make(chan string, parallelism)
258 for i := 0; i < parallelism; i++ {
259 wg.Add(1)
260 go func() {
261 defer wg.Done()
262 for serial := range work {
263 err := r.revokeBySerial(ctx, serial, reasonCode, false)
264 if err != nil {
265 r.log.Errf("failed to revoke %q: %s", serial, err)
266 }
267 }
268 }()
269 }
270
271 stream, err := r.sac.SerialsForIncident(ctx, &sapb.SerialsForIncidentRequest{IncidentTable: tableName})
272 if err != nil {
273 return fmt.Errorf("setting up stream of serials from incident table %q: %s", tableName, err)
274 }
275
276 var atLeastOne bool
277 for {
278 is, err := stream.Recv()
279 if err != nil {
280 if err == io.EOF {
281 break
282 }
283 return fmt.Errorf("streaming serials from incident table %q: %s", tableName, err)
284 }
285 atLeastOne = true
286 work <- is.Serial
287 }
288 if !atLeastOne {
289 r.log.AuditInfof("No serials found in incident table %q", tableName)
290 }
291 close(work)
292 wg.Wait()
293
294 return nil
295 }
296
297 func (r *revoker) revokeByReg(ctx context.Context, regID int64, reasonCode revocation.Reason) error {
298 _, err := r.sac.GetRegistration(ctx, &sapb.RegistrationID{Id: regID})
299 if err != nil {
300 return fmt.Errorf("couldn't fetch registration: %w", err)
301 }
302
303 certObjs, err := sa.SelectPrecertificates(ctx, r.dbMap, "WHERE registrationID = :regID", map[string]interface{}{"regID": regID})
304 if err != nil {
305 return err
306 }
307 for _, certObj := range certObjs {
308 err = r.revokeCertificate(ctx, certObj.Certificate, reasonCode, false)
309 if err != nil {
310 return err
311 }
312 }
313 return nil
314 }
315
316 func (r *revoker) revokeMalformedBySerial(ctx context.Context, serial string, reasonCode revocation.Reason) error {
317 return r.revokeCertificate(ctx, core.Certificate{Serial: serial}, reasonCode, false)
318 }
319
320
321
322
323
324
325
326
327 func (r *revoker) blockByPrivateKey(ctx context.Context, comment string, privateKey string) error {
328 _, publicKey, err := privatekey.Load(privateKey)
329 if err != nil {
330 return err
331 }
332
333 spkiHash, err := getPublicKeySPKIHash(publicKey)
334 if err != nil {
335 return err
336 }
337
338 u, err := user.Current()
339 if err != nil {
340 return err
341 }
342
343 dbcomment := fmt.Sprintf("%s: %s", u.Username, comment)
344
345 now := r.clk.Now()
346 req := &sapb.AddBlockedKeyRequest{
347 KeyHash: spkiHash,
348 AddedNS: now.UnixNano(),
349 Added: timestamppb.New(now),
350 Source: "admin-revoker",
351 Comment: dbcomment,
352 RevokedBy: 0,
353 }
354
355 _, err = r.sac.AddBlockedKey(ctx, req)
356 if err != nil {
357 return err
358 }
359 return nil
360 }
361
362
363
364
365
366
367
368
369 func (r *revoker) revokeByPrivateKey(ctx context.Context, privateKey string) error {
370 _, publicKey, err := privatekey.Load(privateKey)
371 if err != nil {
372 return err
373 }
374
375 spkiHash, err := getPublicKeySPKIHash(publicKey)
376 if err != nil {
377 return err
378 }
379
380 matches, err := r.getCertsMatchingSPKIHash(ctx, spkiHash)
381 if err != nil {
382 return err
383 }
384
385 for i, match := range matches {
386 resp, err := r.sac.GetCertificateStatus(ctx, &sapb.Serial{Serial: match})
387 if err != nil {
388 return fmt.Errorf(
389 "failed to get status for serial %q. Entry %d of %d affected certificates: %w",
390 match,
391 (i + 1),
392 len(matches),
393 err,
394 )
395 }
396
397 if resp.Status != string(core.OCSPStatusGood) {
398 r.log.AuditInfof("serial %q is already revoked, skipping", match)
399 continue
400 }
401
402 err = r.revokeBySerial(ctx, match, revocation.Reason(1), true)
403 if err != nil {
404 return fmt.Errorf(
405 "failed to revoke serial %q. Entry %d of %d affected certificates: %w",
406 match,
407 (i + 1),
408 len(matches),
409 err,
410 )
411 }
412 }
413 return nil
414 }
415
416 func (r *revoker) spkiHashInBlockedKeys(ctx context.Context, spkiHash []byte) (bool, error) {
417 var count int
418 err := r.dbMap.SelectOne(ctx, &count, "SELECT COUNT(*) as count FROM blockedKeys WHERE keyHash = ?", spkiHash)
419 if err != nil {
420 return false, err
421 }
422
423 if count > 0 {
424 return true, nil
425 }
426 return false, nil
427 }
428
429 func (r *revoker) countCertsMatchingSPKIHash(ctx context.Context, spkiHash []byte) (int, error) {
430 var count int
431 err := r.dbMap.SelectOne(ctx, &count, "SELECT COUNT(*) as count FROM keyHashToSerial WHERE keyHash = ?", spkiHash)
432 if err != nil {
433 return 0, err
434 }
435 return count, nil
436 }
437
438
439
440
441
442 func (r *revoker) getRegIDsMatchingEmail(ctx context.Context, email string) ([]int64, error) {
443
444
445 var regIDs []int64
446 _, err := r.dbMap.Select(ctx, ®IDs, "SELECT id FROM registrations WHERE contact LIKE CONCAT('%\"mailto:', ?, '\"%')", email)
447 if err != nil {
448 return nil, err
449 }
450 return regIDs, nil
451 }
452
453
454
455 func (r *revoker) getCertsMatchingSPKIHash(ctx context.Context, spkiHash []byte) ([]string, error) {
456 var h []string
457 _, err := r.dbMap.Select(ctx, &h, "SELECT certSerial FROM keyHashToSerial WHERE keyHash = ?", spkiHash)
458 if err != nil {
459 if db.IsNoRows(err) {
460 return nil, berrors.NotFoundError("no certificates with a matching SPKI hash were found")
461 }
462 return nil, err
463 }
464 return h, nil
465 }
466
467
468 type revocationCodes []revocation.Reason
469
470 func (rc revocationCodes) Len() int { return len(rc) }
471 func (rc revocationCodes) Less(i, j int) bool { return rc[i] < rc[j] }
472 func (rc revocationCodes) Swap(i, j int) { rc[i], rc[j] = rc[j], rc[i] }
473
474 func privateKeyBlock(ctx context.Context, r *revoker, dryRun bool, comment string, count int, spkiHash []byte, keyPath string) error {
475 keyExists, err := r.spkiHashInBlockedKeys(ctx, spkiHash)
476 if err != nil {
477 return fmt.Errorf("while checking if the provided key already exists in the 'blockedKeys' table: %s", err)
478 }
479 if keyExists {
480 return errors.New("the provided key already exists in the 'blockedKeys' table")
481 }
482
483 if dryRun {
484 r.log.AuditInfof(
485 "To block issuance for this key and revoke %d certificates via bad-key-revoker, run with -dry-run=false",
486 count,
487 )
488 r.log.AuditInfo("No keys were blocked or certificates revoked, exiting...")
489 return nil
490 }
491
492 r.log.AuditInfo("Attempting to block issuance for the provided key")
493 err = r.blockByPrivateKey(context.Background(), comment, keyPath)
494 if err != nil {
495 return fmt.Errorf("while attempting to block issuance for the provided key: %s", err)
496 }
497 r.log.AuditInfo("Issuance for the provided key has been successfully blocked, exiting...")
498 return nil
499 }
500
501 func privateKeyRevoke(r *revoker, dryRun bool, comment string, count int, keyPath string) error {
502 if dryRun {
503 r.log.AuditInfof(
504 "To immediately revoke %d certificates and block issuance for this key, run with -dry-run=false",
505 count,
506 )
507 r.log.AuditInfo("No keys were blocked or certificates revoked, exiting...")
508 return nil
509 }
510
511 if count <= 0 {
512
513 return nil
514 }
515
516
517 r.log.AuditInfof("Attempting to revoke %d certificates", count)
518 err := r.revokeByPrivateKey(context.Background(), keyPath)
519 if err != nil {
520 return fmt.Errorf("while attempting to revoke certificates for the provided key: %s", err)
521 }
522 r.log.AuditInfo("All certificates matching using the provided key have been successfully")
523
524
525 r.log.AuditInfo("Attempting to block issuance for the provided key")
526 err = r.blockByPrivateKey(context.Background(), comment, keyPath)
527 if err != nil {
528 return fmt.Errorf("while attempting to block issuance for the provided key: %s", err)
529 }
530 r.log.AuditInfo("All certificates have been successfully revoked and issuance blocked, exiting...")
531 return nil
532 }
533
534
535
536 func getPublicKeySPKIHash(pubKey crypto.PublicKey) ([]byte, error) {
537 rawSubjectPublicKeyInfo, err := x509.MarshalPKIXPublicKey(pubKey)
538 if err != nil {
539 return nil, err
540 }
541 spkiHash := sha256.Sum256(rawSubjectPublicKeyInfo)
542 return spkiHash[:], nil
543 }
544
545 func main() {
546 usage := func() {
547 fmt.Fprint(os.Stderr, usageString)
548 os.Exit(1)
549 }
550 if len(os.Args) <= 2 {
551 usage()
552 }
553
554 command := os.Args[1]
555 flagSet := flag.NewFlagSet(command, flag.ContinueOnError)
556 configFile := flagSet.String("config", "", "File path to the configuration file for this service")
557 dryRun := flagSet.Bool(
558 "dry-run",
559 true,
560 "true (default): only queries for affected certificates. false: will perform the requested block or revoke action",
561 )
562 comment := flagSet.String("comment", "", "Comment to include in the blocked key database entry ")
563 err := flagSet.Parse(os.Args[2:])
564 if err == flag.ErrHelp {
565 os.Exit(1)
566 }
567 cmd.FailOnError(err, "parsing flagset")
568
569 if *configFile == "" {
570 usage()
571 }
572
573 var c Config
574 err = cmd.ReadConfigFile(*configFile, &c)
575 cmd.FailOnError(err, "Reading JSON config file into config structure")
576 err = features.Set(c.Revoker.Features)
577 cmd.FailOnError(err, "Failed to set feature flags")
578
579 ctx := context.Background()
580 r := newRevoker(c)
581
582 args := flagSet.Args()
583 switch {
584 case command == "serial-revoke" && len(args) == 2:
585
586 serial := args[0]
587 reasonCode, err := strconv.Atoi(args[1])
588 cmd.FailOnError(err, "Reason code argument must be an integer")
589
590 err = r.revokeBySerial(ctx, serial, revocation.Reason(reasonCode), false)
591 cmd.FailOnError(err, "Couldn't revoke certificate by serial")
592
593 case command == "batched-serial-revoke" && len(args) == 3:
594
595 serialPath := args[0]
596 reasonCode, err := strconv.Atoi(args[1])
597 cmd.FailOnError(err, "Reason code argument must be an integer")
598 parallelism, err := strconv.Atoi(args[2])
599 cmd.FailOnError(err, "parallelism argument must be an integer")
600 if parallelism < 1 {
601 cmd.Fail("parallelism argument must be >= 1")
602 }
603
604 err = r.revokeSerialBatchFile(ctx, serialPath, revocation.Reason(reasonCode), parallelism)
605 cmd.FailOnError(err, "Batch revocation failed")
606
607 case command == "reg-revoke" && len(args) == 2:
608
609 regID, err := strconv.ParseInt(args[0], 10, 64)
610 cmd.FailOnError(err, "Registration ID argument must be an integer")
611 reasonCode, err := strconv.Atoi(args[1])
612 cmd.FailOnError(err, "Reason code argument must be an integer")
613
614 err = r.revokeByReg(ctx, regID, revocation.Reason(reasonCode))
615 cmd.FailOnError(err, "Couldn't revoke certificate by registration")
616
617 case command == "malformed-revoke" && len(args) == 3:
618
619 serial := args[0]
620 reasonCode, err := strconv.Atoi(args[1])
621 cmd.FailOnError(err, "Reason code argument must be an integer")
622
623 err = r.revokeMalformedBySerial(ctx, serial, revocation.Reason(reasonCode))
624 cmd.FailOnError(err, "Couldn't revoke certificate by serial")
625
626 case command == "list-reasons":
627 var codes revocationCodes
628 for k := range revocation.ReasonToString {
629 codes = append(codes, k)
630 }
631 sort.Sort(codes)
632 fmt.Printf("Revocation reason codes\n-----------------------\n\n")
633 for _, k := range codes {
634 fmt.Printf("%d: %s\n", k, revocation.ReasonToString[k])
635 }
636
637 case (command == "private-key-block" || command == "private-key-revoke") && len(args) == 1:
638
639 keyPath := args[0]
640
641 _, publicKey, err := privatekey.Load(keyPath)
642 cmd.FailOnError(err, "Failed to load the provided private key")
643 r.log.AuditInfo("The provided private key has been successfully verified")
644
645 spkiHash, err := getPublicKeySPKIHash(publicKey)
646 cmd.FailOnError(err, "While obtaining the SPKI hash for the provided key")
647
648 count, err := r.countCertsMatchingSPKIHash(ctx, spkiHash)
649 cmd.FailOnError(err, "While retrieving a count of certificates matching the provided key")
650 r.log.AuditInfof("Found %d certificates matching the provided key", count)
651
652 if command == "private-key-block" {
653 err := privateKeyBlock(ctx, r, *dryRun, *comment, count, spkiHash, keyPath)
654 cmd.FailOnError(err, "")
655 }
656
657 if command == "private-key-revoke" {
658 err := privateKeyRevoke(r, *dryRun, *comment, count, keyPath)
659 cmd.FailOnError(err, "")
660 }
661
662 case command == "incident-table-revoke" && len(args) == 3:
663
664 tableName := args[0]
665
666 reasonCode, err := strconv.Atoi(args[1])
667 cmd.FailOnError(err, "Reason code argument must be an integer")
668
669 parallelism, err := strconv.Atoi(args[2])
670 cmd.FailOnError(err, "parallelism argument must be an integer")
671 if parallelism < 1 {
672 cmd.Fail("parallelism argument must be >= 1")
673 }
674 err = r.revokeIncidentTableSerials(ctx, tableName, revocation.Reason(reasonCode), parallelism)
675 cmd.FailOnError(err, "Couldn't revoke serials in incident table")
676
677 case command == "clear-email" && len(args) == 1:
678 email := args[0]
679 err := r.clearEmailAddress(ctx, email)
680 cmd.FailOnError(err, "Clearing email address")
681
682 default:
683 fmt.Fprintf(os.Stderr, "unrecognized subcommand %q\n\n", command)
684 usage()
685 }
686 }
687
688 func init() {
689
690
691 cmd.RegisterCommand("admin-revoker", main, &cmd.ConfigValidator{Config: &Config{}})
692 cmd.RegisterCommand("admin", main, &cmd.ConfigValidator{Config: &Config{}})
693 }
694
View as plain text