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 }