quic_crypto (quic v1.8.1)

View Source

TLS 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

group/0

-type group() :: x25519 | secp256r1 | secp384r1 | x25519mlkem768.

Private key material for a key exchange: raw curve key for ECDHE, opaque {MlKemDecapsKey, X25519Priv} for the hybrid group.

kex_private/0

-type kex_private() :: binary() | {binary(), binary()}.

Functions

cipher_to_hash(_)

-spec cipher_to_hash(atom()) -> atom().

Map cipher suite to corresponding hash algorithm.

compute_finished_verify(FinishedKey, TranscriptHash)

-spec compute_finished_verify(binary(), binary()) -> binary().

Compute the Finished verify_data. verify_data = HMAC(finished_key, Transcript-Hash(Handshake Context))

compute_finished_verify(Cipher, FinishedKey, TranscriptHash)

-spec compute_finished_verify(atom(), binary(), binary()) -> binary().

Compute the Finished verify_data with cipher-specific hash.

compute_psk_binder(EarlySecret, TruncatedClientHelloHash, Type)

-spec compute_psk_binder(binary(), binary(), resumption | external) -> binary().

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(Cipher, EarlySecret, TruncatedClientHelloHash, Type)

-spec compute_psk_binder(atom(), binary(), binary(), resumption | external) -> binary().

Compute PSK binder with cipher-specific hash.

compute_retry_integrity_tag(OriginalDCID, RetryPacketWithoutTag, Version)

-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.

compute_shared_secret(Curve, OurPrivate, TheirPublic)

-spec compute_shared_secret(group(), kex_private(), binary()) ->
                               binary() | {error, illegal_parameter | internal_error}.

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_app_secret(MasterSecret, TranscriptHash)

-spec derive_client_app_secret(binary(), binary()) -> binary().

Derive client application traffic secret. client_application_traffic_secret_0 = Derive-Secret( master_secret, "c ap traffic", ClientHello...server Finished)

derive_client_app_secret(Cipher, MasterSecret, TranscriptHash)

-spec derive_client_app_secret(atom(), binary(), binary()) -> binary().

Derive client application traffic secret with cipher-specific hash.

derive_client_early_traffic_secret(EarlySecret, ClientHelloHash)

-spec derive_client_early_traffic_secret(binary(), binary()) -> binary().

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(Cipher, EarlySecret, ClientHelloHash)

-spec derive_client_early_traffic_secret(atom(), binary(), binary()) -> binary().

Derive client early traffic secret with cipher-specific hash.

derive_client_handshake_secret(HandshakeSecret, TranscriptHash)

-spec derive_client_handshake_secret(binary(), binary()) -> binary().

Derive client handshake traffic secret. client_handshake_traffic_secret = Derive-Secret( handshake_secret, "c hs traffic", ClientHello...ServerHello)

derive_client_handshake_secret(Cipher, HandshakeSecret, TranscriptHash)

-spec derive_client_handshake_secret(atom(), binary(), binary()) -> binary().

Derive client handshake traffic secret with cipher-specific hash.

derive_early_exporter_master_secret(EarlySecret, ClientHelloHash)

-spec derive_early_exporter_master_secret(binary(), binary()) -> binary().

Derive early exporter master secret. early_exporter_master_secret = Derive-Secret(early_secret, "e exp master", ClientHello)

derive_early_secret()

-spec derive_early_secret() -> binary().

Derive early secret without PSK (zeros). early_secret = HKDF-Extract(0, 0)

derive_early_secret(PSK)

-spec derive_early_secret(binary()) -> binary().

Derive early secret with PSK. early_secret = HKDF-Extract(0, PSK)

derive_early_secret(Cipher, PSK)

-spec derive_early_secret(atom(), binary()) -> binary().

Derive early secret with cipher-specific hash.

derive_finished_key(TrafficSecret)

-spec derive_finished_key(binary()) -> binary().

Derive the finished key from a traffic secret. finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)

derive_finished_key(Cipher, TrafficSecret)

-spec derive_finished_key(atom(), binary()) -> binary().

Derive the finished key with cipher-specific hash.

derive_handshake_secret(EarlySecret, SharedSecret)

-spec derive_handshake_secret(binary(), binary()) -> binary().

Derive handshake secret from early secret and ECDHE shared secret. handshake_secret = HKDF-Extract( Derive-Secret(early_secret, "derived", ""), shared_secret)

derive_handshake_secret(Cipher, EarlySecret, SharedSecret)

-spec derive_handshake_secret(atom(), binary(), binary()) -> binary().

Derive handshake secret with cipher-specific hash.

derive_handshake_secret_psk_only(EarlySecret)

-spec derive_handshake_secret_psk_only(binary()) -> binary().

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_psk_only(Cipher, EarlySecret)

-spec derive_handshake_secret_psk_only(atom(), binary()) -> binary().

Derive handshake secret for psk_ke with cipher-specific hash.

derive_master_secret(HandshakeSecret)

-spec derive_master_secret(binary()) -> binary().

Derive master secret from handshake secret. master_secret = HKDF-Extract( Derive-Secret(handshake_secret, "derived", ""), 0)

derive_master_secret(Cipher, HandshakeSecret)

-spec derive_master_secret(atom(), binary()) -> binary().

Derive master secret with cipher-specific hash.

derive_secret(Secret, Label, Messages)

-spec derive_secret(binary(), binary(), binary()) -> binary().

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(Hash, Secret, Label, Messages)

-spec derive_secret(atom(), binary(), binary(), binary()) -> binary().

Derive-Secret with specified hash algorithm.

derive_server_app_secret(MasterSecret, TranscriptHash)

-spec derive_server_app_secret(binary(), binary()) -> binary().

Derive server application traffic secret. server_application_traffic_secret_0 = Derive-Secret( master_secret, "s ap traffic", ClientHello...server Finished)

derive_server_app_secret(Cipher, MasterSecret, TranscriptHash)

-spec derive_server_app_secret(atom(), binary(), binary()) -> binary().

Derive server application traffic secret with cipher-specific hash.

derive_server_handshake_secret(HandshakeSecret, TranscriptHash)

-spec derive_server_handshake_secret(binary(), binary()) -> binary().

Derive server handshake traffic secret. server_handshake_traffic_secret = Derive-Secret( handshake_secret, "s hs traffic", ClientHello...ServerHello)

derive_server_handshake_secret(Cipher, HandshakeSecret, TranscriptHash)

-spec derive_server_handshake_secret(atom(), binary(), binary()) -> binary().

Derive server handshake traffic secret with cipher-specific hash.

generate_key_pair(Curve)

-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.

group_supported(Group)

-spec group_supported(group()) -> boolean().

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+).

hash_len(_)

hrr_transcript_prefix(HashOrCipher, HashClientHello1)

-spec hrr_transcript_prefix(atom(), binary()) -> binary().

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_key_exchange(Curve, Malformed)

-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}.

transcript_hash(Messages)

-spec transcript_hash(binary()) -> binary().

Compute transcript hash of handshake messages (default SHA-256).

transcript_hash(HashOrCipher, Messages)

-spec transcript_hash(atom(), binary()) -> binary().

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_retry_integrity_tag(OriginalDCID, RetryPacket, Version)

-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.