← Week 3: Integration and Application

Day 17: Validating X.509-SVIDs in Rust

Phase 4 · August 21, 2026

← Week 3: Integration and Application

Agenda (2–3 hours)

  • Read (30 min): X.509-SVID spec §4 (validation); SPIFFE Trust Domain spec §4
  • Build (120 min): A complete SVID validator using x509-parser + trust bundle
  • Test (30 min): Validate real SVIDs from your running SPIRE deployment
← Week 3: Integration and Application

SVID Validation Requirements (from spec)

To validate an X.509-SVID, a relying party MUST:

  1. Validate the cert chain against the trust bundle for the SVID's trust domain
  2. Check the validity periodnotBefore ≤ now ≤ notAfter
  3. Verify exactly one URI SAN with scheme spiffe://
  4. Verify the trust domain in the SPIFFE ID matches the expected trust domain
  5. Verify the CA flag — leaf SVIDs MUST have CA:false
  6. Verify key usage — MUST include digitalSignature

Optionally: check the path against an authorization policy.

← Week 3: Integration and Application

The Validator Structure

// spiffe-demo/src/validate.rs

use x509_parser::prelude::*;

#[derive(Debug)]
pub enum SvidValidationError {
    ExpiredCert,
    NoSpiffeId,
    MultipleSpiffeIds,
    WrongTrustDomain { expected: String, got: String },
    ChainVerificationFailed(String),
    CaFlagSet,
    MissingDigitalSignature,
}

pub struct SvidValidator {
    pub trust_domain: String,
    pub trust_bundle: Vec<Vec<u8>>,  // CA cert DERs
}

impl SvidValidator {
    pub fn validate(&self, cert_der: &[u8]) -> Result<String, SvidValidationError> {
        // Returns the validated SPIFFE ID on success
    }
}
← Week 3: Integration and Application

Implementing the Validator

impl SvidValidator {
    pub fn validate(&self, cert_der: &[u8]) -> Result<String, SvidValidationError> {
        let (_, cert) = X509Certificate::from_der(cert_der)
            .map_err(|e| SvidValidationError::ChainVerificationFailed(e.to_string()))?;
        
        // 1. Validity period
        // x509-parser: cert.validity().is_valid() checks against current time
        if !cert.validity().is_valid() {
            return Err(SvidValidationError::ExpiredCert);
        }
        
        // 2. CA flag
        if cert.is_ca() {
            return Err(SvidValidationError::CaFlagSet);
        }
        
        // 3. Key usage: digitalSignature
        if let Some(ku) = cert.key_usage()? {
            if !ku.value.digital_signature() {
                return Err(SvidValidationError::MissingDigitalSignature);
            }
        }
        
        // 4. Extract SPIFFE ID from URI SAN
        let spiffe_id = self.extract_and_validate_spiffe_id(&cert)?;
        
        // 5. Chain verification (delegate to trust bundle)
        self.verify_chain(cert_der)?;
        
        Ok(spiffe_id)
    }
}
← Week 3: Integration and Application

Extracting the SPIFFE ID

fn extract_and_validate_spiffe_id(
    &self,
    cert: &X509Certificate<'_>,
) -> Result<String, SvidValidationError> {
    use x509_parser::extensions::GeneralName;
    
    let mut spiffe_ids: Vec<String> = Vec::new();
    
    if let Ok(Some(san_ext)) = cert.subject_alternative_names() {
        for name in &san_ext.value.general_names {
            if let GeneralName::URI(uri) = name {
                if uri.starts_with("spiffe://") {
                    spiffe_ids.push(uri.to_string());
                }
            }
        }
    }
    
    match spiffe_ids.len() {
        0 => return Err(SvidValidationError::NoSpiffeId),
        2.. => return Err(SvidValidationError::MultipleSpiffeIds),
        _ => {}
    }
    
    let id = &spiffe_ids[0];
    // Parse: spiffe://<trust-domain>/<path>
    let trust_domain = id.strip_prefix("spiffe://")
        .and_then(|s| s.split('/').next())
        .unwrap_or("");
    
    if trust_domain != self.trust_domain {
        return Err(SvidValidationError::WrongTrustDomain {
            expected: self.trust_domain.clone(),
            got: trust_domain.to_string(),
        });
    }
    
    Ok(id.clone())
}
← Week 3: Integration and Application

Chain Verification

The trust bundle is a set of CA certs. To verify the chain:

fn verify_chain(&self, cert_der: &[u8]) -> Result<(), SvidValidationError> {
    // x509-parser doesn't do full chain verification directly.
    // Options:
    // 1. Use openssl bindings (openssl crate) — verify_cert()
    // 2. Use webpki/rustls-webpki for chain verification
    // 3. Manual: verify each cert's signature against the CA above it
    
    // For this exercise: use rustls-webpki
    use webpki::{EndEntityCert, TrustAnchor, ECDSA_P256_SHA256};
    
    let trust_anchors: Vec<TrustAnchor> = self.trust_bundle.iter()
        .filter_map(|der| TrustAnchor::try_from_cert_der(der).ok())
        .collect();
    
    let cert = EndEntityCert::try_from(cert_der.as_ref())
        .map_err(|e| SvidValidationError::ChainVerificationFailed(e.to_string()))?;
    
    // verify_for_usage checks validity + chain, not hostname
    // For SVIDs there's no hostname to verify — only chain + SPIFFE ID
    // ...
    Ok(())
}

Note: chain verification for SVIDs without hostname checking is a gap in webpki.
Document this gap and use openssl bindings or a manual approach.

← Week 3: Integration and Application

Challenge Assignment

Implement SvidValidator with all checks from the spec:

  1. Validity period check
  2. CA flag check
  3. Key usage check (digitalSignature)
  4. SPIFFE ID extraction — must be exactly one URI SAN starting with spiffe://
  5. Trust domain match
  6. Chain verification against the trust bundle

Test it against:

  • A valid SVID from your running SPIRE deployment
  • A tampered SVID (flip a byte in the cert — should fail chain verification)
  • An SVID with an expired notAfter (create one with ttl 1s in SPIRE registration)
  • A regular TLS cert from toy-pki — should fail (no SPIFFE URI SAN)

Add a spiffe-demo validate <cert.pem> subcommand.
Print which check failed and why when validation fails.

← Week 3: Integration and Application

Resources

  • X.509-SVID spec §4: github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md
  • x509-parser SAN extraction: docs.rs/x509-parser — SubjectAlternativeName
  • webpki / rustls-webpki: docs.rs/webpki — chain verification
  • openssl Rust crate: docs.rs/openssl — X509::verify, X509StoreBuilder