277 lines
7.7 KiB
Go
277 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/user"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/privatekey"
|
|
sapb "github.com/letsencrypt/boulder/sa/proto"
|
|
)
|
|
|
|
// subcommandBlockKey encapsulates the "admin block-key" command.
|
|
type subcommandBlockKey struct {
|
|
parallelism uint
|
|
comment string
|
|
|
|
privKey string
|
|
spkiFile string
|
|
certFile string
|
|
csrFile string
|
|
csrFileExpectedCN string
|
|
|
|
checkSignature bool
|
|
}
|
|
|
|
var _ subcommand = (*subcommandBlockKey)(nil)
|
|
|
|
func (s *subcommandBlockKey) Desc() string {
|
|
return "Block a keypair from any future issuance"
|
|
}
|
|
|
|
func (s *subcommandBlockKey) Flags(flag *flag.FlagSet) {
|
|
// General flags relevant to all key input methods.
|
|
flag.UintVar(&s.parallelism, "parallelism", 10, "Number of concurrent workers to use while blocking keys")
|
|
flag.StringVar(&s.comment, "comment", "", "Additional context to add to database comment column")
|
|
|
|
// Flags specifying the input method for the keys to be blocked.
|
|
flag.StringVar(&s.privKey, "private-key", "", "Block issuance for the pubkey corresponding to this private key")
|
|
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")
|
|
flag.StringVar(&s.certFile, "cert-file", "", "Block issuance for the public key of the single PEM-formatted certificate in this file")
|
|
flag.StringVar(&s.csrFile, "csr-file", "", "Block issuance for the public key of the single PEM-formatted CSR in this file")
|
|
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")
|
|
|
|
flag.BoolVar(&s.checkSignature, "check-signature", true, "Check self-signature of CSR before revoking")
|
|
}
|
|
|
|
func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
|
|
// This is a map of all input-selection flags to whether or not they were set
|
|
// to a non-default value. We use this to ensure that exactly one input
|
|
// selection flag was given on the command line.
|
|
setInputs := map[string]bool{
|
|
"-private-key": s.privKey != "",
|
|
"-spki-file": s.spkiFile != "",
|
|
"-cert-file": s.certFile != "",
|
|
"-csr-file": s.csrFile != "",
|
|
}
|
|
activeFlag, err := findActiveInputMethodFlag(setInputs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var spkiHashes [][]byte
|
|
switch activeFlag {
|
|
case "-private-key":
|
|
var spkiHash []byte
|
|
spkiHash, err = a.spkiHashFromPrivateKey(s.privKey)
|
|
spkiHashes = [][]byte{spkiHash}
|
|
case "-spki-file":
|
|
spkiHashes, err = a.spkiHashesFromFile(s.spkiFile)
|
|
case "-cert-file":
|
|
spkiHashes, err = a.spkiHashesFromCertPEM(s.certFile)
|
|
case "-csr-file":
|
|
spkiHashes, err = a.spkiHashFromCSRPEM(s.csrFile, s.checkSignature, s.csrFileExpectedCN)
|
|
default:
|
|
return errors.New("no recognized input method flag set (this shouldn't happen)")
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("collecting spki hashes to block: %w", err)
|
|
}
|
|
|
|
err = a.blockSPKIHashes(ctx, spkiHashes, s.comment, s.parallelism)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *admin) spkiHashFromPrivateKey(keyFile string) ([]byte, error) {
|
|
_, publicKey, err := privatekey.Load(keyFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading private key file: %w", err)
|
|
}
|
|
|
|
spkiHash, err := core.KeyDigest(publicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("computing SPKI hash: %w", err)
|
|
}
|
|
|
|
return spkiHash[:], nil
|
|
}
|
|
|
|
func (a *admin) spkiHashesFromFile(filePath string) ([][]byte, error) {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening spki hashes file: %w", err)
|
|
}
|
|
|
|
var spkiHashes [][]byte
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
spkiHex := scanner.Text()
|
|
if spkiHex == "" {
|
|
continue
|
|
}
|
|
spkiHash, err := hex.DecodeString(spkiHex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decoding hex spki hash %q: %w", spkiHex, err)
|
|
}
|
|
|
|
if len(spkiHash) != 32 {
|
|
return nil, fmt.Errorf("got spki hash of unexpected length: %q (%d)", spkiHex, len(spkiHash))
|
|
}
|
|
|
|
spkiHashes = append(spkiHashes, spkiHash)
|
|
}
|
|
|
|
return spkiHashes, nil
|
|
}
|
|
|
|
func (a *admin) spkiHashesFromCertPEM(filename string) ([][]byte, error) {
|
|
cert, err := core.LoadCert(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("loading certificate pem: %w", err)
|
|
}
|
|
|
|
spkiHash, err := core.KeyDigest(cert.PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("computing SPKI hash: %w", err)
|
|
}
|
|
|
|
return [][]byte{spkiHash[:]}, nil
|
|
}
|
|
|
|
func (a *admin) spkiHashFromCSRPEM(filename string, checkSignature bool, expectedCN string) ([][]byte, error) {
|
|
csrFile, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading CSR file %q: %w", filename, err)
|
|
}
|
|
|
|
data, _ := pem.Decode(csrFile)
|
|
if data == nil {
|
|
return nil, fmt.Errorf("no PEM data found in %q", filename)
|
|
}
|
|
|
|
a.log.AuditInfof("Parsing key to block from CSR PEM: %x", data)
|
|
|
|
csr, err := x509.ParseCertificateRequest(data.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing CSR %q: %w", filename, err)
|
|
}
|
|
|
|
if checkSignature {
|
|
err = csr.CheckSignature()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("checking CSR signature: %w", err)
|
|
}
|
|
}
|
|
|
|
if csr.Subject.CommonName != expectedCN {
|
|
return nil, fmt.Errorf("Got CSR CommonName %q, expected %q", csr.Subject.CommonName, expectedCN)
|
|
}
|
|
|
|
spkiHash, err := core.KeyDigest(csr.PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("computing SPKI hash: %w", err)
|
|
}
|
|
|
|
return [][]byte{spkiHash[:]}, nil
|
|
}
|
|
|
|
func (a *admin) blockSPKIHashes(ctx context.Context, spkiHashes [][]byte, comment string, parallelism uint) error {
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("getting admin username: %w", err)
|
|
}
|
|
|
|
var errCount atomic.Uint64
|
|
wg := new(sync.WaitGroup)
|
|
work := make(chan []byte, parallelism)
|
|
for i := uint(0); i < parallelism; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for spkiHash := range work {
|
|
err = a.blockSPKIHash(ctx, spkiHash, u, comment)
|
|
if err != nil {
|
|
errCount.Add(1)
|
|
if errors.Is(err, berrors.AlreadyRevoked) {
|
|
a.log.Errf("not blocking %x: already blocked", spkiHash)
|
|
} else {
|
|
a.log.Errf("failed to block %x: %s", spkiHash, err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
for _, spkiHash := range spkiHashes {
|
|
work <- spkiHash
|
|
}
|
|
close(work)
|
|
wg.Wait()
|
|
|
|
if errCount.Load() > 0 {
|
|
return fmt.Errorf("encountered %d errors while revoking certs; see logs above for details", errCount.Load())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *admin) blockSPKIHash(ctx context.Context, spkiHash []byte, u *user.User, comment string) error {
|
|
exists, err := a.saroc.KeyBlocked(ctx, &sapb.SPKIHash{KeyHash: spkiHash})
|
|
if err != nil {
|
|
return fmt.Errorf("checking if key is already blocked: %w", err)
|
|
}
|
|
if exists.Exists {
|
|
return berrors.AlreadyRevokedError("the provided key already exists in the 'blockedKeys' table")
|
|
}
|
|
|
|
stream, err := a.saroc.GetSerialsByKey(ctx, &sapb.SPKIHash{KeyHash: spkiHash})
|
|
if err != nil {
|
|
return fmt.Errorf("setting up stream of serials from SA: %s", err)
|
|
}
|
|
|
|
var count int
|
|
for {
|
|
_, err := stream.Recv()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return fmt.Errorf("streaming serials from SA: %s", err)
|
|
}
|
|
count++
|
|
}
|
|
|
|
a.log.Infof("Found %d unexpired certificates matching the provided key", count)
|
|
|
|
_, err = a.sac.AddBlockedKey(ctx, &sapb.AddBlockedKeyRequest{
|
|
KeyHash: spkiHash[:],
|
|
Added: timestamppb.New(a.clk.Now()),
|
|
Source: "admin-revoker",
|
|
Comment: fmt.Sprintf("%s: %s", u.Username, comment),
|
|
RevokedBy: 0,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("blocking key: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|