...

Source file src/github.com/letsencrypt/boulder/cmd/admin-revoker/main.go

Documentation: github.com/letsencrypt/boulder/cmd/admin-revoker

     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  		// Similarly, the Revoker needs a TLSConfig to set up its GRPC client
    83  		// certs, but doesn't get the TLS field from ServiceConfig, so declares
    84  		// its own.
    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  	// TODO(#6840) Rework admin-revoker to export prometheus metrics.
   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  				// handle newlines gracefully
   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  // clearEmailAddress clears the given email address from all accounts that have it.
   227  // Finding relevant accounts will be very slow because it does not use an index.
   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  			// Log, but don't fail, because it took a long time to find the relevant registration IDs
   242  			// and we don't want to have to redo that work.
   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  // blockByPrivateKey blocks future issuance for certificates with a a public key
   321  // matching the SubjectPublicKeyInfo hash generated from the PublicKey embedded
   322  // in privateKey. The embedded PublicKey will be verified as an actual match for
   323  // the provided private key before any blocking takes place. This method does
   324  // not revoke any certificates directly. However, 'bad-key-revoker', which
   325  // references the 'blockedKeys' table, will eventually revoke certificates with
   326  // a matching SPKI hash.
   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  // revokeByPrivateKey revokes all certificates with a public key matching the
   363  // SubjectPublicKeyInfo hash generated from the PublicKey embedded in
   364  // privateKey. The embedded PublicKey will be verified as an actual match for the
   365  // provided private key before any revocation takes place. The provided key will
   366  // not be added to the 'blockedKeys' table. This is done to avoid a race between
   367  // 'admin-revoker' and 'bad-key-revoker'. You MUST call blockByPrivateKey after
   368  // calling this function, on pain of violating the BRs.
   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  // getRegIDsMatchingEmail returns a list of registration IDs where the contacts list
   439  // contains the given email address. Since this uses a substring match, it is important
   440  // to subsequently parse the JSON list of addresses and look for exact matches.
   441  // Note: Since this does not use an index, it is very slow.
   442  func (r *revoker) getRegIDsMatchingEmail(ctx context.Context, email string) ([]int64, error) {
   443  	// We use SQL `CONCAT` rather than interpolating with `+` or `%s` because we want to
   444  	// use a `?` placeholder for the email, which prevents SQL injection.
   445  	var regIDs []int64
   446  	_, err := r.dbMap.Select(ctx, &regIDs, "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  // TODO(#5899) Use an non-wrapped sql.Db client to iterate over results and
   454  // return them on a channel.
   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  // This abstraction is needed so that we can use sort.Sort below
   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  		// Do not revoke.
   513  		return nil
   514  	}
   515  
   516  	// Revoke certificates.
   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  	// Block future issuance.
   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  // getPublicKeySPKIHash returns a hash of the SubjectPublicKeyInfo for the
   535  // provided public key.
   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  		// 1: serial,  2: reasonCode
   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  		// 1: serial file path,  2: reasonCode, 3: parallelism
   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  		// 1: registration ID,  2: reasonCode
   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  		// 1: serial, 2: reasonCode
   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  		// 1: keyPath
   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  		// 1: tableName, 2: reasonCode, 3: parallelism
   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  	// admin-revoker is the old name. Now that this can also clear email addresses,
   690  	// admin is the new name
   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