zigtls is a Zig-first TLS termination library for load balancers and edge proxies.
It is built to be imported by other Zig projects and to run behind production-style release gates (strict interop, BoGo profile checks, timing/perf assertions, and reliability drills).
- TLS termination primitives for event-loop integrations (
zigtls.termination) - Non-blocking adapter for transport I/O loops (
zigtls.adapter) - Cert reload, ticket key management, metrics, and handshake policy hooks
- Strict validation tooling and reproducible evidence scripts under
scripts/releaseandscripts/interop
- Zig minimum version:
0.15.2(build.zig.zon) - Current library version string:
0.1.0-dev(src/root.zig)
Add this repository as a Zig dependency, then import zigtls from your build graph.
You can add zigtls in your project like below command.
zig fetch --save https://github.com/Geun-Oh/zigtls/releases/download/v0.1.2/zigtls-v0.1.2.tar.gzThan it will be added into your build.zig.zon dependency entry (replace tag/hash with your pinned release):
.dependencies = .{
.zigtls = .{
.url = "https://github.com/Geun-Oh/zigtls/releases/download/<tag>/zigtls-<tag>.tar.gz",
.hash = "<content-hash>",
},
},Tag releases automatically publish this tarball and emit the exact snippet/hash in release notes via .github/workflows/tag-release.yml (trigger: push on v* tags).
Wire the module in build.zig:
const zigtls_dep = b.dependency("zigtls", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zigtls", zigtls_dep.module("zigtls"));Use in code:
const zigtls = @import("zigtls");Build and run the included event-loop sample:
zig build lb-exampleSample source: examples/lb_event_loop_sample.zig
Examples index: examples/README.md
QUIC TLS integration example guide: examples/quic_tls_usage.zig
This sample is an event-loop/protocol flow demo. It uses mock credentials, not production certificate issuance or rotation.
Production-oriented server-side usage shape (real cert/key binding):
const std = @import("std");
const zigtls = @import("zigtls");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var store = zigtls.cert_reload.Store.init(allocator);
defer store.deinit();
// PEM files are provisioned/rotated by external tooling (ACME, cert-manager, etc.).
_ = try store.reloadFromFiles("./certs/server-cert.pem", "./certs/server-key.pem");
var conn = try zigtls.termination.Connection.initChecked(allocator, .{
.session = .{
.role = .server,
.suite = .tls_aes_128_gcm_sha256,
},
.dynamic_server_credentials = .{
.store = &store,
// Auto signer path expects Ed25519 PKCS#8 private key.
.auto_sign_from_store_ed25519 = true,
},
});
defer conn.deinit();
conn.accept(.{ .connection_id = 1, .correlation_id = 1 });
// 1) ingest TLS record bytes from socket
// _ = try conn.ingest_tls_bytes(record_bytes);
// 2) drain outbound TLS records to socket
// _ = try conn.drain_tls_records(out_buf);
// 3) read decrypted plaintext
// _ = conn.read_plaintext(app_buf);
}For non-Ed25519 keys, use manual signer callback mode via dynamic_server_credentials.sign_certificate_verify.
- Library root exports:
src/root.zigzigtls.tls13zigtls.quic(experimental QUIC TLS 1.3 helpers)zigtls.terminationzigtls.adapterzigtls.cert_reloadzigtls.rate_limitzigtls.metrics
zigtls.quic.tls13provides QUIC-facing TLS key schedule helpers only: Initial secret derivation, packet protection key/IV/HP derivation, key-update derivation, and QUIC packet nonce construction.zigtls.quic.transport_parametersprovides QUIC transport-parameters varint encode/decode helpers with duplicate/truncation validation.- QUIC transport engine responsibilities (packetization, ACK/loss recovery, congestion control, CID lifecycle, UDP IO) are intentionally out of
zigtls.quicpublic API scope and are expected to be implemented by the embedding QUIC engine. - Termination API details:
docs/termination-api.md - Error matrix:
docs/error-termination-matrix.md
| Area | Provided by zigtls | Must be implemented by QUIC engine |
|---|---|---|
| TLS 1.3 handshake state machine | Yes (zigtls.tls13.session) |
No |
| QUIC transport-parameter encoding/decoding | Yes (zigtls.quic.transport_parameters) |
No |
| QUIC key derivation (Initial/Handshake/1-RTT key material helpers) | Yes (zigtls.quic.tls13) |
No |
| Packet format/seal/open, ACK/loss recovery, congestion control | No | Yes |
| Connection ID lifecycle/migration | No | Yes |
| UDP socket I/O and timers | No | Yes |
zigtls intentionally follows a BoringSSL-style boundary: TLS + crypto primitives only.
A full QUIC transport stack is out of scope for this library.
- Build/parse QUIC transport parameters in your QUIC engine, and pass serialized local parameters to TLS config (
quic_mode,quic_transport_parameters). - Client side: start handshake with
engine.beginQuicClientHandshake(options)and send returned egress frames fromengine.popOutboundQuicHandshake()as QUIC Initial CRYPTO.- If
peer_validation.enforce_certificate_verify == true(default), configure bothpeer_validation.trust_storeand an expected server name (peer_validation.expected_server_nameoroptions.server_name).
- If
- Feed reassembled peer CRYPTO payload bytes into
engine.ingestQuicHandshake(level, payload). - After each ingest, drain
engine.popOutboundQuicHandshake()and emit those payloads at the returned QUIC encryption level. - Watch
IngestResult.actionsfor.quic_key_readyand/or pollengine.snapshotQuicSecrets()to detect newly available Handshake/1-RTT secrets. - Convert exported secret bytes to
zigtls.quic.tls13.TrafficSecret, derive packet-protection keys withderivePacketProtectionKeys, and install them in your QUIC packet protection path. - Read captured peer transport-parameters bytes through
engine.peerQuicTransportParameters()after ClientHello/EncryptedExtensions ingress validation, then decode/validate in your QUIC engine.
QUIC-native handshake API surface in zigtls.tls13.session.Engine:
beginQuicClientHandshake(options): client-only helper that builds/enqueues TLS ClientHello and updates transcript internally.options.server_namefalls back topeer_validation.expected_server_namewhen omitted. Re-entrant calls are rejected except one retry after a valid HelloRetryRequest ingress event.ingestQuicHandshake(level, payload): ingress path for QUIC CRYPTO payload bytes (initial/handshake/application).popOutboundQuicHandshake(): egress path that returns{ level, payload }for QUIC CRYPTO emission.noteOutboundQuicHandshake(payload): compatibility hook only when your QUIC engine still constructs ClientHello bytes itself (do not call for payloads returned bypopOutboundQuicHandshake()).peerQuicTransportParameters(): returns the peer QUIC TP bytes captured during handshake validation.snapshotQuicSecrets(): exports currently available handshake/application traffic secrets.
Example secret-export -> key-derivation path:
const snap = engine.snapshotQuicSecrets();
if (snap.application_write) |sec| {
const ts = try zigtls.quic.tls13.trafficSecretFromBytes(
snap.suite,
sec.bytes[0..sec.len],
);
const keys = try zigtls.quic.tls13.derivePacketProtectionKeys(snap.suite, &ts);
_ = keys; // install into your QUIC packet protection path
}Companion transport-engine example code (packet/recovery/session glue) lives in:
../zigtls-test-quic/src/quic/*.zig../zigtls-test-quic/src/main.zig
Base regression gate:
zig build testQUIC helper regression subset:
zig test src/quic.zigTask/release gate entrypoint:
bash scripts/release/verify_task_gates.shStrict preflight (interop + gates + evidence sync):
bash scripts/release/preflight.sh --strict-interop --task-gates --sync-evidence-docsExternal consumer compatibility check:
bash scripts/release/check_external_consumer.shExternal consumer URL/hash compatibility check:
bash scripts/release/check_external_consumer_url.sh --url <tarball-url-or-path> --hash <content-hash>- Release workflow:
docs/release-runbook.md - Sign-off checklist:
docs/release-signoff.md - Risk acceptance:
docs/risk-acceptance.md - Security response:
docs/security-response-policy.md - Rollout/canary/rollback:
docs/rollout-canary-gate.md - API compatibility policy:
docs/api-compatibility-policy.md
Contributions are welcome.
- Fork and create a feature branch.
- Follow repository gates before submitting:
zig build testbash scripts/release/verify_task_gates.sh --basic-only
- Keep changes atomic and revert-friendly.
- For behavior changes, include tests and update relevant docs.
- Use patch-style commit messages (
MINOR|MEDIUM|MAJOR, optionallyBUG/).
For release-sensitive changes, follow docs/release-runbook.md and docs/release-signoff.md.
This project is licensed under the MIT License. See LICENSE.