Skip to content

Commit

Permalink
Allow admin command to block key from a CSR file (#7770)
Browse files Browse the repository at this point in the history
One format we receive key compromise reports is as a CSR file. For
example, from https://pwnedkeys.com/revokinator

This allows the admin command to block a key from a CSR directly,
instead of needing to validate it manually and get the SPKI or key from
it.

I've added a flag (default true) to check the signature on the CSR, in
case we ever decide we want to block a key from a CSR with a bad
signature for whatever reason.
  • Loading branch information
mcpherrinm authored Nov 4, 2024
1 parent 0268560 commit 1fa6678
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 3 deletions.
57 changes: 54 additions & 3 deletions cmd/admin/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package main
import (
"bufio"
"context"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"flag"
"fmt"
Expand All @@ -26,9 +28,14 @@ import (
type subcommandBlockKey struct {
parallelism uint
comment string
privKey string
spkiFile string
certFile string

privKey string
spkiFile string
certFile string
csrFile string
csrFileExpectedCN string

checkSignature bool
}

var _ subcommand = (*subcommandBlockKey)(nil)
Expand All @@ -46,6 +53,10 @@ func (s *subcommandBlockKey) Flags(flag *flag.FlagSet) {
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 {
Expand All @@ -56,6 +67,7 @@ func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
"-private-key": s.privKey != "",
"-spki-file": s.spkiFile != "",
"-cert-file": s.certFile != "",
"-csr-file": s.csrFile != "",
}
maps.DeleteFunc(setInputs, func(_ string, v bool) bool { return !v })
if len(setInputs) == 0 {
Expand All @@ -75,6 +87,8 @@ func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
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)")
}
Expand Down Expand Up @@ -146,6 +160,43 @@ func (a *admin) spkiHashesFromCertPEM(filename string) ([][]byte, error) {
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 {
Expand Down
47 changes: 47 additions & 0 deletions cmd/admin/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,53 @@ func TestSPKIHashesFromFile(t *testing.T) {
}
}

// The key is the p256 test key from RFC9500
const goodCSR = `
-----BEGIN CERTIFICATE REQUEST-----
MIG6MGICAQAwADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEIlSPiPt4L/teyj
dERSxyoeVY+9b3O+XkjpMjLMRcWxbEzRDEy41bihcTnpSILImSVymTQl9BQZq36Q
pCpJQnKgADAKBggqhkjOPQQDAgNIADBFAiBadw3gvL9IjUfASUTa7MvmkbC4ZCvl
21m1KMwkIx/+CQIhAKvuyfCcdZ0cWJYOXCOb1OavolWHIUzgEpNGUWul6O0s
-----END CERTIFICATE REQUEST-----
`

// TestCSR checks that we get the correct SPKI from a CSR, even if its signature is invalid
func TestCSR(t *testing.T) {
expectedSPKIHash := "b2b04340cfaee616ec9c2c62d261b208e54bb197498df52e8cadede23ac0ba5e"

goodCSRFile := path.Join(t.TempDir(), "good.csr")
err := os.WriteFile(goodCSRFile, []byte(goodCSR), 0600)
test.AssertNotError(t, err, "writing good csr")

a := admin{log: blog.NewMock()}

goodHash, err := a.spkiHashFromCSRPEM(goodCSRFile, true, "")
test.AssertNotError(t, err, "expected to read CSR")

if len(goodHash) != 1 {
t.Fatalf("expected to read 1 SPKI from CSR, read %d", len(goodHash))
}
test.AssertEquals(t, hex.EncodeToString(goodHash[0]), expectedSPKIHash)

// Flip a bit, in the signature, to make a bad CSR:
badCSR := strings.Replace(goodCSR, "Wul6", "Wul7", 1)

csrFile := path.Join(t.TempDir(), "bad.csr")
err = os.WriteFile(csrFile, []byte(badCSR), 0600)
test.AssertNotError(t, err, "writing bad csr")

_, err = a.spkiHashFromCSRPEM(csrFile, true, "")
test.AssertError(t, err, "expected invalid signature")

badHash, err := a.spkiHashFromCSRPEM(csrFile, false, "")
test.AssertNotError(t, err, "expected to read CSR with bad signature")

if len(badHash) != 1 {
t.Fatalf("expected to read 1 SPKI from CSR, read %d", len(badHash))
}
test.AssertEquals(t, hex.EncodeToString(badHash[0]), expectedSPKIHash)
}

// mockSARecordingBlocks is a mock which only implements the AddBlockedKey gRPC
// method.
type mockSARecordingBlocks struct {
Expand Down

0 comments on commit 1fa6678

Please sign in to comment.