quic_crypto (quic v1.8.1)
View SourceTLS 1.3 key schedule and cryptographic operations for QUIC.
This module implements the TLS 1.3 key schedule used by QUIC for deriving encryption keys at each handshake stage.
Key Schedule
TLS 1.3 uses a three-stage key schedule: 1. Early Secret (from PSK, or zeros for non-PSK) 2. Handshake Secret (from ECDHE shared secret) 3. Master Secret (for application data)
Traffic Secrets
From each secret, traffic secrets are derived for both client and server directions.
Summary
Types
Private key material for a key exchange: raw curve key for ECDHE, opaque {MlKemDecapsKey, X25519Priv} for the hybrid group.
Functions
Map cipher suite to corresponding hash algorithm.
Compute the Finished verify_data. verify_data = HMAC(finished_key, Transcript-Hash(Handshake Context))
Compute the Finished verify_data with cipher-specific hash.
Compute PSK binder value for a pre_shared_key extension. RFC 8446 Section 4.2.11.2: binder_key = Derive-Secret(early_secret, "res binder" | "ext binder", "") binder = HMAC(binder_key, Transcript-Hash(Truncated ClientHello)) For resumption PSK, use "res binder". For external PSK, use "ext binder".
Compute PSK binder with cipher-specific hash.
Compute the integrity tag for a Retry packet. Used by servers to generate tags and by clients to verify.
Compute the key-exchange shared secret (client side for the hybrid group). For ECDHE groups: ECDH(our_private, their_public). For x25519mlkem768 the peer share is the server's 1120-byte concatenation of the ML-KEM ciphertext and X25519 public key, and the shared secret is mlkem_secret || x25519_secret (64 bytes). A peer share whose length does not match the group, or a group that does not match the private key we hold, is {error, illegal_parameter}: the caller turns it into a TLS alert rather than letting crypto raise mid-handshake.
Derive client application traffic secret. client_application_traffic_secret_0 = Derive-Secret( master_secret, "c ap traffic", ClientHello...server Finished)
Derive client application traffic secret with cipher-specific hash.
Derive client early traffic secret. client_early_traffic_secret = Derive-Secret(early_secret, "c e traffic", ClientHello) This is used to encrypt 0-RTT data before the handshake completes.
Derive client early traffic secret with cipher-specific hash.
Derive client handshake traffic secret. client_handshake_traffic_secret = Derive-Secret( handshake_secret, "c hs traffic", ClientHello...ServerHello)
Derive client handshake traffic secret with cipher-specific hash.
Derive early exporter master secret. early_exporter_master_secret = Derive-Secret(early_secret, "e exp master", ClientHello)
Derive early secret without PSK (zeros). early_secret = HKDF-Extract(0, 0)
Derive early secret with PSK. early_secret = HKDF-Extract(0, PSK)
Derive early secret with cipher-specific hash.
Derive the finished key from a traffic secret. finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)
Derive the finished key with cipher-specific hash.
Derive handshake secret from early secret and ECDHE shared secret. handshake_secret = HKDF-Extract( Derive-Secret(early_secret, "derived", ""), shared_secret)
Derive handshake secret with cipher-specific hash.
Derive handshake secret for psk_ke (PSK-only, no DHE). RFC 8446 §7.1: when (EC)DHE is not used, the IKM is a zero-vector of the negotiated hash's length.
Derive handshake secret for psk_ke with cipher-specific hash.
Derive master secret from handshake secret. master_secret = HKDF-Extract( Derive-Secret(handshake_secret, "derived", ""), 0)
Derive master secret with cipher-specific hash.
Derive-Secret with raw messages (will be hashed). Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
Derive-Secret with specified hash algorithm.
Derive server application traffic secret. server_application_traffic_secret_0 = Derive-Secret( master_secret, "s ap traffic", ClientHello...server Finished)
Derive server application traffic secret with cipher-specific hash.
Derive server handshake traffic secret. server_handshake_traffic_secret = Derive-Secret( handshake_secret, "s hs traffic", ClientHello...ServerHello)
Derive server handshake traffic secret with cipher-specific hash.
Generate a key-exchange key pair for the specified group. Returns {PublicShare, PrivateKey}. For the classical ECDHE groups PublicShare/PrivateKey are the raw curve keys. For the post-quantum hybrid group x25519mlkem768 (draft-ietf-tls-ecdhe-mlkem) the public share is the 1216-byte concatenation of the ML-KEM-768 encapsulation key and the X25519 public key, and the private key is an opaque {MlKemDecapsKey, X25519Priv} pair; both are only ever consumed by compute_shared_secret/3.
Whether this runtime can negotiate the given group: it must be one quic_tls has a wire code for, and the hybrid group also needs ML-KEM-768 support in crypto (OTP 28.1+).
Synthetic message_hash handshake message that replaces ClientHello1 in the transcript after a HelloRetryRequest (RFC 8446 §4.4.1). HashClientHello1 is the digest over the complete CH1 handshake message. Both peers prepend the result to the post-HRR transcript.
Server-side key exchange against a client key share. Returns {ServerShare, ServerPrivate, SharedSecret}. For ECDHE groups this generates a server key pair and computes ECDH; ServerPrivate is the curve private key (kept for parity with the previous behaviour). For x25519mlkem768 the server encapsulates against the client's ML-KEM encapsulation key: the server share is ciphertext || x25519 public (1120 bytes), the shared secret is mlkem || x25519 (64 bytes), and there is no retained private key (the exchange is complete). A client share whose length does not match the group is {error, illegal_parameter}.
Compute transcript hash of handshake messages (default SHA-256).
Compute transcript hash with specified hash algorithm or cipher. Accepts both hash atoms (sha256, sha384) and cipher atoms (aes_128_gcm, aes_256_gcm).
Verify the integrity tag of a Retry packet. RFC 9001 Section 5.8: - Retry Pseudo-Packet = <ODCID length> <ODCID> <Retry packet without tag> - Tag = AES-128-GCM(Key, Nonce, AAD=Pseudo-Packet, "") Returns true if the tag is valid, false otherwise.
Types
Functions
Map cipher suite to corresponding hash algorithm.
Compute the Finished verify_data. verify_data = HMAC(finished_key, Transcript-Hash(Handshake Context))
Compute the Finished verify_data with cipher-specific hash.
Compute PSK binder value for a pre_shared_key extension. RFC 8446 Section 4.2.11.2: binder_key = Derive-Secret(early_secret, "res binder" | "ext binder", "") binder = HMAC(binder_key, Transcript-Hash(Truncated ClientHello)) For resumption PSK, use "res binder". For external PSK, use "ext binder".
Compute PSK binder with cipher-specific hash.
-spec compute_retry_integrity_tag(binary(), binary(), non_neg_integer()) -> binary().
Compute the integrity tag for a Retry packet. Used by servers to generate tags and by clients to verify.
Derive client application traffic secret. client_application_traffic_secret_0 = Derive-Secret( master_secret, "c ap traffic", ClientHello...server Finished)
Derive client application traffic secret with cipher-specific hash.
Derive client early traffic secret. client_early_traffic_secret = Derive-Secret(early_secret, "c e traffic", ClientHello) This is used to encrypt 0-RTT data before the handshake completes.
Derive client early traffic secret with cipher-specific hash.
Derive client handshake traffic secret. client_handshake_traffic_secret = Derive-Secret( handshake_secret, "c hs traffic", ClientHello...ServerHello)
Derive client handshake traffic secret with cipher-specific hash.
Derive early exporter master secret. early_exporter_master_secret = Derive-Secret(early_secret, "e exp master", ClientHello)
-spec derive_early_secret() -> binary().
Derive early secret without PSK (zeros). early_secret = HKDF-Extract(0, 0)
Derive early secret with PSK. early_secret = HKDF-Extract(0, PSK)
Derive early secret with cipher-specific hash.
Derive the finished key from a traffic secret. finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)
Derive the finished key with cipher-specific hash.
Derive handshake secret from early secret and ECDHE shared secret. handshake_secret = HKDF-Extract( Derive-Secret(early_secret, "derived", ""), shared_secret)
Derive handshake secret with cipher-specific hash.
Derive handshake secret for psk_ke (PSK-only, no DHE). RFC 8446 §7.1: when (EC)DHE is not used, the IKM is a zero-vector of the negotiated hash's length.
Derive handshake secret for psk_ke with cipher-specific hash.
Derive master secret from handshake secret. master_secret = HKDF-Extract( Derive-Secret(handshake_secret, "derived", ""), 0)
Derive master secret with cipher-specific hash.
Derive-Secret with raw messages (will be hashed). Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
Derive-Secret with specified hash algorithm.
Derive server application traffic secret. server_application_traffic_secret_0 = Derive-Secret( master_secret, "s ap traffic", ClientHello...server Finished)
Derive server application traffic secret with cipher-specific hash.
Derive server handshake traffic secret. server_handshake_traffic_secret = Derive-Secret( handshake_secret, "s hs traffic", ClientHello...ServerHello)
Derive server handshake traffic secret with cipher-specific hash.
-spec generate_key_pair(group()) -> {binary(), kex_private()}.
Generate a key-exchange key pair for the specified group. Returns {PublicShare, PrivateKey}. For the classical ECDHE groups PublicShare/PrivateKey are the raw curve keys. For the post-quantum hybrid group x25519mlkem768 (draft-ietf-tls-ecdhe-mlkem) the public share is the 1216-byte concatenation of the ML-KEM-768 encapsulation key and the X25519 public key, and the private key is an opaque {MlKemDecapsKey, X25519Priv} pair; both are only ever consumed by compute_shared_secret/3.
Whether this runtime can negotiate the given group: it must be one quic_tls has a wire code for, and the hybrid group also needs ML-KEM-768 support in crypto (OTP 28.1+).
Synthetic message_hash handshake message that replaces ClientHello1 in the transcript after a HelloRetryRequest (RFC 8446 §4.4.1). HashClientHello1 is the digest over the complete CH1 handshake message. Both peers prepend the result to the post-HRR transcript.
-spec server_key_exchange(group(), binary()) -> {binary(), kex_private() | undefined, binary()} | {error, illegal_parameter}.
Server-side key exchange against a client key share. Returns {ServerShare, ServerPrivate, SharedSecret}. For ECDHE groups this generates a server key pair and computes ECDH; ServerPrivate is the curve private key (kept for parity with the previous behaviour). For x25519mlkem768 the server encapsulates against the client's ML-KEM encapsulation key: the server share is ciphertext || x25519 public (1120 bytes), the shared secret is mlkem || x25519 (64 bytes), and there is no retained private key (the exchange is complete). A client share whose length does not match the group is {error, illegal_parameter}.
Compute transcript hash of handshake messages (default SHA-256).
Compute transcript hash with specified hash algorithm or cipher. Accepts both hash atoms (sha256, sha384) and cipher atoms (aes_128_gcm, aes_256_gcm).
-spec verify_retry_integrity_tag(binary(), binary(), non_neg_integer()) -> boolean().
Verify the integrity tag of a Retry packet. RFC 9001 Section 5.8: - Retry Pseudo-Packet = <ODCID length> <ODCID> <Retry packet without tag> - Tag = AES-128-GCM(Key, Nonce, AAD=Pseudo-Packet, "") Returns true if the tag is valid, false otherwise.