%%% -*- erlang -*- %%% %%% QUIC TLS 1.3 Message Handling %%% RFC 8446 - TLS 1.3 %%% RFC 9001 - Using TLS to Secure QUIC %%% %%% Copyright (c) 2024-2026 Benoit Chesneau %%% Apache License 2.0 %%% %%% @doc TLS 1.3 message generation and parsing for QUIC. %%% %%% This module handles TLS 1.3 handshake messages as they appear in %%% QUIC CRYPTO frames. Messages are encoded without the TLS record layer. %%% %%% == TLS Messages in QUIC == %%% %%% QUIC uses TLS 1.3 for the cryptographic handshake, but without the %%% TLS record layer. TLS handshake messages are sent directly in %%% CRYPTO frames. %%% -module(quic_tls). -include("quic.hrl"). -export([ %% ClientHello build_client_hello/1, %% Server message parsing (client-side) parse_server_hello/1, parse_encrypted_extensions/1, parse_certificate/1, parse_certificate_verify/1, parse_finished/1, %% Client Finished build_finished/1, verify_finished/3, verify_finished/4, %% Server-side message building parse_client_hello/1, build_server_hello/1, build_encrypted_extensions/1, build_certificate/2, build_certificate_verify/3, %% Transport parameters encode_transport_params/1, decode_transport_params/1, %% TLS message framing encode_handshake_message/2, decode_handshake_message/1 ]). %%==================================================================== %% ClientHello %%==================================================================== %% @doc Build a TLS 1.3 ClientHello message for QUIC. %% Options: %% - server_name: SNI hostname (binary) %% - alpn: List of ALPN protocols (list of binaries) %% - transport_params: QUIC transport parameters (map) %% %% Returns: {ClientHelloMsg, PrivateKey, Random} -spec build_client_hello(map()) -> {binary(), binary(), binary()}. build_client_hello(Opts) -> %% Generate client random (32 bytes) Random = crypto:strong_rand_bytes(32), %% Generate ECDHE key pair (x25519) {PubKey, PrivKey} = crypto:generate_key(ecdh, x25519), %% Build extensions Extensions = build_client_hello_extensions(PubKey, Opts), %% Legacy session ID (empty for TLS 1.3) SessionId = <<>>, %% Cipher suites (TLS 1.3 only) %% Only offering AES-128-GCM for now until AES-256-GCM derivation is fixed CipherSuites = <>, %% Legacy compression methods (null only) CompressionMethods = <<1, 0>>, %% Build ClientHello body ClientHello = << ?TLS_VERSION_1_2:16, % legacy_version (always 0x0303) Random:32/binary, % random (byte_size(SessionId)):8, % legacy_session_id length SessionId/binary, % legacy_session_id (byte_size(CipherSuites)):16, % cipher_suites length CipherSuites/binary, % cipher_suites CompressionMethods/binary, % legacy_compression_methods (byte_size(Extensions)):16, % extensions length Extensions/binary % extensions >>, %% Wrap in handshake message Msg = encode_handshake_message(?TLS_CLIENT_HELLO, ClientHello), {Msg, PrivKey, Random}. %% Build ClientHello extensions build_client_hello_extensions(PubKey, Opts) -> ServerName = maps:get(server_name, Opts, undefined), Alpn = maps:get(alpn, Opts, []), TransportParams = maps:get(transport_params, Opts, #{}), %% Supported versions (TLS 1.3 only) %% Length is in bytes (2 bytes per version), not version count SupportedVersions = encode_extension(?EXT_SUPPORTED_VERSIONS, <<2, ?TLS_VERSION_1_3:16>>), %% Supported groups (x25519) SupportedGroups = encode_extension(?EXT_SUPPORTED_GROUPS, <<2:16, ?GROUP_X25519:16>>), %% Signature algorithms SigAlgs = encode_extension(?EXT_SIGNATURE_ALGORITHMS, <<8:16, ?SIG_ECDSA_SECP256R1_SHA256:16, ?SIG_RSA_PSS_RSAE_SHA256:16, ?SIG_RSA_PKCS1_SHA256:16, ?SIG_ED25519:16>>), %% Key share (x25519 public key) KeyShareEntry = <>, KeyShare = encode_extension(?EXT_KEY_SHARE, <<(byte_size(KeyShareEntry)):16, KeyShareEntry/binary>>), %% Server Name Indication SNI = case ServerName of undefined -> <<>>; Name when is_binary(Name) -> NameLen = byte_size(Name), NameList = <<0, NameLen:16, Name/binary>>, encode_extension(?EXT_SERVER_NAME, <<(byte_size(NameList)):16, NameList/binary>>) end, %% ALPN AlpnExt = case Alpn of [] -> <<>>; Protocols -> ProtocolList = encode_alpn_list(Protocols), encode_extension(?EXT_ALPN, <<(byte_size(ProtocolList)):16, ProtocolList/binary>>) end, %% QUIC Transport Parameters TransportParamsData = encode_transport_params(TransportParams), TransportParamsExt = encode_extension(?EXT_QUIC_TRANSPORT_PARAMS, TransportParamsData), %% PSK Key Exchange Modes (psk_dhe_ke only) PskModes = encode_extension(?EXT_PSK_KEY_EXCHANGE_MODES, <<1, 1>>), % psk_dhe_ke = 1 iolist_to_binary([ SupportedVersions, SupportedGroups, SigAlgs, KeyShare, SNI, AlpnExt, TransportParamsExt, PskModes ]). encode_alpn_list(Protocols) -> iolist_to_binary([<<(byte_size(P)):8, P/binary>> || P <- Protocols]). %%==================================================================== %% Server Message Parsing %%==================================================================== %% @doc Parse a ServerHello message. %% Returns server's public key and selected cipher suite. -spec parse_server_hello(binary()) -> {ok, #{public_key := binary(), cipher := atom(), random := binary()}} | {error, term()}. parse_server_hello(<< ?TLS_VERSION_1_2:16, % legacy_version Random:32/binary, SessionIdLen:8, SessionId:SessionIdLen/binary, CipherSuite:16, 0, % legacy_compression_method ExtensionsLen:16, Extensions:ExtensionsLen/binary, _Rest/binary >>) -> %% Parse extensions to get key_share case parse_extensions(Extensions) of {ok, ExtMap} -> case maps:find(?EXT_KEY_SHARE, ExtMap) of {ok, KeyShareData} -> case parse_server_key_share(KeyShareData) of {ok, PubKey} -> Cipher = cipher_from_suite(CipherSuite), {ok, #{ public_key => PubKey, cipher => Cipher, random => Random, session_id => SessionId, extensions => ExtMap }}; Error -> Error end; error -> {error, missing_key_share} end; Error -> Error end; parse_server_hello(_) -> {error, invalid_server_hello}. %% @doc Parse EncryptedExtensions message. -spec parse_encrypted_extensions(binary()) -> {ok, #{alpn => binary(), transport_params => map()}} | {error, term()}. parse_encrypted_extensions(<>) -> case parse_extensions(Extensions) of {ok, ExtMap} -> Alpn = case maps:find(?EXT_ALPN, ExtMap) of {ok, <<_ListLen:16, ProtoLen:8, Proto:ProtoLen/binary, _/binary>>} -> Proto; _ -> undefined end, TransportParams = case maps:find(?EXT_QUIC_TRANSPORT_PARAMS, ExtMap) of {ok, TPData} -> case decode_transport_params(TPData) of {ok, TP} -> TP; _ -> #{} end; _ -> #{} end, {ok, #{alpn => Alpn, transport_params => TransportParams}}; Error -> Error end; parse_encrypted_extensions(_) -> {error, invalid_encrypted_extensions}. %% @doc Parse Certificate message. -spec parse_certificate(binary()) -> {ok, #{context := binary(), certificates := [binary()]}} | {error, term()}. parse_certificate(<>) -> Certs = parse_certificate_list(CertsData), {ok, #{context => Context, certificates => Certs}}; parse_certificate(_) -> {error, invalid_certificate}. parse_certificate_list(<<>>) -> []; parse_certificate_list(<>) -> [Cert | parse_certificate_list(Rest)]. %% @doc Parse CertificateVerify message. -spec parse_certificate_verify(binary()) -> {ok, #{algorithm := non_neg_integer(), signature := binary()}} | {error, term()}. parse_certificate_verify(<>) -> {ok, #{algorithm => Algorithm, signature => Signature}}; parse_certificate_verify(_) -> {error, invalid_certificate_verify}. %% @doc Parse Finished message. -spec parse_finished(binary()) -> {ok, binary()} | {error, term()}. parse_finished(VerifyData) when byte_size(VerifyData) >= 32 -> %% SHA-256 produces 32 bytes <> = VerifyData, {ok, Data}; parse_finished(_) -> {error, invalid_finished}. %%==================================================================== %% Client Finished %%==================================================================== %% @doc Build a Finished message. %% VerifyData should be computed using quic_crypto:compute_finished_verify/2. -spec build_finished(binary()) -> binary(). build_finished(VerifyData) -> encode_handshake_message(?TLS_FINISHED, VerifyData). %% @doc Verify a Finished message (default SHA-256). %% TrafficSecret is the sender's traffic secret. %% TranscriptHash is the hash of all messages up to (but not including) Finished. -spec verify_finished(binary(), binary(), binary()) -> boolean(). verify_finished(ReceivedVerifyData, TrafficSecret, TranscriptHash) -> FinishedKey = quic_crypto:derive_finished_key(TrafficSecret), ExpectedVerifyData = quic_crypto:compute_finished_verify(FinishedKey, TranscriptHash), crypto:hash_equals(ReceivedVerifyData, ExpectedVerifyData). %% @doc Verify a Finished message with cipher-specific hash. -spec verify_finished(binary(), binary(), binary(), atom()) -> boolean(). verify_finished(ReceivedVerifyData, TrafficSecret, TranscriptHash, Cipher) -> FinishedKey = quic_crypto:derive_finished_key(Cipher, TrafficSecret), ExpectedVerifyData = quic_crypto:compute_finished_verify(Cipher, FinishedKey, TranscriptHash), crypto:hash_equals(ReceivedVerifyData, ExpectedVerifyData). %%==================================================================== %% Transport Parameters %%==================================================================== %% @doc Encode QUIC transport parameters. %% Params is a map with keys like: %% original_dcid, max_idle_timeout, max_udp_payload_size, %% initial_max_data, initial_max_stream_data_bidi_local, etc. -spec encode_transport_params(map()) -> binary(). encode_transport_params(Params) -> Encoded = maps:fold(fun(Key, Value, Acc) -> case encode_transport_param(Key, Value) of <<>> -> Acc; Bin -> [Bin | Acc] end end, [], Params), iolist_to_binary(lists:reverse(Encoded)). encode_transport_param(original_dcid, Value) -> encode_tp(?TP_ORIGINAL_DCID, Value); encode_transport_param(max_idle_timeout, Value) -> encode_tp(?TP_MAX_IDLE_TIMEOUT, quic_varint:encode(Value)); encode_transport_param(max_udp_payload_size, Value) -> encode_tp(?TP_MAX_UDP_PAYLOAD_SIZE, quic_varint:encode(Value)); encode_transport_param(initial_max_data, Value) -> encode_tp(?TP_INITIAL_MAX_DATA, quic_varint:encode(Value)); encode_transport_param(initial_max_stream_data_bidi_local, Value) -> encode_tp(?TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL, quic_varint:encode(Value)); encode_transport_param(initial_max_stream_data_bidi_remote, Value) -> encode_tp(?TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE, quic_varint:encode(Value)); encode_transport_param(initial_max_stream_data_uni, Value) -> encode_tp(?TP_INITIAL_MAX_STREAM_DATA_UNI, quic_varint:encode(Value)); encode_transport_param(initial_max_streams_bidi, Value) -> encode_tp(?TP_INITIAL_MAX_STREAMS_BIDI, quic_varint:encode(Value)); encode_transport_param(initial_max_streams_uni, Value) -> encode_tp(?TP_INITIAL_MAX_STREAMS_UNI, quic_varint:encode(Value)); encode_transport_param(ack_delay_exponent, Value) -> encode_tp(?TP_ACK_DELAY_EXPONENT, quic_varint:encode(Value)); encode_transport_param(max_ack_delay, Value) -> encode_tp(?TP_MAX_ACK_DELAY, quic_varint:encode(Value)); encode_transport_param(disable_active_migration, true) -> encode_tp(?TP_DISABLE_ACTIVE_MIGRATION, <<>>); encode_transport_param(active_connection_id_limit, Value) -> encode_tp(?TP_ACTIVE_CONNECTION_ID_LIMIT, quic_varint:encode(Value)); encode_transport_param(initial_scid, Value) -> encode_tp(?TP_INITIAL_SCID, Value); encode_transport_param(_, _) -> <<>>. encode_tp(Id, Value) -> IdBin = quic_varint:encode(Id), LenBin = quic_varint:encode(byte_size(Value)), <>. %% @doc Decode QUIC transport parameters. -spec decode_transport_params(binary()) -> {ok, map()} | {error, term()}. decode_transport_params(Data) -> try decode_transport_params_loop(Data, #{}) catch error:_ -> {error, invalid_transport_params} end. decode_transport_params_loop(<<>>, Acc) -> {ok, Acc}; decode_transport_params_loop(Data, Acc) -> {Id, Rest1} = quic_varint:decode(Data), {Len, Rest2} = quic_varint:decode(Rest1), <> = Rest2, Key = tp_id_to_key(Id), DecodedValue = decode_tp_value(Id, Value), decode_transport_params_loop(Rest3, maps:put(Key, DecodedValue, Acc)). tp_id_to_key(?TP_ORIGINAL_DCID) -> original_dcid; tp_id_to_key(?TP_MAX_IDLE_TIMEOUT) -> max_idle_timeout; tp_id_to_key(?TP_STATELESS_RESET_TOKEN) -> stateless_reset_token; tp_id_to_key(?TP_MAX_UDP_PAYLOAD_SIZE) -> max_udp_payload_size; tp_id_to_key(?TP_INITIAL_MAX_DATA) -> initial_max_data; tp_id_to_key(?TP_INITIAL_MAX_STREAM_DATA_BIDI_LOCAL) -> initial_max_stream_data_bidi_local; tp_id_to_key(?TP_INITIAL_MAX_STREAM_DATA_BIDI_REMOTE) -> initial_max_stream_data_bidi_remote; tp_id_to_key(?TP_INITIAL_MAX_STREAM_DATA_UNI) -> initial_max_stream_data_uni; tp_id_to_key(?TP_INITIAL_MAX_STREAMS_BIDI) -> initial_max_streams_bidi; tp_id_to_key(?TP_INITIAL_MAX_STREAMS_UNI) -> initial_max_streams_uni; tp_id_to_key(?TP_ACK_DELAY_EXPONENT) -> ack_delay_exponent; tp_id_to_key(?TP_MAX_ACK_DELAY) -> max_ack_delay; tp_id_to_key(?TP_DISABLE_ACTIVE_MIGRATION) -> disable_active_migration; tp_id_to_key(?TP_PREFERRED_ADDRESS) -> preferred_address; tp_id_to_key(?TP_ACTIVE_CONNECTION_ID_LIMIT) -> active_connection_id_limit; tp_id_to_key(?TP_INITIAL_SCID) -> initial_scid; tp_id_to_key(?TP_RETRY_SCID) -> retry_scid; tp_id_to_key(Id) -> {unknown, Id}. decode_tp_value(?TP_ORIGINAL_DCID, Value) -> Value; decode_tp_value(?TP_STATELESS_RESET_TOKEN, Value) -> Value; decode_tp_value(?TP_INITIAL_SCID, Value) -> Value; decode_tp_value(?TP_RETRY_SCID, Value) -> Value; decode_tp_value(?TP_DISABLE_ACTIVE_MIGRATION, <<>>) -> true; decode_tp_value(?TP_PREFERRED_ADDRESS, Value) -> Value; decode_tp_value(_, Value) -> %% Most parameters are varints {Int, _} = quic_varint:decode(Value), Int. %%==================================================================== %% TLS Message Framing %%==================================================================== %% @doc Encode a TLS handshake message with type and length. -spec encode_handshake_message(non_neg_integer(), binary()) -> binary(). encode_handshake_message(Type, Body) -> Length = byte_size(Body), <>. %% @doc Decode a TLS handshake message. %% Returns {Type, Body, Rest} or {error, Reason}. -spec decode_handshake_message(binary()) -> {ok, {non_neg_integer(), binary()}, binary()} | {error, term()}. decode_handshake_message(<>) -> {ok, {Type, Body}, Rest}; decode_handshake_message(<<_Type:8, Length:24, Data/binary>>) when byte_size(Data) < Length -> {error, incomplete}; decode_handshake_message(_) -> {error, invalid}. %%==================================================================== %% Internal Functions %%==================================================================== encode_extension(Type, Data) -> <>. parse_extensions(Data) -> parse_extensions(Data, #{}). parse_extensions(<<>>, Acc) -> {ok, Acc}; parse_extensions(<>, Acc) -> parse_extensions(Rest, maps:put(Type, Data, Acc)); parse_extensions(_, _) -> {error, invalid_extensions}. parse_server_key_share(<>) -> {ok, PubKey}; parse_server_key_share(<>) -> {ok, PubKey}; parse_server_key_share(_) -> {error, unsupported_key_share}. cipher_from_suite(?TLS_AES_128_GCM_SHA256) -> aes_128_gcm; cipher_from_suite(?TLS_AES_256_GCM_SHA384) -> aes_256_gcm; cipher_from_suite(?TLS_CHACHA20_POLY1305_SHA256) -> chacha20_poly1305; cipher_from_suite(_) -> aes_128_gcm. %%==================================================================== %% Server-side TLS Functions %%==================================================================== %% @doc Parse a ClientHello message. %% Returns a map with: %% - random: 32-byte client random %% - session_id: Legacy session ID %% - cipher_suites: List of cipher suites offered %% - extensions: Map of extensions %% - key_share: Client's public key for key exchange %% - server_name: SNI hostname (if present) %% - alpn_protocols: List of ALPN protocols (if present) %% - transport_params: QUIC transport parameters (if present) -spec parse_client_hello(binary()) -> {ok, map()} | {error, term()}. parse_client_hello(<< ?TLS_VERSION_1_2:16, % Legacy version Random:32/binary, SessionIdLen:8, SessionId:SessionIdLen/binary, CipherSuitesLen:16, CipherSuites:CipherSuitesLen/binary, CompressionLen:8, _Compression:CompressionLen/binary, ExtLen:16, Extensions:ExtLen/binary, _Rest/binary >>) -> %% Parse cipher suites Ciphers = parse_cipher_suites(CipherSuites), %% Parse extensions case parse_extensions(Extensions) of {ok, ExtMap} -> %% Extract key_share KeyShare = case maps:find(?EXT_KEY_SHARE, ExtMap) of {ok, KsData} -> parse_client_key_shares(KsData); error -> undefined end, %% Extract SNI ServerName = case maps:find(?EXT_SERVER_NAME, ExtMap) of {ok, SniData} -> parse_server_name_ext(SniData); error -> undefined end, %% Extract ALPN ALPNProtocols = case maps:find(?EXT_ALPN, ExtMap) of {ok, AlpnData} -> parse_alpn_ext(AlpnData); error -> [] end, %% Extract QUIC transport parameters TransportParams = case maps:find(?EXT_QUIC_TRANSPORT_PARAMS, ExtMap) of {ok, TpData} -> decode_transport_params(TpData); error -> #{} end, {ok, #{ random => Random, session_id => SessionId, cipher_suites => Ciphers, extensions => ExtMap, key_share => KeyShare, server_name => ServerName, alpn_protocols => ALPNProtocols, transport_params => TransportParams }}; {error, Reason} -> {error, Reason} end; parse_client_hello(_) -> {error, invalid_client_hello}. %% @doc Build a ServerHello message. %% Options: %% - random: Server random (32 bytes, generated if not provided) %% - session_id: Legacy session ID to echo %% - cipher_suite: Selected cipher suite %% - key_share: Server's public key -spec build_server_hello(map()) -> {binary(), binary()}. build_server_hello(Opts) -> %% Generate server random Random = maps:get(random, Opts, crypto:strong_rand_bytes(32)), %% Echo session ID from client SessionId = maps:get(session_id, Opts, <<>>), SessionIdLen = byte_size(SessionId), %% Selected cipher suite CipherSuite = maps:get(cipher_suite, Opts, ?TLS_AES_128_GCM_SHA256), %% Generate ECDHE key pair if not provided {PubKey, PrivKey} = case maps:find(key_pair, Opts) of {ok, {Pub, Priv}} -> {Pub, Priv}; error -> crypto:generate_key(ecdh, x25519) end, %% Build extensions Extensions = build_server_hello_extensions(PubKey, Opts), ExtLen = byte_size(Extensions), %% Build ServerHello ServerHello = << ?TLS_VERSION_1_2:16, % Legacy version (TLS 1.2) Random:32/binary, SessionIdLen:8, SessionId/binary, CipherSuite:16, 0:8, % Legacy compression method (null) ExtLen:16, Extensions/binary >>, %% Wrap with handshake header Message = encode_handshake_message(?TLS_SERVER_HELLO, ServerHello), {Message, PrivKey}. %% @doc Build EncryptedExtensions message. %% Options: %% - alpn: Selected ALPN protocol %% - transport_params: QUIC transport parameters -spec build_encrypted_extensions(map()) -> binary(). build_encrypted_extensions(Opts) -> Extensions = build_encrypted_extensions_list(Opts), ExtLen = byte_size(Extensions), Body = <>, encode_handshake_message(?TLS_ENCRYPTED_EXTENSIONS, Body). %% @doc Build Certificate message. %% Certs is a list of DER-encoded certificates (server cert first). -spec build_certificate(binary(), [binary()]) -> binary(). build_certificate(Context, Certs) -> CertList = build_cert_list(Certs), CertListLen = byte_size(CertList), ContextLen = byte_size(Context), Body = <>, encode_handshake_message(?TLS_CERTIFICATE, Body). %% @doc Build CertificateVerify message. %% PrivateKey is the server's private key. %% TranscriptHash is the hash of all handshake messages up to (not including) CertificateVerify. -spec build_certificate_verify(non_neg_integer(), crypto:key_id(), binary()) -> binary(). build_certificate_verify(SignatureAlgorithm, PrivateKey, TranscriptHash) -> %% Build the content to sign %% RFC 8446 Section 4.4.3: %% Content = 64 spaces + "TLS 1.3, server CertificateVerify" + 0x00 + TranscriptHash Spaces = binary:copy(<<32>>, 64), ContextString = <<"TLS 1.3, server CertificateVerify">>, Content = <>, %% Sign with the appropriate algorithm {SigAlg, HashAlg, SignOpts} = get_signature_params(SignatureAlgorithm), Signature = crypto:sign(SigAlg, HashAlg, Content, PrivateKey, SignOpts), SigLen = byte_size(Signature), Body = <>, encode_handshake_message(?TLS_CERTIFICATE_VERIFY, Body). %%==================================================================== %% Server-side Internal Functions %%==================================================================== parse_cipher_suites(<<>>) -> []; parse_cipher_suites(<>) -> [Suite | parse_cipher_suites(Rest)]. parse_client_key_shares(<>) -> parse_key_share_entries(Data); parse_client_key_shares(_) -> undefined. parse_key_share_entries(<<>>) -> []; parse_key_share_entries(<>) -> [{Group, KeyData} | parse_key_share_entries(Rest)]; parse_key_share_entries(_) -> []. parse_server_name_ext(<>) -> parse_server_name_list(Data); parse_server_name_ext(_) -> undefined. parse_server_name_list(<<0:8, NameLen:16, Name:NameLen/binary, _/binary>>) -> Name; % Type 0 = hostname parse_server_name_list(_) -> undefined. parse_alpn_ext(<>) -> parse_alpn_list(Data); parse_alpn_ext(_) -> []. parse_alpn_list(<<>>) -> []; parse_alpn_list(<>) -> [Proto | parse_alpn_list(Rest)]; parse_alpn_list(_) -> []. build_server_hello_extensions(PubKey, _Opts) -> %% Supported versions extension (mandatory for TLS 1.3) SupportedVersions = encode_extension(?EXT_SUPPORTED_VERSIONS, <>), %% Key share extension KeyShare = encode_extension(?EXT_KEY_SHARE, <>), <>. build_encrypted_extensions_list(Opts) -> %% ALPN extension (if ALPN was negotiated) ALPNExt = case maps:find(alpn, Opts) of {ok, ALPN} when is_binary(ALPN), byte_size(ALPN) > 0 -> ALPNLen = byte_size(ALPN), encode_extension(?EXT_ALPN, <<(ALPNLen + 1):16, ALPNLen:8, ALPN/binary>>); _ -> <<>> end, %% QUIC transport parameters TPExt = case maps:find(transport_params, Opts) of {ok, TP} when map_size(TP) > 0 -> TPData = encode_transport_params(TP), encode_extension(?EXT_QUIC_TRANSPORT_PARAMS, TPData); _ -> <<>> end, <>. build_cert_list([]) -> <<>>; build_cert_list([Cert | Rest]) -> CertLen = byte_size(Cert), RestCerts = build_cert_list(Rest), %% Each cert entry: cert_data<1..2^24-1> extensions<0..2^16-1> <>. get_signature_params(?SIG_RSA_PSS_RSAE_SHA256) -> {rsa, sha256, [{rsa_padding, rsa_pkcs1_pss_padding}, {rsa_pss_saltlen, -1}]}; get_signature_params(?SIG_RSA_PSS_RSAE_SHA384) -> {rsa, sha384, [{rsa_padding, rsa_pkcs1_pss_padding}, {rsa_pss_saltlen, -1}]}; get_signature_params(?SIG_RSA_PSS_RSAE_SHA512) -> {rsa, sha512, [{rsa_padding, rsa_pkcs1_pss_padding}, {rsa_pss_saltlen, -1}]}; get_signature_params(?SIG_ECDSA_SECP256R1_SHA256) -> {ecdsa, sha256, []}; get_signature_params(?SIG_ECDSA_SECP384R1_SHA384) -> {ecdsa, sha384, []}; get_signature_params(_) -> {rsa, sha256, [{rsa_padding, rsa_pkcs1_pss_padding}, {rsa_pss_saltlen, -1}]}.