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:
@@ -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" {
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user