# X.509 Path Validation
`synta-x509-verification` implements RFC 5280 §6 certificate path validation
and the CA/Browser Forum Baseline Requirements. It is crypto-agnostic: all
signature verification is delegated to a caller-supplied `SignatureVerifier`.
## Dependency
With OpenSSL (default):
```toml
[dependencies]
synta-x509-verification = "0.1"
synta-certificate = "0.1"
synta = "0.1"
```
With NSS instead of OpenSSL:
```toml
[dependencies]
synta-x509-verification = { version = "0.1", default-features = false, features = ["nss"] }
synta-certificate = { version = "0.1", default-features = false, features = ["std", "derive", "nss"] }
synta = "0.1"
```
## Quick start: TLS server verification
The simplest approach uses `default_signature_verifier()`, which automatically selects
the NSS or OpenSSL backend based on which feature is active:
```rust,ignore
use std::time::{SystemTime, UNIX_EPOCH};
use synta::{Decoder, Encoding};
use synta_certificate::{Certificate, default_signature_verifier};
use synta_x509_verification::{
ops::VerificationCertificate,
policy::{PolicyDefinition, Subject},
trust_store::Store,
types::DNSName,
verify, RevocationChecks,
};
fn parse<'a>(der: &'a [u8]) -> Certificate<'a> {
Decoder::new(der, Encoding::Der).decode().unwrap()
}
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let root_der = std::fs::read("root.der")?;
let intermediate_der = std::fs::read("intermediate.der")?;
let leaf_der = std::fs::read("leaf.der")?;
let root = VerificationCertificate::new(parse(&root_der), &root_der);
let intermediate = VerificationCertificate::new(parse(&intermediate_der), &intermediate_der);
let leaf = VerificationCertificate::new(parse(&leaf_der), &leaf_der);
let store = Store::new([root]);
let hostname = DNSName::new("example.com").unwrap();
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
// default_signature_verifier() selects NSS when the nss feature is active,
// otherwise falls back to the OpenSSL verifier.
let verifier = default_signature_verifier();
let policy = PolicyDefinition::new_server(
verifier,
vec![Subject::Dns(hostname)],
now,
);
let intermediates = [intermediate];
let chain = verify(&leaf, &intermediates, &policy, &store, RevocationChecks::default())?;
println!("Verified chain of {} certificates", chain.len());
Ok(())
}
```
To pin to a specific backend type, use `OpensslSignatureVerifier` or
`NssSignatureVerifier` directly:
```rust,ignore
use synta_certificate::OpensslSignatureVerifier; // requires openssl feature
// or:
use synta_certificate::NssSignatureVerifier; // requires nss feature
let policy = PolicyDefinition::new_server(OpensslSignatureVerifier, subjects, now);
```
## Validation flow
```mermaid
flowchart TD
A([verify leaf · intermediates · policy · store · revocation])
B["Build chain<br/>leaf → intermediates → trust anchor"]
C["Verify each signature<br/>SignatureVerifier trait"]
D{Signatures OK?}
E["Apply policy<br/>EKU · validity · key size · profile"]
F{Policy OK?}
G["Name constraints<br/>dNSName · iPAddress · rfc822Name · directoryName"]
H{Constraints OK?}
I["Revocation<br/>CRL and/or OCSP — soft-fail on missing response"]
J{Revoked?}
K([Ok — chain])
L([Err — ValidationError])
A --> B --> C --> D
D -->|yes| E
D -->|no| L
E --> F
F -->|ok| G
F -->|fail| L
G --> H
H -->|ok| I
H -->|fail| L
I --> J
J -->|no| K
J -->|yes| L
classDef ok fill:#16a34a,stroke:#14532d,color:#ffffff
classDef err fill:#dc2626,stroke:#7f1d1d,color:#ffffff
class K ok
class L err
```
## Policy presets
| `PolicyDefinition::new_server(ops, subjects, time)` | serverAuth | CABF classical |
| `PolicyDefinition::new_client(ops, time)` | clientAuth | CABF classical |
| `PolicyDefinition::new_server_pq(ops, subjects, time)` | serverAuth | CABF + ML-DSA/ML-KEM |
| `PolicyDefinition::new_client_pq(ops, time)` | clientAuth | CABF + ML-DSA/ML-KEM |
## Validation profiles
`ValidationProfile` controls which compliance rules apply:
| NameConstraints criticality | enforced regardless of flag | MUST be critical |
| EKU in EE cert | MUST be present | absent = any purpose |
| CA cert in leaf position | rejected | permitted |
```rust,ignore
use synta_x509_verification::ValidationProfile;
let mut policy = PolicyDefinition::new_server(MyVerifier, subjects, now);
policy.profile = ValidationProfile::Rfc5280;
```
## Policy fields
All fields on `PolicyDefinition` are public:
```rust,ignore
use synta_certificate::OpensslSignatureVerifier;
use synta_x509_verification::policy::PolicyDefinition;
let now = 0i64; // Unix timestamp
let mut policy = PolicyDefinition::new_server(OpensslSignatureVerifier, vec![], now);
policy.max_chain_depth = 3; // at most 3 intermediate CAs
policy.minimum_rsa_modulus = 3072; // require RSA-3072+
policy.extended_key_usage = None; // skip EKU check
```
## Custom crypto backend
```rust,ignore
use synta_certificate::SignatureVerifier;
struct MyVerifier;
impl SignatureVerifier for MyVerifier {
type Error = Box<dyn std::error::Error + Send + Sync>;
fn verify_certificate_signature(
&self,
tbs_der: &[u8],
sig_alg_der: &[u8],
signature_bits: &[u8],
issuer_spki_der: &[u8],
) -> Result<(), Self::Error> {
// verify tbs_der using issuer_spki_der
Ok(())
}
}
```
## Revocation checking
```rust,ignore
use synta_x509_verification::{CrlStore, OcspStore, RevocationChecks, verify};
// No revocation
let chain = verify(&leaf, &intermediates, &policy, &store, RevocationChecks::default())?;
// CRL revocation
let mut crl_store = CrlStore::new();
crl_store.add_der(std::fs::read("issuing-ca.crl")?);
let chain = verify(&leaf, &intermediates, &policy, &store, RevocationChecks {
crls: Some(&crl_store),
ocsp: None,
})?;
// OCSP revocation
let mut ocsp_store = OcspStore::new();
ocsp_store.add_der(std::fs::read("leaf-ocsp.der")?);
let chain = verify(&leaf, &intermediates, &policy, &store, RevocationChecks {
crls: None,
ocsp: Some(&ocsp_store),
})?;
```
Revocation semantics: **soft-fail** when no matching CRL/OCSP response is found;
**hard-fail** only when a positively revoked entry is found.
## Name constraints
Four GeneralName types are evaluated:
| `dNSName` | Leading-dot optional; wildcards (`*.example.com`) supported |
| `iPAddress` | CIDR range; 8 bytes (IPv4) or 32 bytes (IPv6) |
| `rfc822Name` | Exact, OnDomain (`@domain`), or InDomain (`.domain`) |
| `directoryName` | DER byte prefix match |
Other alternatives (URI, OtherName) cause a validation error if they appear in a
NameConstraints extension. A budget of 65 536 constraint checks per call
prevents denial-of-service.
## Post-quantum algorithm support
```rust,ignore
let policy = PolicyDefinition::new_server_pq(
MyVerifier,
vec![Subject::Dns(DNSName::new("example.com").unwrap())],
now_unix,
);
```
Or extend an existing policy manually:
```rust,ignore
use synta_x509_verification::{
WEBPKI_PERMITTED_SPKI_ALGORITHMS_WITH_PQ,
WEBPKI_PERMITTED_SIGNATURE_ALGORITHMS_WITH_PQ,
};
policy.permitted_spki_algorithms = WEBPKI_PERMITTED_SPKI_ALGORITHMS_WITH_PQ;
policy.permitted_signature_algorithms = WEBPKI_PERMITTED_SIGNATURE_ALGORITHMS_WITH_PQ;
```
ML-DSA and ML-KEM checks enforced automatically when a PQ algorithm appears in
the allowlist: parameter-absence, exact public-key-size validation, and KeyUsage
bit enforcement per RFC 9881 (ML-DSA) and RFC 9935 (ML-KEM).
## Error types
```rust,ignore
use synta_x509_verification::{ValidationError, ValidationErrorKind};
match err.kind {
ValidationErrorKind::CandidatesExhausted(inner) => { /* all issuer paths exhausted */ }
ValidationErrorKind::ExtensionError { oid, reason } => { /* extension policy failure */ }
ValidationErrorKind::FatalError(msg) => { /* budget exceeded or hard limit */ }
ValidationErrorKind::Other(msg) => { /* other validation failure */ }
}
```
## x509-limbo compliance
The crate is tested against the [x509-limbo](https://github.com/trailofbits/x509-limbo)
test suite with 9 747 tests passing. Run locally:
```shell
bash synta-x509-verification/tests/limbo/fetch.sh
cargo test -p synta-x509-verification --test limbo -- --nocapture
```
## Known limitations
- OCSP CertID issuer hashes are not recomputed; matching uses `serialNumber` +
signature verification.
- Name constraints: URI, OtherName, and other GeneralName alternatives are not
evaluated.
- No PKIX policy mapping (Certificate Policies, Policy Mappings, etc.).
- Trust store uses byte-exact DER comparison rather than RFC 5280 DN matching
algorithm.