Skip to main content

core.net.tls13 — TLS 1.3

A pure-Verum implementation of TLS 1.3, replacing the legacy intrinsic-backed core.net.tls.* surface with a fully-typed, refinement-verified pipeline. The crate tracks these specifications:

SpecTitleScope
RFC 8446The Transport Layer Security (TLS) Protocol Version 1.3Record layer, handshake, key schedule, 0-RTT
RFC 8448Example Handshake TracesByte-level KAT coverage
RFC 8449Record Size Limitmax_fragment_length extension
RFC 8879Certificate CompressionInterop with brotli / zstd compression
RFC 5869HKDFKey schedule primitive
RFC 9147 §4.2DTLS 1.3 early-data semanticsCross-referenced by QUIC 0-RTT

TLS 1.3 design in Verum has two deliberate departures from wire-mirror implementations:

  1. Typed handshake phases — the session value carries its handshake state as a type parameter. Moving from Handshaking to Established is a type transition, not a runtime check.
  2. Transcript hash as a dependent parameter — each handshake message is indexed by the prefix hash it must be appended to, so transcript-mismatch bugs (e.g., Lucky-13 style) are compile-time errors rather than runtime protocol aborts.

Module map

ConcernModuleKey types
Record layercore.net.tls13.recordRecordAead, ContentType, ProtectedRecord
Key schedulecore.net.tls13.keyschedule.{derive_secret, schedule}EarlySecret, HandshakeSecret, MasterSecret
Handshake drivercore.net.tls13.handshakeClientSm, ServerSm, HandshakeDriver
Typed sessioncore.net.tls13.sessionTlsClient<Phase>, TlsServer<Phase>, Progress
Cipher suitescore.net.tls13.cipher_suiteCipherSuite, AeadKind, HashKind
Named groupscore.net.tls13.named_groupNamedGroup, Curve25519, P256, P384, P521
Signature schemescore.net.tls13.sig_schemeSignatureScheme, default_offer_list
Extensionscore.net.tls13.handshake.extensionExtension, SNI, ALPN, KeyShare, EarlyData, …
Alertscore.net.tls13.alertAlert, AlertLevel, AlertDescription
0-RTT / resumptioncore.net.tls13.handshake.{early_data, psk, ticket_issuer}EarlyKeys, PskIdentity, ClientSession
Post-handshake authcore.net.tls13.handshake.post_handshake_authPostHandshakeAuthExt, CertificateRequest trigger
Anti-replaycore.net.tls13.handshake.zero_rtt_antireplayAntiReplayCache, bloom-filter + strike-register
kTLS handoffcore.net.tls13.record.ktlsLinux 5.x kernel-offloaded record layer

Typed session flow

The client session carries its phase as a type parameter. The compiler enforces that write/read are only callable on TlsClient<Established>.

mount core.net.tls13.session.{TlsClient, Progress};
mount core.net.tls13.handshake.{ClientConfig};
mount core.net.tls13.cipher_suite.{CipherSuite};
mount core.net.tls13.named_group.{NamedGroup};
mount core.net.tls13.sig_scheme.{SignatureScheme};
mount core.security.x509.{TrustStore};

async fn tls_handshake(transport: &mut TcpStream)
-> Result<TlsClient<Established>, TlsError>
{
let cfg = ClientConfig {
server_name: "example.test",
alpn: [b"h3".to_list()],
trust: TrustStore.system()?,
groups: [NamedGroup.X25519],
suites: [CipherSuite.TlsAes128GcmSha256],
sig_schemes: SignatureScheme.default_offer_list(),
verify_hostname: true,
random: csprng_32()?,
key_shares: [],
};

// Start handshake — get the ClientHello to ship.
let (mut client, first_wire) = TlsClient.new(cfg)?;
transport.write_all(first_wire.as_slice()).await?;

// Drive to Established.
loop {
let inbound = transport.read_record().await?;
match client.progress(inbound.as_slice())? {
Progress.Done(established) => return Ok(established),
Progress.Continue(next) => {
client = next;
if let Some(out) = client.drain_outbound() {
transport.write_all(out.as_slice()).await?;
}
}
}
}
}

The returned TlsClient<Established> exposes write(&mut self, buf) and read(&mut self, buf) — neither method exists on TlsClient<Handshaking>, so the compile-time proof "session in handshake cannot emit application data" is free.

Key schedule (RFC 8446 §7.1)

The key schedule is a linear HKDF chain. Each derivation is a separate function so that mis-labeling a secret (e.g., deriving the c hs traffic secret from the server_handshake_secret) is caught by the type system:

zero_salt PSK (or zero)
│ │
└─── HKDF-Extract ──────────────┘

early_secret

┌──────┼──────────┐
│ │ │
c e c hs e exp
traffic master master

Derive-Secret

ECDHE(e, s)

HKDF-Extract(., .)

handshake_secret

┌──────┼──────────┐
│ │ │
c hs s hs ...
traffic traffic

zero_salt

HKDF-Extract

master_secret

┌──────┼──────────┐
│ │ │
c ap s ap exp
traffic traffic master

The three entry points (early_secret, handshake_secret, master_secret) are distinct types; derive_secret is module-parametric over the hash (Sha256, Sha384).

Refinement contracts (V1–V9)

Invariants encoded at type level and discharged by verum verify:

#InvariantModule
V1Record sequence monotonic (per direction)record
V2AEAD nonce = static IV xor sequence counter (§5.3)record
V3SignatureScheme.default_offer_list() distinct + non-emptysig_scheme
V4CipherSuite.aead_kind() matches hash kind (TLS 1.3 tuple invariant)cipher_suite
V5Transcript hash extends monotonicallyhandshake.transcript
V6Post-handshake auth CertificateRequest is allowed at 0 or more positionshandshake.post_handshake_auth
V7Anti-replay (psk_id, ClientHello hash) window never shrinkshandshake.zero_rtt_antireplay
V8Alert levels (Warning/Fatal) exhaust description enumalert
V9Key-update boundaries ≥ confidentiality limitshared with QUIC key_update

Every invariant has a theorem file under vcs/specs/L2-standard/net/tls13/. The proofs depend on HKDF being axiomatised against its RFC 5869 specification (visible via verum audit --framework-axioms).

0-RTT / resumption

Resumption uses RFC 8446 §4.6.1 NewSessionTicket messages; the server mints a ClientSession (ticket + master secret handle + max early data size), and the client's next connection offers it in the pre-shared-key extension.

mount core.net.tls13.handshake.{ClientConfig, early_data};
mount core.net.tls13.session.{TlsClient};

async fn resume_with_0rtt(
cfg: ClientConfig,
session: ClientSession,
early_payload: &[Byte],
) -> Result<TlsClient<Established>, TlsError> {
if (early_payload.len() as UInt32) > session.max_early_data_size {
return Err(TlsError.EarlyDataTooLarge(early_payload.len()));
}
let (mut client, first_wire, early_wire) =
TlsClient.new_resumed(cfg, session, early_payload)?;
// ship first_wire + early_wire immediately ... drive progress as before.
}

The server must enforce anti-replay. zero_rtt_antireplay offers two strategies — single-use tickets (burn on first use) and bloom-filter dedup over (psk_id, ClientHello-hash) — per RFC 8446 §8.

Post-handshake authentication (RFC 8446 §4.6.2)

post_handshake_auth.PostHandshakeAuthExt lets the server mint a CertificateRequest at any point after the handshake completes. The typed session carries a phantom parameter HasClientAuth that flips once the client sends its post-handshake Certificate message, so the application layer can assert client.is_authenticated() in a refinement-typed context.

kTLS offload

core.net.tls13.record.ktls exposes a Linux 5.x kernel-offloaded record layer: once the handshake completes, TlsClient<Established> can hand its traffic keys to the kernel via setsockopt(TLS_TX) / setsockopt(TLS_RX). Subsequent write/read then go through plain TcpStream I/O — the kernel applies AEAD. This saves a copy on the data-plane without weakening the TLS 1.3 state machine guarantees (the handshake, key update, alerts, and post-handshake auth all stay in Verum).

Testing

  • Byte-level RFC 8448 traces: vcs/specs/L2-standard/net/tls13/rfc8448_*.vr (§3, §4, §5, §6, §7).
  • Alert tests exercise every alert description at both levels.
  • Anti-replay cache unit tests simulate 10⁶-entry bloom filters and verify false-positive rate.

See also

  • core.net.quic — QUIC consumes tls13.handshake via CRYPTO frames; the session surface is reused.
  • core.net.h3 — ALPN-negotiated HTTP/3 sits on top.
  • core.security — certificate chain validation (TrustStore.verify(...)) and the cipher / AEAD / KDF primitives the key schedule consumes.

Status (2026-04)

Full handshake path (1-RTT + 0-RTT) lands. L2 typecheck suite covers every message type; RFC 8448 byte-level KAT suite passes. Server-side post-handshake auth is wired; anti-replay ships as two independent strategies.