nquic_protocol_handshake (nquic v1.0.0)
View SourceTLS 1.3 handshake driver for the QUIC protocol state.
Pure functions over #conn_state{} covering the QUIC half of the
handshake (RFC 9001 §4 client initiation, §6 secret installation,
§4.1 CRYPTO buffering and dispatch, §8.3 0-RTT acceptance, RFC 8446
§4.6.1 NewSessionTicket emission). The TLS 1.3 message construction
itself lives in nquic_tls_client / nquic_tls_server (with shared
codec helpers in nquic_tls); this module owns the QUIC-side wiring:
key installation, transport-parameter validation, packet number space
initialisation, CRYPTO fragment reassembly, and event emission.
Extracted from nquic_protocol as part of REVIEW_PLAN.md Phase 4.4.
The trunk still owns Version Negotiation and Retry orchestration, plus
the public dispatchers; both call back into this module.
Summary
Functions
Apply a Compatible Version Negotiation switch on the server side.
Rederives Initial-space packet protection keys against the wire DCID
used for the client's most recent Initial (the original DCID, or the
Retry-rewritten one when a Retry was issued), updates
conn_state.version, propagates the new version into the TLS state's
quic_version so subsequent handshake/app key derivations pick the
RFC 9369 labels, and rewrites the server's own
version_information.chosen_version so the outgoing EncryptedExtensions
TPs reflect the switch (RFC 9368 §4).
Discard Handshake-level keys and state once the handshake is confirmed.
RFC 9001 §4.9.2 / RFC 9000 §4.1.2: the client confirms the handshake on
receipt of a HANDSHAKE_DONE frame, after which Handshake keys MUST be
discarded. Dropping the keyring entry makes any retransmitted
Handshake-level packet fail to decrypt and be dropped, rather than
re-entering TLS handshake processing with a torn-down tls_state.
Discard Initial-level keys and state. RFC 9001 §4.9.1: the client MUST discard Initial keys when it first sends a Handshake packet. The server MUST discard Initial keys when it first successfully processes a Handshake packet. After discard, the endpoint MUST NOT send or process Initial packets. Dropping the keyring entry makes any later Initial packet fail to decrypt and be silently dropped via the existing decrypt-error path.
Install 1-RTT (application) packet protection keys.
Client-side also validates the peer's retry_source_connection_id
(RFC 9000 §7.3) and version_information (RFC 9369 §3) transport
parameters, initialises connection-level flow limits, and updates the
loss detector's max-datagram budget.
Install Handshake-space packet protection keys derived from the TLS
handshake secret. Server-side also extracts the peer's transport
parameters (remote_params), initialises connection-level flow
limits, and updates the loss detector's max-datagram budget.
Load a leaf certificate (DER) and private key from PEM files.
Used by the gen_statem wrapper at conn startup when the listener has
not pre-loaded certificates. Returns {undefined, undefined} when no
file is configured or the file cannot be read; the caller treats that
as "no local cert", which is fine for clients that do not require
client authentication and for tests that drive the handshake without
real certs.
The listener has its own preload path (nquic_listener_sup:preload_certs/1)
that supports chains and CA certificates.
Pure selection of a Compatible Version Negotiation target (RFC 9368 §4.1).
Walks the server's preference list and returns the first version that
is in the client's advertised other_versions, is supported locally,
and is compatible with VInitial (the version on the wire of the
client's first Initial). If that first match equals VInitial, or if
no version qualifies, the result is no_switch. The server MUST NOT
pick a version the client did not list.
Preference semantics: list order is preference order, first match
wins. The default [1] is the only-v1 / no-switch configuration.
Begin a client handshake.
Generates the ClientHello (with optional PSK / 0-RTT material when a
session ticket is configured), derives Initial-space packet protection
keys, optionally installs 0-RTT keys, initialises the Initial packet
number space, and queues the ClientHello as an Initial-space CRYPTO
frame for the next flush/1. The caller is responsible for flushing
the queue and transmitting the resulting datagram.
Used by the gen_statem wrapper at handshake start, and re-used by the
Version Negotiation handler in nquic_protocol when restarting under
a new version.
Functions
-spec apply_server_compat_version_switch(non_neg_integer(), nquic_protocol:state()) -> nquic_protocol:state().
Apply a Compatible Version Negotiation switch on the server side.
Rederives Initial-space packet protection keys against the wire DCID
used for the client's most recent Initial (the original DCID, or the
Retry-rewritten one when a Retry was issued), updates
conn_state.version, propagates the new version into the TLS state's
quic_version so subsequent handshake/app key derivations pick the
RFC 9369 labels, and rewrites the server's own
version_information.chosen_version so the outgoing EncryptedExtensions
TPs reflect the switch (RFC 9368 §4).
-spec buffer_crypto(nquic_packet:space(), non_neg_integer(), binary(), nquic_protocol:state()) -> {binary(), nquic_protocol:state()}.
-spec discard_handshake_keys(nquic_protocol:state()) -> nquic_protocol:state().
Discard Handshake-level keys and state once the handshake is confirmed.
RFC 9001 §4.9.2 / RFC 9000 §4.1.2: the client confirms the handshake on
receipt of a HANDSHAKE_DONE frame, after which Handshake keys MUST be
discarded. Dropping the keyring entry makes any retransmitted
Handshake-level packet fail to decrypt and be dropped, rather than
re-entering TLS handshake processing with a torn-down tls_state.
-spec discard_initial_keys(nquic_protocol:state()) -> nquic_protocol:state().
Discard Initial-level keys and state. RFC 9001 §4.9.1: the client MUST discard Initial keys when it first sends a Handshake packet. The server MUST discard Initial keys when it first successfully processes a Handshake packet. After discard, the endpoint MUST NOT send or process Initial packets. Dropping the keyring entry makes any later Initial packet fail to decrypt and be silently dropped via the existing decrypt-error path.
-spec install_app_keys(map(), nquic_protocol:state(), map()) -> {ok, nquic_protocol:state()} | {error, {transport_parameter_error | version_negotiation_error, term()}}.
Install 1-RTT (application) packet protection keys.
Client-side also validates the peer's retry_source_connection_id
(RFC 9000 §7.3) and version_information (RFC 9369 §3) transport
parameters, initialises connection-level flow limits, and updates the
loss detector's max-datagram budget.
-spec install_handshake_keys(map(), nquic_protocol:state(), map()) -> {ok, nquic_protocol:state()}.
Install Handshake-space packet protection keys derived from the TLS
handshake secret. Server-side also extracts the peer's transport
parameters (remote_params), initialises connection-level flow
limits, and updates the loss detector's max-datagram budget.
-spec load_certs(file:filename() | undefined, file:filename() | undefined) -> {binary() | undefined, term() | undefined}.
Load a leaf certificate (DER) and private key from PEM files.
Used by the gen_statem wrapper at conn startup when the listener has
not pre-loaded certificates. Returns {undefined, undefined} when no
file is configured or the file cannot be read; the caller treats that
as "no local cert", which is fine for clients that do not require
client authentication and for tests that drive the handshake without
real certs.
The listener has its own preload path (nquic_listener_sup:preload_certs/1)
that supports chains and CA certificates.
-spec make_client_hello_maybe_psk(#transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined}, [binary()] | undefined, string() | binary() | undefined, map() | undefined) -> {ok, binary(), map()} | {error, term()}.
-spec make_client_hello_maybe_psk(#transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined}, [binary()] | undefined, string() | binary() | undefined, map() | undefined, [aes_128_gcm | aes_256_gcm | chacha20_poly1305] | undefined) -> {ok, binary(), map()} | {error, term()}.
-spec preferred_address_event(nquic_protocol:state()) -> [nquic_protocol:event()].
-spec process_handshake_crypto_client(binary(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec process_handshake_crypto_server(binary(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec process_initial_crypto_client(binary(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec process_initial_crypto_server(binary(), nquic_protocol:state()) -> {ok, [nquic_protocol:event()], nquic_protocol:state()} | {error, term(), nquic_protocol:state()}.
-spec queue_new_session_ticket(binary(), crypto:hash_state(), binary(), nquic_protocol:state()) -> nquic_protocol:state().
-spec select_server_compat_version(#transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined} | undefined, [non_neg_integer()], non_neg_integer()) -> no_switch | {switch, non_neg_integer()}.
Pure selection of a Compatible Version Negotiation target (RFC 9368 §4.1).
Walks the server's preference list and returns the first version that
is in the client's advertised other_versions, is supported locally,
and is compatible with VInitial (the version on the wire of the
client's first Initial). If that first match equals VInitial, or if
no version qualifies, the result is no_switch. The server MUST NOT
pick a version the client did not list.
Preference semantics: list order is preference order, first match
wins. The default [1] is the only-v1 / no-switch configuration.
-spec start_client_handshake(nquic_protocol:state()) -> {ok, nquic_protocol:state()}.
Begin a client handshake.
Generates the ClientHello (with optional PSK / 0-RTT material when a
session ticket is configured), derives Initial-space packet protection
keys, optionally installs 0-RTT keys, initialises the Initial packet
number space, and queues the ClientHello as an Initial-space CRYPTO
frame for the next flush/1. The caller is responsible for flushing
the queue and transmitting the resulting datagram.
Used by the gen_statem wrapper at handshake start, and re-used by the
Version Negotiation handler in nquic_protocol when restarting under
a new version.
-spec validate_retry_scid(nquic_protocol:state(), #transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined}) -> ok | {error, nquic_error:any_reason()}.
-spec validate_version_info(nquic_protocol:state(), #transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined}) -> ok | {error, nquic_error:any_reason()}.