Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7dc2d2aad | |||
| a627d0ee6d |
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"phase": 5,
|
||||
"phase": 10,
|
||||
"stage": "complete",
|
||||
"milestone": "v0.12",
|
||||
"milestone_slug": "security-hardening",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-07T11:03:00Z",
|
||||
"updated_at": "2026-08-07T11:19:00Z",
|
||||
"milestone_complete": false,
|
||||
"previous_milestone": "v0.11",
|
||||
"wave": "B (P06 ACL rewrite, P07 password removal, P08 master key seal) next",
|
||||
"phases_shiped": ["P0","P1","P2","P3","P4","P5"],
|
||||
"tags_shipped": ["v0.11.0","v0.11.1","v0.11.2","v0.11.3","v0.11.4","v0.11.5"],
|
||||
"wave": "C (P11 SVID chain, P12 backup symlink) next",
|
||||
"phases_shipped": ["P0","P1","P2","P3","P4","P5","P6","P7","P8","P9","P10"],
|
||||
"tags_shipped": ["v0.11.0","v0.11.1","v0.11.2","v0.11.3","v0.11.4","v0.11.5","v0.11.6","v0.11.7","v0.11.8","v0.11.9","v0.11.10"],
|
||||
"binding_conditions": ["C-29","C-30","C-31","C-32","C-33","C-34","C-35","C-36","C-37","C-38"],
|
||||
"phase_count": 29,
|
||||
"load_bearing_rule": "R-021"
|
||||
|
||||
@@ -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