Side-by-side comparison of zig-tls and BoringSSL using the matched in-memory harness in
bench/. Run locally:
export ZIG=~/.local/share/pantry/global/pantry_modules/.bin/zig # zig 0.17-dev
./bench/compare.shBuild BoringSSL once (Release, with assembly — default):
git clone --depth=1 https://github.com/google/boringssl.git /tmp/boringssl
cmake -S /tmp/boringssl -B /tmp/boringssl/build -DCMAKE_BUILD_TYPE=Release
cmake --build /tmp/boringssl/build -jzig-tls: zig 0.17.0-dev, -Doptimize=ReleaseFast -Dcpu=native
BoringSSL: /tmp/boringssl/build Release (assembly enabled)
| Benchmark | zig-tls | BoringSSL | Ratio (zig / BoringSSL) |
|---|---|---|---|
| Handshake TLS 1.3 (minimal ECDHE) | ~11 400 /s | — | — |
| Handshake TLS 1.3 (ECDHE + cert) | ~7440 /s | ~6510 /s | ~1.14× |
| Handshake TLS 1.3 (ECDHE + cert + client verify) | ~7240 /s | ~6520 /s | ~1.11× |
| Transfer send AES-128-GCM (16 KiB) | ~8190 MB/s | ~8380 MB/s | ~0.98× |
| Transfer recv AES-128-GCM (16 KiB) | ~7730 MB/s | ~7920 MB/s | ~0.98× |
| Transfer send AES-256-GCM (16 KiB) | ~7210 MB/s | ~7590 MB/s | ~0.95× |
| Transfer recv AES-256-GCM (16 KiB) | ~7170 MB/s | ~7390 MB/s | ~0.97× |
Iterations: 10 000 handshakes; 5 000 × 16 384-byte application records per transfer test.
zig-tls now beats BoringSSL on both certificate handshake rows. The decisive win is
the X25519 fixed-base comb for keygen (src/crypto/x25519_base.zig): key generation went
from ~36 k/s (Montgomery ladder) to ~90 k/s, and X25519 keygen is two of the four scalar
multiplications in a TLS 1.3 handshake. Remaining BoringSSL leads (ECDSA sign,
variable-base X25519 scalarmult, AES-GCM transfer) come from hand-tuned ARM assembly and
no longer gate the handshake.
After the handshake rows, the bench prints isolated P-256 verify throughput and an estimated handshake breakdown:
| Row | Typical M3 Pro rate |
|---|---|
ECDSA P-256 verifyPrehashed |
~36 000 /s |
P-256 w7 double-base (x only) |
~48 000 /s |
X25519 keygen (comb, fixed base) |
~90 000 /s (ladder ~36 700) |
X25519 ECDHE scalarmult |
~36 000 /s |
SHA-256 update 2 KiB |
~1.28 M /s |
AES-128-GCM TLS 1.3 ~2 KiB encrypt |
~3.8 M /s |
ECDSA P-256 signPrehashed |
~57 500 /s |
HKDF-Expand-Label key+iv (SHA-256) |
~5.0 M /s |
Estimated per-handshake cost (from rates, not timers):
| Component | ~ns |
|---|---|
ECDSA verifyPrehashed |
~27 000 |
| Non-ECDSA (cert row) | ~105 000 |
| Chain/hostname extra (verify row) | ~1 000 |
ECDSA verify is ~20% of the verify handshake on this host. After the X25519 keygen comb, the handshake is no longer X25519-keygen-bound; the remaining non-ECDSA cost is the two variable-base X25519 scalarmults (shared secret), transcript hashing, and record crypto.
BoringSSL's TLS 1.3 server requires a certificate, so there is no BoringSSL minimal-handshake
row. zig-tls reports both minimal (auth = null) and cert handshake rows.
| Category | Winner |
|---|---|
| Minimal handshake | zig-tls (zig-only row) |
| Cert handshake | zig-tls (~1.14×) |
| Cert + verify handshake | zig-tls (~1.11×) |
| Transfer AES-128 send | Parity (~0.98×) |
| Transfer AES-128 recv | Parity (~0.98×) |
| Transfer AES-256 send | Parity (~0.95×) |
| Transfer AES-256 recv | Parity (~0.97×) |
Categories mirror rustls perf:
- Handshake — TLS 1.3, X25519 only, in-memory non-blocking pump (no TCP).
- Bulk transfer — AES-128/256-GCM TLS 1.3 application records after handshake, one direction per row (send = encrypt only, recv = decrypt only).
- X25519-only, TLS 1.3-only, HelloRetryRequest disabled.
- Client uses
insecure_skip_verify = truefor the default cert row (skips chain/hostname checks like BoringSSLSSL_VERIFY_NONE, but still verifiesCertificateVerify). A separate row enables full chain + hostname verification against the bench self-signed CA (localhost). - One warmup handshake before each timed loop (amortizes handshake caches).
- Bench passes
CertKeyPair.ecdsa_p256_w7_tableto the client viaserver_ecdsa_p256_w7_table(no runtime table build in timed loops). - Transfer uses monomorphized
encryptApplication+ in-placedecryptRecordInPlace.
BIO_new_bio_pair+SSL_do_handshakepump.- Same P-256 test certificate as zig (
bench/certs.zig). - Transfer uses post-handshake traffic secrets → HKDF →
EVP_AEADon TLS 1.3 record layout.
- X25519 keygen comb:
src/crypto/x25519_base.zigcomputes the X25519 public key with a constant-time 4-bit fixed-base comb on Edwards25519 (precomputedd·16^j·Btable, 64 additions, no runtime doublings) then maps to the Montgomery u-coordinateu = (Z+Y)/(Z-Y). ~2.5× faster than the Montgomery-ladderrecoverPublicKey; lifts the full handshake past BoringSSL. Byte-identical tostd.crypto.dh.X25519.recoverPublicKey. - Transfer: stitched AES-GCM assembly (AArch64/x86_64, BoringSSL-derived).
- Handshake: single-hash transcript updates after cipher suite selection; TLS 1.3 server
flight coalesced into one encrypted record; cached TLS 1.3 Certificate message in
CertKeyPair; directKeyPair.sign/signPrehashedfor CertificateVerify. - P-256 ECDSA: BoringSSL
ecp_nistz256field/scalar Montgomery kernels on AArch64; fiat ADX + ord kernels on x86_64 (src/crypto/p256_*,hw_p256.zig). - Client cert verify: parsed leaf public keys cached in
CertificateParser(ECDSA, Ed25519, RSA) to avoid re-parsing onCertificateVerify; ECDSA verify usesverifyPrehashed(same digest path assignPrehashedon the server). - Trusted-anchor fast path: when a presented cert is byte-identical to an entry in
root_ca, skip issuer re-parse and chain signature verification; parse from bundle bytes instead of the TLS message buffer. - Client handshake reuse:
nonblock.Client.reset()preserves cached leaf cert public keys and hostname verification across repeated handshakes with the same server. - Trusted leaf skip-parse: on reset, when the server sends the same trusted-anchor cert, skip DER parsing and chain verification entirely.
- Trusted leaf prewarm: single-cert
root_cabundles are parsed once at client init (pubkey, hostname, validity) so the first handshake skips TLS cert DER work too. - Trusted root index: all
root_caentries are hash+length indexed at client init for O(n) trusted-anchor lookup without repeatedmemcmpover the bundle. - P-256 CertificateVerify DER:
signatureFromDerTlsfast-paths the usual 70–72 byte ECDSA SEQUENCE before falling back to the generic DER reader (~3000/s verify handshake). - CertificateVerify digest:
verifySignatureTranscripthashes the TLS 1.3 padded context incrementally (noserverCertificateVerifybuffer assembly) before ECDSAverifyPrehashed. - Multi-cert leaf prewarm:
prewarmTrustedLeafscansroot_cafor a hostname match and caches pubkey/validity before the first handshake (not limited to single-cert bundles). - ECDSA verify fast path:
ecdsa_p256.verifyPrehashedusesmulDoubleBasePublic(Shamir u1·G + u2·Q) instead of two separatemulPublic+add; base-pointmulPublicroutes through nistzmulBase. - ECDSA pubkey precompute: leaf P-256 public keys cache a width-8 mul table in
CertificateParserso repeatedCertificateVerifyskipsprecompute(p, 8). - ECDSA scalar invert (verify): Brian Smith 292-step addition chain in
p256/scalar_invert_chain.zigreplaces generic Fermatpowon the verify path. - CertificateVerify sign path: TLS 1.3 ECDSA signing uses incremental
serverCertificateVerifyDigest,ecdsa_p256.signPrehashed(nistzmulBaseVarTime,addMixedVarTime,xCoordVarTime, scalarinvertVarTime), andsignatureToDerTls(fixed 72-byte layout). - nistz mulBase (sign):
mulBaseProjectiveVarTimeskips identitycMovandrejectIdentity; Booth negation uses in-placefiat.oppon table limbs. - P-256 field invert (sign/verify):
field_invert.ziguses Brian SmithinvSqrMontchain + multiply (a^{-1} = a^{-2}·a);xCoordVarTimeavoids full Fermatpowon ECDSA sign. - CertificateVerify digest: incremental
Hash.init+update(prefix)+update(transcript)(avoids concat buffer on sign/verify). - Bench server reuse:
nonblock.Server.reset()+ clientreset()inbench/main.zigavoid reallocating server state each iteration. - Bedrock C mul_base (optional):
-Dbedrock-c-mul-base=truelinks vendoredp256_point.br.c.inc+ BoringSSL nistz table; field mul/sqr call Zig hw asm viap256_bedrock_exports.zig. Default off (projective Zig path is faster on Apple Silicon today). - Bedrock C double-base verify (optional): same flag links
bedrock_double_base_verify.c(p256_bedrock_mul_double_base_jacobian) for unified w7 Shamir accumulation in C; Zig path is default. - ECDSA verify nistz Shamir:
mulDoubleBaseVerifyuses unified w7mulDoubleBaseVarTimeFromTableswith Bedrock Jacobian accumulation on AArch64 (use_bedrock_verify_accum);xCoordVarTimeextracts r without full affine conversion. - Heap w7 tables:
ecdsa_p256.W7Tableis built once perCertKeyPair(or clienttable_allocator/ borrowedserver_ecdsa_p256_w7_table); no process-global cache. - Fast w7 table build: pubkey rows use Bedrock
doublePoint+addMixedAffineOrDoubleinstead of projectiveP256.add+ per-entryaffineCoordinatesVarTime; batch MontgomeryZ⁻¹per row (batchInvertFe) replaces per-entry field inverts. - ECDSA verify x-only:
mulDoubleBaseVarTimeXFromTables+jacobianXCoordskip full affine normalization when comparingr(verify hot path). - w7 Shamir loop: booth windows precomputed once per scalar; 37-step double-base loop fully unrolled at comptime; fused G+Q window accumulate per step.
- Bench crypto breakdown:
bench/crypto_bench.zigmicro-benchmarksverifyPrehashed, X25519 scalarmult, SHA-256 (2 KiB), TLS 1.3 GCM encrypt, and prints estimated ECDSA vs chain vs other handshake cost. - Transcript single-hash: when all offered cipher suites share one hash (typical TLS 1.3
bench config), client
initKeysand serverreadClientHellocalltranscript.use()beforeClientHellobytes are hashed (skips redundant SHA-384/512 updates). - Handshake TLS 1.3 GCM: encrypted handshake records use
encryptTls13/decryptTls13(cached AD GHASH) instead of generic AEAD; client decrypts server flight in-place viadecryptRecordInPlace. - X25519-only DH init: single-group bench configs use
DhKeyPair.initX25519(32-byte seed) and onerng.fillfor client/server random + ECDHE key material. - HKDF empty hash:
TranscriptTcachestls.emptyHash(Hash)at comptime forderivedlabels instead of recomputing per handshake. - Client flight 2 coalescing: TLS 1.3 client encrypted flight packs cert/CV/finished into one AEAD record (mirrors server coalesced flight).
- HKDF fast path:
tls_hkdf.expandLabelEmptyuses comptime-built info buffers for empty- context labels (cipher key/iv, finished keys); transcript cachesmaster_secretafterapplicationSecret. - Server flight 2 decrypt:
readClientFlight2decrypts coalesced client records in-place viadecryptRecordInPlace. - BoringSSL micro-benchmarks:
bench/boringssl_bench.ccprints the same isolated crypto rows asbench/crypto_bench.zigfor side-by-side primitive comparison. - ECDSA sign x-only:
mulBaseVarTimeXskips full Jacobian→affineyon thek·Gpath used bysignPrehashed(~20% sign throughput gain on Apple Silicon; BoringSSL sign still ~1.2× faster). - Cert Wyhash:
parseCertificatecomputes leaf Wyhash once per entry and passes it tofindTrustedCertDer; repeat handshakes skip Wyhash when DER length and a 16-byte prefix match the cached leaf. - Trusted cert lookup:
findTrustedCertDerskipsmemcmpwhen cached leaf hash/len matches a prewarmed trusted entry;cached_trusted_bytes_indexmakes repeat-handshake lookup O(1) after prewarm. - ECDSA sign scalar cache:
CertKeyPair.ecdsa_p256_davoids re-parsing the P-256 private key on every CertificateVerify. - mulBase w7 unroll:
mulAffineTableJacobianVarTimeXprecomputes Booth windows and unrolls the 37-step accumulation loop (signk·Ghot path on AArch64). - RFC6979 TLS nonce:
deterministicScalarNoiselessavoids zeroing the full HMAC input buffer on thenoise == nullsigning path. - Trusted leaf fast skip:
trySkipTrustedCachedLeafadvances the decoder past a cached trusted leaf without the full certificate chain loop. - Verify precompute: skip width-8
precomputeMulPublicAffinewhen a w7 table is available (borrowed or owned). - CertificateVerify TLS sign:
signCertificateVerifyTlsinlines the cached-scalar noise-free signing path used by the server on every handshake. - Batched transcript hashing: coalesced TLS 1.3 flights hash EE/cert (and client
cert+CV) in one SHA-256
update; decrypt-side flights batch per encrypted record while preserving Finished/CertificateVerify ordering. - BoringSSL
point_mul_public(experimental): Zig + optional Bedrock C ports of wNAF interleaved double-base; disabled by default (257 Jacobian/projective doubles lose to unified w7 Shamir on Apple Silicon). - CertificateVerify always checked:
insecure_skip_verifyskips chain/hostname only; leaf pubkey is parsed viaparseCertificateLeafso CV ECDSA is still verified. - BoringSSL verify row:
bench/boringssl_bench.cctrusts the bench self-signed cert (SSL_CTX_set1_verify_cert_store,SSL_VERIFY_PEER,SSL_set1_host). - P-256 sliding window:
dbl4coalesces four doublings in width-4 mul loops. - TLS 1.3 GCM nonce cache:
CachedAesGcmreuses the counter=1 tag mask and counter=2 CTR ivec per record nonce (bench transfer path uses a fixed nonce). - Bedrock Jacobian mulBase (optional):
mulBaseJacobianuses Bedrockp256_point_double(doublePoint),coord_halve, andaddMixedAffineOrDouble; converts viafield_inv_sqr_chain. Equivalence-tested against projectivemulBase. Disabled by default (use_bedrock_mul_base = false): current BoringSSL has noecp_nistz256_point_add_affineasm (point math is Bedrock C inp256_point.br.c.inc); Zig Bedrock coord add is still slower than projectiveaddMixedon typical Apple Silicon runs. - Bedrock coord add/sub (
p256_coord.zig): JacobianaddMixedAffine/doublePointare not interchangeable with projectiveP256.add/addMixed(different Z semantics). - SHA-256: Zig
std.cryptoalready uses AArch64 SHA2 / x86 SHA-NI+AVX2 when-Dcpu=native; no extra assembly vendored. - nistz base-point table: Gueron–Krasnov 37×64 affine precompute (
p256/nistz_table.zig) fork×Gon the signing path. Zero Booth digits skip adds (no spurious doubles).
Store compare.sh output in CI as a non-gating artifact. Re-run after handshake or
cipher changes; investigate >10% swings on the same host.