# Real-World CA Store Benchmarks
```bash
BENCH_CA_FEATURES=bench-nss,bench-ossl,bench-openssl \
./contrib/ci/local-ci.sh bench-ca-roots
```
These benchmarks test synta against the CA certificate databases that ship in production
operating systems. Unlike the PKITS comparison above, which uses five small identical-format
certs, the CA stores contain hundreds to thousands of certificates from many different CAs,
covering a wide range of DN complexity, extension sets, key types, and DER sizes. They
measure sustained throughput under realistic diversity.
## Mozilla NSS Root Store
180 root CA certificates from Mozilla's `certdata.txt` — the same trust anchor set shipped
by Fedora's `ca-certificates` package and embedded in the Mozilla NSS library. All 180 certs
are self-signed root CAs with diverse key types (RSA 2048/4096, ECDSA P-256/P-384) and DN
structures. The median cert by DER size is "Entrust.net Premium 2048 Secure Server CA"
(1,070 bytes); the benchmark uses this cert for single-certificate and field-access
sub-benchmarks to get stable results that are not sensitive to certificate-size outliers.
## CCADB V4 All Certificate Information
9,898 certificates from the Common CA Database (CCADB), covering the full multi-level
hierarchy used by Mozilla, Chrome, Apple, and Microsoft:
| 0 | 919 | Root CAs (self-signed) |
| 1 | 6,627 | Intermediates issued directly by roots |
| 2 | 2,212 | Two levels deep |
| 3 | 137 | Three levels deep |
| 4 | 3 | Four levels deep |
Intermediate CA certificates tend to have more complex DNs and more extensions than the root
CAs in the Mozilla store. The CCADB median cert is "Bayerische SSL-CA-2014-01" (10,432 bytes).
## ML-DSA Synthetic CA Hierarchy
9,889 certificates generated by `tests/vectors/generate_mldsa_certs.py`, mirroring the
CCADB trust hierarchy with post-quantum signatures. Each CCADB certificate's subject DN
and full extension set are preserved; only the algorithm, key, and signature are replaced
with ML-DSA-65 or ML-DSA-87 (alternating by certificate index across the full run). The
hierarchy depth structure mirrors CCADB:
| 0 | 919 | Root CAs (self-signed) |
| 1 | 6,627 | Intermediates issued directly by roots |
| 2 | 2,212 | Two levels deep |
| 3 | 137 | Three levels deep |
| 4 | 3 | Four levels deep |
Nine CCADB certificates were skipped: OpenSSL's `x509 -x509toreq -copy_extensions copy`
step failed to convert them to CSR form, typically because those certs use non-standard DER
encodings or critical extensions that the `x509toreq` pipeline cannot copy into a
PKCS#10 request. (The failures are in OpenSSL's cert→CSR conversion; synta parses all
9,898 original CCADB certs without error.) This leaves 9,889 of the original 9,898 certs
in the synthetic database.
The median cert by DER size is "TrustCor Basic Secure Site (CA1)" (6,705 bytes). ML-DSA
certs range from 5,530 B to 16,866 B; the distribution is shifted left relative to the
CCADB RSA/ECDSA median (10,432 B) because the smallest CCADB certs (compact root CAs with
few extensions) become the new median position after ML-DSA key replacement enlarges all
certs uniformly.
Run:
```bash
SYNTA_CERT_DB=mldsa cargo bench -p synta-bench --bench ccadb_certs
```
## Throughput Results
| `synta_parse_all` | synta | Mozilla (180 certs) | **87.8 µs** | **2.0 M/sec** |
| `nss_parse_all` | NSS | Mozilla (180 certs) | 1.577 ms | 114 K/sec |
| `openssl_parse_all` | rust-openssl | Mozilla (180 certs) | 3.552 ms | 50.7 K/sec |
| `ossl_parse_all` | ossl | Mozilla (180 certs) | 3.617 ms | 49.8 K/sec |
| `synta_parse_and_access` | synta | Mozilla (180 certs) | **261 µs** | **690 K/sec** |
| `synta_build_trust_chain` | synta | Mozilla (180 certs) | **11.6 µs** | — |
| `synta_parse_all` | synta | CCADB (9,898 certs) | **5.10 ms** | **1.94 M/sec** |
| `nss_parse_all` | NSS | CCADB (9,898 certs) | 106 ms | 93 K/sec |
| `openssl_parse_all` | rust-openssl | CCADB (9,898 certs) | 203 ms | 48.8 K/sec |
| `ossl_parse_all` | ossl | CCADB (9,898 certs) | 214 ms | 46.3 K/sec |
| `synta_parse_and_access` | synta | CCADB (9,898 certs) | **16.1 ms** | **615 K/sec** |
| `synta_parse_roots` | synta | CCADB (919 roots) | **457.7 µs** | **2.01 M/sec** |
| `synta_parse_intermediates` | synta | CCADB (8,979 intermediates) | **4.735 ms** | **1.90 M/sec** |
| `synta_build_dependency_tree` | synta | CCADB (9,898 certs) | **559 µs** | — |
| `synta_parse_all` | synta | ML-DSA synth (9,889 certs) | **5.78 ms** | **1.71 M/sec** |
| `nss_parse_all` | NSS | ML-DSA synth (9,889 certs) | 103 ms | 96.4 K/sec |
| `openssl_parse_all` | rust-openssl | ML-DSA synth (9,889 certs) | 239 ms | 41.4 K/sec |
| `ossl_parse_all` | ossl | ML-DSA synth (9,889 certs) | 256 ms | 38.6 K/sec |
| `synta_parse_and_access` | synta | ML-DSA synth (9,889 certs) | **17.5 ms** | **566 K/sec** |
| `synta_parse_roots` | synta | ML-DSA synth (919 roots) | **463 µs** | **1.98 M/sec** |
| `synta_parse_intermediates` | synta | ML-DSA synth (8,970 ints.) | **5.10 ms** | **1.76 M/sec** |
| `synta_build_dependency_tree` | synta | ML-DSA synth (9,889 certs) | **549 µs** | — |
NSS is **18–21× slower** than synta across all three datasets; rust-openssl is **40–41×
slower** and ossl is **41–44× slower**. All three C-backed libraries successfully parse
ML-DSA certificates (NSS 3.120+ and OpenSSL 3.4+ support ML-DSA natively). NSS's absolute
parse time is nearly identical across CCADB traditional certs (106 ms) and ML-DSA synthetic
certs (103 ms) — confirming that NSS's dominant cost is eager DN formatting at parse time,
which depends on DN attribute count rather than the signature algorithm.
synta's throughput is consistent at ~1.7–2.0 M certs/sec across all three datasets,
confirming linear O(n) scaling.
## Single-Certificate Performance (Hot Cache)
Each benchmark suite includes a `per_cert` sub-benchmark that repeatedly parses a single
median-sized certificate whose DER bytes remain in the L1/L2 cache. This isolates parse
throughput from dataset-iteration overhead and memory access patterns.
| Mozilla `synta_per_cert` | 1,070 B | **487 ns** | **1,611 ns** |
| CCADB `synta_per_cert` | 10,432 B | **520 ns** | **1,634 ns** |
| ML-DSA `synta_per_cert` | 6,705 B | **523 ns** | **1,952 ns** |
The Mozilla cert (1,070 B) parses 7% faster than the CCADB median (10,432 B) because the
larger cert has more bytes in tag+length headers of its extension list and
SubjectPublicKeyInfo — the only parts synta reads at parse time.
## Per-Field Access Latency
Pre-parsed certificate, single field read, no allocation unless noted:
| `issuer_raw` / `subject_raw` | 4.1 / 4.1 ns | 4.2 / 4.1 ns | 4.5 / 4.4 ns | Zero-copy slice |
| `public_key_bytes` / `signature_bytes` | 4.1 / 4.1 ns | 4.2 / 4.2 ns | 4.6 / 4.4 ns | Zero-copy slice |
| `signature_algorithm` / `public_key_algorithm` | 5.9 / 5.4 ns | 5.9 / 5.5 ns | 6.3 / 6.4 ns | OID → `&'static str` |
| `serial_number` | 10.9 ns | 6.8 ns | 7.5 ns | Integer → i64, length-dependent |
| `validity` | 180 ns | 206 ns | 231 ns | Two time-string allocations |
| `issuer_dn` | 401 ns | 224 ns | 246 ns | `format_dn()` → `String` |
| `subject_dn` | 404 ns | 292 ns | 324 ns | `format_dn()` → `String` |
Zero-copy fields (`issuer_raw`, `subject_raw`, `public_key_bytes`, `signature_bytes`) cost
~4–5 ns — the price of reading a pointer and length from a struct field.
`identify_signature_algorithm()` and `identify_public_key_algorithm()` match the OID
component array against a static table and return `&'static str` — no allocation, no string
formatting. The ~5–6 ns cost is a few comparisons and a pointer return.
`validity` (~180–231 ns) allocates two strings: UTCTime and GeneralizedTime are formatted
from their raw DER bytes into owned `String`s.
`format_dn()` is the most variable field: it walks the Name DER bytes, decodes each
SEQUENCE OF SET OF SEQUENCE, looks up each attribute OID by name, and formats the result
into an owned `String`. Cost is proportional to the DN's attribute count and string lengths.
## Trust Hierarchy Construction
- **Mozilla** `build_trust_chain` (11.6 µs): builds a `HashMap<subject_bytes, index>` keyed
on the DER Name bytes pre-extracted from each certificate's `CKA_SUBJECT` entry in
`certdata.txt`. The Name bytes are identical to `issuer_raw.as_bytes()` on any certificate
issued by that CA, so chain lookup requires no re-parsing. 180 entries complete in 11.6 µs
— dominated by hash computation over 10–200 byte keys.
- **CCADB** `build_dependency_tree` (559 µs): builds a `HashMap<sha256_fingerprint, index>`
and resolves each certificate's `Parent SHA-256 Fingerprint` CSV field over 9,898 entries.
The majority of the cost is 9,898 SHA-256 string hash operations; actual certificate
parsing is a small fraction.
- **ML-DSA synthetic** `build_dependency_tree` (549 µs): identical structure to CCADB —
same SHA-256 fingerprint `HashMap`, same parent-resolution logic — over 9,889 entries.
Time is nearly identical to CCADB (549 µs vs 559 µs) because the cost is dominated by
SHA-256 string hashing over the CSV fingerprint values, independent of certificate content
or algorithm.
## Why C Libraries Are Slower
`CERT_NewTempCertificate` (NSS) and OpenSSL's `d2i_X509` perform significantly more work
per certificate than synta:
1. **Eager DN formatting** — NSS formats the issuer and subject Distinguished Names into
internal C strings during `CERT_NewTempCertificate`, even when the caller never reads
them. Distinguished Name formatting is the single most expensive operation in certificate
parsing; doing it unconditionally at parse time accounts for roughly 80% of NSS's total
parse cost. OpenSSL decodes DN structure eagerly as well.
2. **Arena and heap allocation** — each NSS certificate allocates a `PLArena` block and
copies the full DER buffer into it (`copyDER = 1`). OpenSSL allocates from the C heap.
These allocations are additional work beyond decoding.
3. **Library state and locking** — NSS acquires internal locks on every
`CERT_NewTempCertificate` call to update the certificate cache, even when the resulting
certificate is marked as temporary. This serialises concurrent parsing in multi-threaded
applications.
4. **FFI boundary costs** — the `rust-openssl` and `ossl` measurements include the overhead
of crossing from Rust into the C library via `extern "C"` calls and pointer marshalling.
synta defers all of (1): `issuer` and `subject` are stored as `RawDer<'a>` (borrowed byte
spans) and decoded only when the caller calls `format_dn()`. There is no locking, no arena,
and no FFI boundary.