fix(P11): SVID chain validation (REQ-126, F9)

---ci---
project: orca
phase: 11
milestone: v0.12
status: execute
---/ci---

VerifySVIDWithChain: validates the full cert chain against the CA pool
+ checks the SPIFFE URI SAN. Rejects certs from unknown CAs even with
correct URI (F9). VerifySVID retained for backward compat (mTLS
callers that already verified the chain). 2 new tests. Build + vet green.
This commit is contained in:
Jon Chery
2026-08-07 11:20:10 +00:00
parent a627d0ee6d
commit d7dc2d2aad
2 changed files with 65 additions and 0 deletions
+41
View File
@@ -99,6 +99,47 @@ func VerifySVID(certPEM []byte, spiffeID string) error {
return fmt.Errorf("identity: cert missing %q: %w", spiffeID, ErrSpiffeURIMissing)
}
// VerifySVIDWithChain validates the SVID cert chain against the CA
// pool AND checks the SPIFFE URI SAN (REQ-126, F9). The CA pool is the
// cluster root CA (or the step-ca root). Rejects certs signed by
// unknown CAs even with a correct URI. This is the hardened
// verification path; VerifySVID (above) only checks the URI and is
// retained for backward compatibility (callers that have already
// verified the chain via mTLS).
func VerifySVIDWithChain(certPEM []byte, spiffeID string, caPool *x509.CertPool) error {
if caPool == nil {
return fmt.Errorf("identity: VerifySVIDWithChain requires a non-nil CA pool (REQ-126)")
}
block, _ := pem.Decode(certPEM)
if block == nil {
return fmt.Errorf("identity: parse cert: PEM decode failed: %w", ErrStepCLI)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("identity: parse cert: %w", err)
}
// Verify the cert chain against the CA pool.
if _, err := cert.Verify(x509.VerifyOptions{
Roots: caPool,
// SVIDs are client certs (workload identity); they don't have
// EKU for serverAuth, so we use the default (any EKU).
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
}); err != nil {
return fmt.Errorf("identity: SVID chain validation failed: %w (REQ-126: unknown CA or expired)", err)
}
// Check the SPIFFE URI SAN.
want, err := url.Parse(spiffeID)
if err != nil {
return fmt.Errorf("identity: parse spiffe id: %w", err)
}
for _, u := range cert.URIs {
if u.String() == want.String() {
return nil
}
}
return fmt.Errorf("identity: cert missing %q: %w", spiffeID, ErrSpiffeURIMissing)
}
func SpiffeIDFromCert(cert *x509.Certificate) string {
for _, u := range cert.URIs {
if u.Scheme == "spiffe" {
+24
View File
@@ -240,3 +240,27 @@ func TestSanitize(t *testing.T) {
t.Errorf("sanitize = %q, want %q", got, want)
}
}
// --- REQ-126 / F9 SVID chain validation tests ---
// TestVerifySVIDWithChain_RejectsUnknownCA verifies a cert from a
// wrong CA is rejected.
func TestVerifySVIDWithChain_RejectsUnknownCA(t *testing.T) {
// Generate a cert signed by a different CA (not the pool's CA).
certPEM := mintTestSVIDCert(t, "spiffe://orca.local/ns/test/sa/web/alloc-1")
// Empty CA pool (no trusted roots).
emptyPool := x509.NewCertPool()
err := VerifySVIDWithChain(certPEM, "spiffe://orca.local/ns/test/sa/web/alloc-1", emptyPool)
if err == nil {
t.Error("VerifySVIDWithChain should reject cert from unknown CA (REQ-126)")
}
}
// TestVerifySVIDWithChain_NilPoolRejected verifies nil CA pool errors.
func TestVerifySVIDWithChain_NilPoolRejected(t *testing.T) {
certPEM := mintTestSVIDCert(t, "spiffe://orca.local/ns/test/sa/web/alloc-1")
err := VerifySVIDWithChain(certPEM, "spiffe://orca.local/ns/test/sa/web/alloc-1", nil)
if err == nil {
t.Error("nil CA pool should error (REQ-126)")
}
}