Skip to content
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
39 changes: 27 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
// Validity: 90 days. Re-run before expiry; Caddy reloads
// automatically on file change.
//
// pilot-ca verify <root.crt> <leaf.crt>
// Confirm a leaf cert chains to the root and is currently valid.
// pilot-ca verify <root.crt> <leaf.crt> [hostname]
// Confirm a leaf cert chains to the root, is currently valid,
// and (when hostname is given) that the leaf SAN matches hostname.
// Exit 0 on success.
//
// The root cert (PEM) is what gets embedded in pilot-daemon via
Expand Down Expand Up @@ -87,7 +88,7 @@ func main() {
fmt.Fprintln(os.Stderr, "usage: pilot-ca <subcommand> [args...]")
fmt.Fprintln(os.Stderr, " init-root <out-dir>")
fmt.Fprintln(os.Stderr, " issue-beacon <root-dir> <hostname> <out-dir>")
fmt.Fprintln(os.Stderr, " verify <root.crt> <leaf.crt>")
fmt.Fprintln(os.Stderr, " verify <root.crt> <leaf.crt> [hostname]")
}
flag.Parse()
args := flag.Args()
Expand All @@ -113,11 +114,16 @@ func main() {
die("issue-beacon: %v", err)
}
case "verify":
if len(args) != 3 {
hostname := ""
if len(args) == 3 {
// verify <root.crt> <leaf.crt> — chain-only (backward compat)
} else if len(args) == 4 {
hostname = args[3]
} else {
flag.Usage()
os.Exit(2)
}
if err := verifyChain(args[1], args[2]); err != nil {
if err := verifyChain(args[1], args[2], hostname); err != nil {
die("verify: %v", err)
}
default:
Expand Down Expand Up @@ -241,10 +247,11 @@ func issueBeacon(rootDir, hostname, outDir string) error {
}

// verifyChain confirms a leaf cert chains to a root cert and is valid
// for the current wall clock. Used in CI before bundling a leaf into
// a beacon deployment, and as a sanity check in install/upgrade
// scripts.
func verifyChain(rootCrtPath, leafCrtPath string) error {
// for the current wall clock. When hostname is non-empty, the leaf's
// SAN is also checked against hostname via x509.VerifyOptions.DNSName.
// Used in CI before bundling a leaf into a beacon deployment, and as a
// sanity check in install/upgrade scripts.
func verifyChain(rootCrtPath, leafCrtPath, hostname string) error {
rootPEM, err := os.ReadFile(rootCrtPath)
if err != nil {
return fmt.Errorf("read root cert: %w", err)
Expand All @@ -265,14 +272,22 @@ func verifyChain(rootCrtPath, leafCrtPath string) error {
if err != nil {
return fmt.Errorf("leaf parse: %w", err)
}
if _, err := leaf.Verify(x509.VerifyOptions{
opts := x509.VerifyOptions{
Roots: pool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}); err != nil {
}
if hostname != "" {
opts.DNSName = hostname
}
if _, err := leaf.Verify(opts); err != nil {
return fmt.Errorf("verify failed: %w", err)
}
fmt.Printf("OK — %s chains to %s\n", leafCrtPath, rootCrtPath)
fmt.Printf(" CN=%s not-after=%s\n", leaf.Subject.CommonName, leaf.NotAfter.Format(time.RFC3339))
if hostname != "" {
fmt.Printf(" hostname=%s CN=%s not-after=%s\n", hostname, leaf.Subject.CommonName, leaf.NotAfter.Format(time.RFC3339))
} else {
fmt.Printf(" CN=%s not-after=%s\n", leaf.Subject.CommonName, leaf.NotAfter.Format(time.RFC3339))
}
return nil
}

Expand Down
6 changes: 3 additions & 3 deletions zz_branches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestVerifyChain_MissingLeafFile(t *testing.T) {
if err := initRoot(rootDir); err != nil {
t.Fatalf("initRoot: %v", err)
}
err := verifyChain(filepath.Join(rootDir, "root.crt"), "/nonexistent/leaf.crt")
err := verifyChain(filepath.Join(rootDir, "root.crt"), "/nonexistent/leaf.crt", "")
if err == nil || !strings.Contains(err.Error(), "read leaf cert") {
t.Errorf("err = %v; want 'read leaf cert'", err)
}
Expand All @@ -95,7 +95,7 @@ func TestVerifyChain_RootPEMAppendFails(t *testing.T) {
if err := os.WriteFile(leafPath, []byte("garbage"), 0o644); err != nil {
t.Fatalf("write leaf: %v", err)
}
err := verifyChain(rootPath, leafPath)
err := verifyChain(rootPath, leafPath, "")
if err == nil || !strings.Contains(err.Error(), "root cert PEM parse failed") {
t.Errorf("err = %v; want 'root cert PEM parse failed'", err)
}
Expand All @@ -114,7 +114,7 @@ func TestVerifyChain_LeafParseError(t *testing.T) {
if err := os.WriteFile(leafPath, []byte(garbageLeaf), 0o644); err != nil {
t.Fatalf("write leaf: %v", err)
}
err := verifyChain(filepath.Join(rootDir, "root.crt"), leafPath)
err := verifyChain(filepath.Join(rootDir, "root.crt"), leafPath, "")
if err == nil || !strings.Contains(err.Error(), "leaf parse") {
t.Errorf("err = %v; want 'leaf parse'", err)
}
Expand Down
6 changes: 3 additions & 3 deletions zz_pilot_ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func TestVerifyChain_AcceptsValid(t *testing.T) {
if err := issueBeacon(rootDir, host, leafDir); err != nil {
t.Fatalf("issueBeacon: %v", err)
}
if err := verifyChain(filepath.Join(rootDir, "root.crt"), filepath.Join(leafDir, host+".crt")); err != nil {
if err := verifyChain(filepath.Join(rootDir, "root.crt"), filepath.Join(leafDir, host+".crt"), host); err != nil {
t.Errorf("verifyChain rejected valid chain: %v", err)
}
}
Expand All @@ -223,7 +223,7 @@ func TestVerifyChain_RejectsUnrelatedRoot(t *testing.T) {
t.Fatalf("issueBeacon: %v", err)
}
// Leaf was signed by rootA, but we verify against rootB.
err := verifyChain(filepath.Join(rootB, "root.crt"), filepath.Join(leafDir, host+".crt"))
err := verifyChain(filepath.Join(rootB, "root.crt"), filepath.Join(leafDir, host+".crt"), host)
if err == nil {
t.Fatal("verifyChain accepted leaf signed by a different root; expected rejection")
}
Expand All @@ -237,7 +237,7 @@ func TestVerifyChain_RejectsBadPEM(t *testing.T) {
if err := os.WriteFile(junk, []byte("not a pem"), 0o644); err != nil {
t.Fatalf("seed junk: %v", err)
}
err := verifyChain(junk, junk)
err := verifyChain(junk, junk, "")
if err == nil {
t.Fatal("verifyChain accepted garbage PEM; expected rejection")
}
Expand Down
Loading