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 (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -26,9 +28,14 @@ import (
|
||||||
type subcommandBlockKey struct {
|
type subcommandBlockKey struct {
|
||||||
parallelism uint
|
parallelism uint
|
||||||
comment string
|
comment string
|
||||||
privKey string
|
|
||||||
spkiFile string
|
privKey string
|
||||||
certFile string
|
spkiFile string
|
||||||
|
certFile string
|
||||||
|
csrFile string
|
||||||
|
csrFileExpectedCN string
|
||||||
|
|
||||||
|
checkSignature bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ subcommand = (*subcommandBlockKey)(nil)
|
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.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.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.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 {
|
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 != "",
|
"-private-key": s.privKey != "",
|
||||||
"-spki-file": s.spkiFile != "",
|
"-spki-file": s.spkiFile != "",
|
||||||
"-cert-file": s.certFile != "",
|
"-cert-file": s.certFile != "",
|
||||||
|
"-csr-file": s.csrFile != "",
|
||||||
}
|
}
|
||||||
maps.DeleteFunc(setInputs, func(_ string, v bool) bool { return !v })
|
maps.DeleteFunc(setInputs, func(_ string, v bool) bool { return !v })
|
||||||
if len(setInputs) == 0 {
|
if len(setInputs) == 0 {
|
||||||
|
|
@ -75,6 +87,8 @@ func (s *subcommandBlockKey) Run(ctx context.Context, a *admin) error {
|
||||||
spkiHashes, err = a.spkiHashesFromFile(s.spkiFile)
|
spkiHashes, err = a.spkiHashesFromFile(s.spkiFile)
|
||||||
case "-cert-file":
|
case "-cert-file":
|
||||||
spkiHashes, err = a.spkiHashesFromCertPEM(s.certFile)
|
spkiHashes, err = a.spkiHashesFromCertPEM(s.certFile)
|
||||||
|
case "-csr-file":
|
||||||
|
spkiHashes, err = a.spkiHashFromCSRPEM(s.csrFile, s.checkSignature, s.csrFileExpectedCN)
|
||||||
default:
|
default:
|
||||||
return errors.New("no recognized input method flag set (this shouldn't happen)")
|
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
|
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 {
|
func (a *admin) blockSPKIHashes(ctx context.Context, spkiHashes [][]byte, comment string, parallelism uint) error {
|
||||||
u, err := user.Current()
|
u, err := user.Current()
|
||||||
if err != nil {
|
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
|
// mockSARecordingBlocks is a mock which only implements the AddBlockedKey gRPC
|
||||||
// method.
|
// method.
|
||||||
type mockSARecordingBlocks struct {
|
type mockSARecordingBlocks struct {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue