Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow admin command to block key from a CSR file #7770

Merged
merged 5 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

aarongable marked this conversation as resolved.
Show resolved Hide resolved
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