Allow admin command to block key from a CSR file (#7770)
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.
This commit is contained in:
parent
02685602a2
commit
1fa66781ee
|
|
@ -3,7 +3,9 @@ package main
|
|||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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)")
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue