github.com/letsencrypt/boulder@v0.20251208.0/cmd/admin/key.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"crypto/x509"
     7  	"encoding/hex"
     8  	"encoding/pem"
     9  	"errors"
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/user"
    15  	"sync"
    16  	"sync/atomic"
    17  
    18  	"google.golang.org/protobuf/types/known/timestamppb"
    19  
    20  	"github.com/letsencrypt/boulder/core"
    21  	berrors "github.com/letsencrypt/boulder/errors"
    22  	"github.com/letsencrypt/boulder/privatekey"
    23  	sapb "github.com/letsencrypt/boulder/sa/proto"
    24  )
    25  
    26  // subcommandBlockKey encapsulates the "admin block-key" command.
    27  type subcommandBlockKey struct {
    28  	parallelism uint
    29  	comment     string
    30  
    31  	privKey           string
    32  	spkiFile          string
    33  	certFile          string
    34  	csrFile           string
    35  	csrFileExpectedCN string
    36  
    37  	checkSignature bool
    38  }
    39  
    40  var _ subcommand = (*subcommandBlockKey)(nil)
    41  
    42  func (s *subcommandBlockKey) Desc() string {
    43  	return "Block a keypair from any future issuance"
    44  }
    45  
    46  func (s *subcommandBlockKey) Flags(flag *flag.FlagSet) {
    47  	// General flags relevant to all key input methods.
    48  	flag.UintVar(&s.parallelism, "parallelism", 10, "Number of concurrent workers to use while blocking keys")
    49  	flag.StringVar(&s.comment, "comment", "", "Additional context to add to database comment column")
    50  
    51  	// Flags specifying the input method for the keys to be blocked.
    52  	flag.StringVar(&s.privKey, "private-key", "", "Block issuance for the pubkey corresponding to this private key")
    53  	flag.StringVar(&s.spkiFile, "spki-file", "", "Block issuance for all keys listed in this file as SHA256 hashes of SPKI, hex encoded, one per line")
    54  	flag.StringVar(&s.certFile, "cert-file", "", "Block issuance for the public key of the single PEM-formatted certificate in this file")
    55  	flag.StringVar(&s.csrFile, "csr-file", "", "Block issuance for the public key of the single PEM-formatted CSR in this file")
    56  	flag.StringVar(&s.csrFileExpectedCN, "csr-file-expected-cn", "The key that signed this CSR has been publicly disclosed. It should not be used for any purpose.", "The Subject CN of a CSR will be verified to match this before blocking")
    57  
    58  	flag.BoolVar(&s.checkSignature, "check-signature", true, "Check self-signature of CSR before revoking")
    59  }
    60  
    61  func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
    62  	// This is a map of all input-selection flags to whether or not they were set
    63  	// to a non-default value. We use this to ensure that exactly one input
    64  	// selection flag was given on the command line.
    65  	setInputs := map[string]bool{
    66  		"-private-key": s.privKey != "",
    67  		"-spki-file":   s.spkiFile != "",
    68  		"-cert-file":   s.certFile != "",
    69  		"-csr-file":    s.csrFile != "",
    70  	}
    71  	activeFlag, err := findActiveInputMethodFlag(setInputs)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	var spkiHashes [][]byte
    77  	switch activeFlag {
    78  	case "-private-key":
    79  		var spkiHash []byte
    80  		spkiHash, err = a.spkiHashFromPrivateKey(s.privKey)
    81  		spkiHashes = [][]byte{spkiHash}
    82  	case "-spki-file":
    83  		spkiHashes, err = a.spkiHashesFromFile(s.spkiFile)
    84  	case "-cert-file":
    85  		spkiHashes, err = a.spkiHashesFromCertPEM(s.certFile)
    86  	case "-csr-file":
    87  		spkiHashes, err = a.spkiHashFromCSRPEM(s.csrFile, s.checkSignature, s.csrFileExpectedCN)
    88  	default:
    89  		return errors.New("no recognized input method flag set (this shouldn't happen)")
    90  	}
    91  	if err != nil {
    92  		return fmt.Errorf("collecting spki hashes to block: %w", err)
    93  	}
    94  
    95  	err = a.blockSPKIHashes(ctx, spkiHashes, s.comment, s.parallelism)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  func (a *admin) spkiHashFromPrivateKey(keyFile string) ([]byte, error) {
   104  	_, publicKey, err := privatekey.Load(keyFile)
   105  	if err != nil {
   106  		return nil, fmt.Errorf("loading private key file: %w", err)
   107  	}
   108  
   109  	spkiHash, err := core.KeyDigest(publicKey)
   110  	if err != nil {
   111  		return nil, fmt.Errorf("computing SPKI hash: %w", err)
   112  	}
   113  
   114  	return spkiHash[:], nil
   115  }
   116  
   117  func (a *admin) spkiHashesFromFile(filePath string) ([][]byte, error) {
   118  	file, err := os.Open(filePath)
   119  	if err != nil {
   120  		return nil, fmt.Errorf("opening spki hashes file: %w", err)
   121  	}
   122  
   123  	var spkiHashes [][]byte
   124  	scanner := bufio.NewScanner(file)
   125  	for scanner.Scan() {
   126  		spkiHex := scanner.Text()
   127  		if spkiHex == "" {
   128  			continue
   129  		}
   130  		spkiHash, err := hex.DecodeString(spkiHex)
   131  		if err != nil {
   132  			return nil, fmt.Errorf("decoding hex spki hash %q: %w", spkiHex, err)
   133  		}
   134  
   135  		if len(spkiHash) != 32 {
   136  			return nil, fmt.Errorf("got spki hash of unexpected length: %q (%d)", spkiHex, len(spkiHash))
   137  		}
   138  
   139  		spkiHashes = append(spkiHashes, spkiHash)
   140  	}
   141  
   142  	return spkiHashes, nil
   143  }
   144  
   145  func (a *admin) spkiHashesFromCertPEM(filename string) ([][]byte, error) {
   146  	cert, err := core.LoadCert(filename)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("loading certificate pem: %w", err)
   149  	}
   150  
   151  	spkiHash, err := core.KeyDigest(cert.PublicKey)
   152  	if err != nil {
   153  		return nil, fmt.Errorf("computing SPKI hash: %w", err)
   154  	}
   155  
   156  	return [][]byte{spkiHash[:]}, nil
   157  }
   158  
   159  func (a *admin) spkiHashFromCSRPEM(filename string, checkSignature bool, expectedCN string) ([][]byte, error) {
   160  	csrFile, err := os.ReadFile(filename)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("reading CSR file %q: %w", filename, err)
   163  	}
   164  
   165  	data, _ := pem.Decode(csrFile)
   166  	if data == nil {
   167  		return nil, fmt.Errorf("no PEM data found in %q", filename)
   168  	}
   169  
   170  	a.log.AuditInfof("Parsing key to block from CSR PEM: %x", data)
   171  
   172  	csr, err := x509.ParseCertificateRequest(data.Bytes)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("parsing CSR %q: %w", filename, err)
   175  	}
   176  
   177  	if checkSignature {
   178  		err = csr.CheckSignature()
   179  		if err != nil {
   180  			return nil, fmt.Errorf("checking CSR signature: %w", err)
   181  		}
   182  	}
   183  
   184  	if csr.Subject.CommonName != expectedCN {
   185  		return nil, fmt.Errorf("Got CSR CommonName %q, expected %q", csr.Subject.CommonName, expectedCN)
   186  	}
   187  
   188  	spkiHash, err := core.KeyDigest(csr.PublicKey)
   189  	if err != nil {
   190  		return nil, fmt.Errorf("computing SPKI hash: %w", err)
   191  	}
   192  
   193  	return [][]byte{spkiHash[:]}, nil
   194  }
   195  
   196  func (a *admin) blockSPKIHashes(ctx context.Context, spkiHashes [][]byte, comment string, parallelism uint) error {
   197  	u, err := user.Current()
   198  	if err != nil {
   199  		return fmt.Errorf("getting admin username: %w", err)
   200  	}
   201  
   202  	var errCount atomic.Uint64
   203  	wg := new(sync.WaitGroup)
   204  	work := make(chan []byte, parallelism)
   205  	for range parallelism {
   206  		wg.Go(func() {
   207  			for spkiHash := range work {
   208  				err = a.blockSPKIHash(ctx, spkiHash, u, comment)
   209  				if err != nil {
   210  					errCount.Add(1)
   211  					if errors.Is(err, berrors.AlreadyRevoked) {
   212  						a.log.Errf("not blocking %x: already blocked", spkiHash)
   213  					} else {
   214  						a.log.Errf("failed to block %x: %s", spkiHash, err)
   215  					}
   216  				}
   217  			}
   218  		})
   219  	}
   220  
   221  	for _, spkiHash := range spkiHashes {
   222  		work <- spkiHash
   223  	}
   224  	close(work)
   225  	wg.Wait()
   226  
   227  	if errCount.Load() > 0 {
   228  		return fmt.Errorf("encountered %d errors while revoking certs; see logs above for details", errCount.Load())
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  func (a *admin) blockSPKIHash(ctx context.Context, spkiHash []byte, u *user.User, comment string) error {
   235  	exists, err := a.saroc.KeyBlocked(ctx, &sapb.SPKIHash{KeyHash: spkiHash})
   236  	if err != nil {
   237  		return fmt.Errorf("checking if key is already blocked: %w", err)
   238  	}
   239  	if exists.Exists {
   240  		return berrors.AlreadyRevokedError("the provided key already exists in the 'blockedKeys' table")
   241  	}
   242  
   243  	stream, err := a.saroc.GetSerialsByKey(ctx, &sapb.SPKIHash{KeyHash: spkiHash})
   244  	if err != nil {
   245  		return fmt.Errorf("setting up stream of serials from SA: %s", err)
   246  	}
   247  
   248  	var count int
   249  	for {
   250  		_, err := stream.Recv()
   251  		if err != nil {
   252  			if err == io.EOF {
   253  				break
   254  			}
   255  			return fmt.Errorf("streaming serials from SA: %s", err)
   256  		}
   257  		count++
   258  	}
   259  
   260  	a.log.Infof("Found %d unexpired certificates matching the provided key", count)
   261  
   262  	_, err = a.sac.AddBlockedKey(ctx, &sapb.AddBlockedKeyRequest{
   263  		KeyHash:   spkiHash[:],
   264  		Added:     timestamppb.New(a.clk.Now()),
   265  		Source:    "admin-revoker",
   266  		Comment:   fmt.Sprintf("%s: %s", u.Username, comment),
   267  		RevokedBy: 0,
   268  	})
   269  	if err != nil {
   270  		return fmt.Errorf("blocking key: %w", err)
   271  	}
   272  
   273  	return nil
   274  }