From b491ae3de00f620da68b31f6ba064dc0f263099f Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Thu, 28 May 2026 17:36:20 +0000 Subject: [PATCH 1/2] test(pilot-ca): verify loadRoot rejects non-CA root cert (PILOT-139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add TestLoadRoot_NotCA: generates a self-signed leaf-like cert (IsCA=false), writes it as root.crt, and asserts loadRoot returns an error. Currently fails — loadRoot does not validate IsCA. --- zz_load_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/zz_load_test.go b/zz_load_test.go index d90c159..92f19ea 100644 --- a/zz_load_test.go +++ b/zz_load_test.go @@ -3,12 +3,19 @@ package main import ( + "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" "os" "path/filepath" "strings" "testing" + "time" ) // TestLoadRoot_MissingCert covers the os.ReadFile error branch. @@ -145,6 +152,59 @@ func TestIssueBeacon_HappyPath(t *testing.T) { } } +// TestLoadRoot_NotCA verifies loadRoot rejects a self-signed cert +// that does not have IsCA set. This is the PILOT-139 fix. +func TestLoadRoot_NotCA(t *testing.T) { + t.Parallel() + dir := t.TempDir() + + // Generate a leaf-like self-signed cert (IsCA=false). + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatalf("GenerateKey: %v", err) + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + t.Fatalf("serial: %v", err) + } + now := time.Now().UTC() + tmpl := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: "test"}, + NotBefore: now, + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + IsCA: false, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatalf("CreateCertificate: %v", err) + } + crtPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + + // Also marshal the key — loadRoot needs both files. + keyDER, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + t.Fatalf("MarshalPKCS8: %v", err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) + + if err := os.WriteFile(filepath.Join(dir, "root.crt"), crtPEM, 0644); err != nil { + t.Fatalf("write crt: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "root.key"), keyPEM, 0600); err != nil { + t.Fatalf("write key: %v", err) + } + + _, _, err = loadRoot(dir) + if err == nil { + t.Error("loadRoot should reject a non-CA root cert") + } + if err != nil && !strings.Contains(err.Error(), "IsCA") { + t.Errorf("err = %v, want IsCA", err) + } +} + // TestIssueBeacon_RequiresHostname covers the empty-hostname branch. func TestIssueBeacon_RequiresHostname(t *testing.T) { t.Parallel() From a5f87091239779fd7b6fda9b868452e63bd10d82 Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Thu, 28 May 2026 17:36:20 +0000 Subject: [PATCH 2/2] fix(pilot-ca): validate IsCA and BasicConstraints in loadRoot (PILOT-139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadRoot parses root.crt and uses it as signing root for issueBeacon without checking that the certificate is a CA. A swapped root.crt (self-signed non-CA) would silently produce invalid cert chains rejected by compliant TLS verifiers. Add two guards after x509.ParseCertificate: - !crt.IsCA → error - !crt.BasicConstraintsValid → error Fixes PILOT-139. --- main.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.go b/main.go index 870b007..1b0e641 100644 --- a/main.go +++ b/main.go @@ -296,6 +296,12 @@ func loadRoot(rootDir string) (*x509.Certificate, any, error) { if err != nil { return nil, nil, fmt.Errorf("root.crt parse: %w", err) } + if !crt.IsCA { + return nil, nil, fmt.Errorf("root.crt: certificate is not a CA (IsCA=false)") + } + if !crt.BasicConstraintsValid { + return nil, nil, fmt.Errorf("root.crt: basic constraints missing or invalid") + } keyBlock, _ := pem.Decode(keyPEM) if keyBlock == nil { return nil, nil, fmt.Errorf("root.key: invalid PEM")