-module(nquic_tls_server).
-moduledoc """
Server-side TLS 1.3 handshake flow for QUIC per RFC 9001.
Processes ClientHello, builds the server handshake flight
(EncryptedExtensions, Certificate, CertificateVerify, Finished) for
full handshakes and the abbreviated EE+Finished flight for PSK
resumption, and verifies the client Finished. Codec helpers shared
with the client live in `nquic_tls`; PSK / NewSessionTicket helpers
remain there too because they straddle both roles.
""".
-include("nquic_tls.hrl").
-include_lib("public_key/include/public_key.hrl").
-export([
make_server_handshake_flight/6,
make_server_handshake_flight_psk/4,
process_client_hello/3,
process_client_hello/4,
validate_psk_offer/4,
verify_client_finished/3,
verify_client_finished/4
]).
-export([
encode_extensions/1,
find_alpn/1,
find_cipher_match/2,
find_match/2,
make_certificate_message/2,
parse_alpn_items/1,
parse_cipher_suites/1,
select_alpn/2,
select_cipher/1, select_cipher/2
]).
-record(server_hello, {
server_version,
random,
session_id,
cipher_suite,
compression_method,
extensions
}).
-record(key_share_server_hello, {
server_share
}).
-doc "Build the server handshake flight (EncryptedExtensions, Certificate, CertificateVerify, Finished).".
-spec make_server_handshake_flight(
binary(), map(), map(), binary(), [binary()], public_key:private_key()
) ->
{ok, binary(), map(), map()} | {error, nquic_error:any_reason()}.
make_server_handshake_flight(HandshakeSecret, Keys, State, CertDER, CertChain, PrivKey) ->
try
maybe
Ctx0 = maps:get(transcript_ctx, Keys),
ServerSecret = maps:get(server_secret, Keys),
TransportParams = maps:get(transport_params, State),
Cipher = maps:get(cipher, State, aes_128_gcm),
Version = maps:get(quic_version, State, 1),
Hash = nquic_keys:cipher_to_hash(Cipher),
HashLen = nquic_tls:hash_length(Hash),
TPBin = nquic_transport:encode(TransportParams),
ExtID = 57,
ExtLen = byte_size(TPBin),
ExtData0 = <<ExtID:16, ExtLen:16, TPBin/binary>>,
ExtData =
case maps:get(selected_alpn, State, undefined) of
undefined ->
ExtData0;
Proto ->
P = <<(byte_size(Proto)):8, Proto/binary>>,
ALPNData = <<(byte_size(P)):16, P/binary>>,
ALPNBin = <<16:16, (byte_size(ALPNData)):16, ALPNData/binary>>,
<<ExtData0/binary, ALPNBin/binary>>
end,
EELen = byte_size(ExtData),
EEBody = <<EELen:16, ExtData/binary>>,
EEHeader = <<8, (byte_size(EEBody)):24>>,
EEBin = <<EEHeader/binary, EEBody/binary>>,
CertMsg = make_certificate_message(CertDER, CertChain),
Ctx1 = crypto:hash_update(Ctx0, EEBin),
Ctx2 = crypto:hash_update(Ctx1, CertMsg),
TranscriptHashCV = crypto:hash_final(Ctx2),
{ok, CertVerifyMsg} ?= make_certificate_verify_message(PrivKey, TranscriptHashCV),
Ctx3 = crypto:hash_update(Ctx2, CertVerifyMsg),
FinishedKey = nquic_keys:qhkdf_expand(ServerSecret, <<"finished">>, <<>>, HashLen),
TranscriptHashFin = crypto:hash_final(Ctx3),
VerifyData = crypto:mac(hmac, Hash, FinishedKey, TranscriptHashFin),
FinLen = byte_size(VerifyData),
FinBody = <<FinLen:24, VerifyData/binary>>,
FinHeader = <<20, FinBody/binary>>,
FinBin = FinHeader,
Ctx4 = crypto:hash_update(Ctx3, FinBin),
TranscriptHashFinal = crypto:hash_final(Ctx4),
{ClientAppSecret, ServerAppSecret} = nquic_keys:master_secrets(
HandshakeSecret, TranscriptHashFinal, Hash
),
{CKey, CIV, CHP} = nquic_keys:derive_packet_protection(
ClientAppSecret, Cipher, Version
),
{SKey, SIV, SHP} = nquic_keys:derive_packet_protection(
ServerAppSecret, Cipher, Version
),
AppKeys = #{
client_secret => ClientAppSecret,
server_secret => ServerAppSecret,
client_key => CKey,
client_iv => CIV,
client_hp => CHP,
server_key => SKey,
server_iv => SIV,
server_hp => SHP,
transcript_ctx => Ctx4,
quic_version => Version
},
FlightBin = <<EEBin/binary, CertMsg/binary, CertVerifyMsg/binary, FinBin/binary>>,
ClientHSSecret = maps:get(client_secret, Keys),
NewState = State#{transcript_ctx => Ctx4, client_secret => ClientHSSecret},
{ok, FlightBin, AppKeys, NewState}
end
catch
error:Reason -> {error, {flight_generation_failed, Reason}}
end.
-doc """
Build the server handshake flight for PSK resumption (no Certificate/CertificateVerify).
EncryptedExtensions + Finished only. Optionally includes early_data extension
to signal 0-RTT acceptance.
""".
-spec make_server_handshake_flight_psk(
binary(), map(), map(), boolean()
) -> {ok, binary(), map(), map()} | {error, term()}.
make_server_handshake_flight_psk(HandshakeSecret, Keys, State, AcceptEarlyData) ->
try
Ctx0 = maps:get(transcript_ctx, Keys),
ServerSecret = maps:get(server_secret, Keys),
Cipher = maps:get(cipher, State, aes_128_gcm),
Version = maps:get(quic_version, State, 1),
Hash = nquic_keys:cipher_to_hash(Cipher),
HashLen = nquic_tls:hash_length(Hash),
TransportParams = maps:get(transport_params, State),
TPBin = nquic_transport:encode(TransportParams),
ExtID = 57,
ExtLen = byte_size(TPBin),
ExtData0 = <<ExtID:16, ExtLen:16, TPBin/binary>>,
ExtData1 =
case maps:get(selected_alpn, State, undefined) of
undefined ->
ExtData0;
Proto ->
P = <<(byte_size(Proto)):8, Proto/binary>>,
ALPNData = <<(byte_size(P)):16, P/binary>>,
ALPNBin = <<16:16, (byte_size(ALPNData)):16, ALPNData/binary>>,
<<ExtData0/binary, ALPNBin/binary>>
end,
ExtData =
case AcceptEarlyData of
true -> <<ExtData1/binary, 0, 42, 0, 0>>;
false -> ExtData1
end,
EELen = byte_size(ExtData),
EEBody = <<EELen:16, ExtData/binary>>,
EEHeader = <<8, (byte_size(EEBody)):24>>,
EEBin = <<EEHeader/binary, EEBody/binary>>,
Ctx1 = crypto:hash_update(Ctx0, EEBin),
FinishedKey = nquic_keys:qhkdf_expand(ServerSecret, <<"finished">>, <<>>, HashLen),
TranscriptHashFin = crypto:hash_final(Ctx1),
VerifyData = crypto:mac(hmac, Hash, FinishedKey, TranscriptHashFin),
FinLen = byte_size(VerifyData),
FinBody = <<FinLen:24, VerifyData/binary>>,
FinBin = <<20, FinBody/binary>>,
Ctx2 = crypto:hash_update(Ctx1, FinBin),
TranscriptHashFinal = crypto:hash_final(Ctx2),
{ClientAppSecret, ServerAppSecret} = nquic_keys:master_secrets(
HandshakeSecret, TranscriptHashFinal, Hash
),
{CKey, CIV, CHP} = nquic_keys:derive_packet_protection(ClientAppSecret, Cipher, Version),
{SKey, SIV, SHP} = nquic_keys:derive_packet_protection(ServerAppSecret, Cipher, Version),
AppKeys = #{
client_secret => ClientAppSecret,
server_secret => ServerAppSecret,
client_key => CKey,
client_iv => CIV,
client_hp => CHP,
server_key => SKey,
server_iv => SIV,
server_hp => SHP,
transcript_ctx => Ctx2,
quic_version => Version
},
FlightBin = <<EEBin/binary, FinBin/binary>>,
ClientHSSecret = maps:get(client_secret, Keys),
NewState = State#{transcript_ctx => Ctx2, client_secret => ClientHSSecret},
{ok, FlightBin, AppKeys, NewState}
catch
error:Reason -> {error, {psk_flight_generation_failed, Reason}}
end.
-doc "Process a ClientHello, generate a ServerHello, and derive handshake secrets.".
-spec process_client_hello(binary(), nquic_transport:params(), [binary()] | undefined) ->
{ok, binary(), map(), map()} | {error, term()}.
process_client_hello(ClientHelloBin, TransportParams, SupportedALPNs) ->
process_client_hello(ClientHelloBin, TransportParams, SupportedALPNs, #{}).
-doc """
Process a ClientHello with options.
Opts may include `psk_selected => Index` to include pre_shared_key in ServerHello.
""".
-spec process_client_hello(binary(), nquic_transport:params(), [binary()] | undefined, map()) ->
{ok, binary(), map(), map()} | {error, term()}.
process_client_hello(ClientHelloBin, TransportParams, SupportedALPNs, Opts) ->
try
maybe
{ok, SessionID, ClientKeyShare, RemoteTP, ClientALPNs, ClientCiphers, PSKInfo} ?=
parse_client_hello(ClientHelloBin),
{ok, SelectedALPN} ?= select_alpn(ClientALPNs, SupportedALPNs),
Preferred = maps:get(cipher_suites, Opts, undefined),
{ok, Cipher} ?= select_cipher(ClientCiphers, Preferred),
Hash = nquic_keys:cipher_to_hash(Cipher),
{ServerPubKey, ServerPrivKey} = crypto:generate_key(ecdh, x25519),
BaseExtensions = #{
server_hello_versions => #server_hello_versions{versions = {3, 4}},
key_share => #key_share_server_hello{
server_share = #key_share_entry{group = x25519, key_exchange = ServerPubKey}
}
},
Extensions =
case maps:get(psk_selected, Opts, undefined) of
undefined -> BaseExtensions;
Index -> BaseExtensions#{pre_shared_key => Index}
end,
SH = #server_hello{
server_version = {3, 3},
random = crypto:strong_rand_bytes(32),
session_id = SessionID,
cipher_suite = nquic_tls:encode_cipher_suite(Cipher),
compression_method = 0,
extensions = Extensions
},
ServerHelloBin = manual_encode_server_hello(SH),
SharedSecret = crypto:compute_key(ecdh, ClientKeyShare, ServerPrivKey, x25519),
Ctx0 = crypto:hash_init(Hash),
Ctx1 = crypto:hash_update(Ctx0, ClientHelloBin),
Ctx2 = crypto:hash_update(Ctx1, ServerHelloBin),
TranscriptHash = crypto:hash_final(Ctx2),
PSKValue = maps:get(psk_value, Opts, undefined),
{ClientHSSecret, ServerHSSecret, HandshakeSecret} = nquic_keys:handshake_secrets(
SharedSecret, TranscriptHash, Hash, PSKValue
),
Version = maps:get(quic_version, Opts, 1),
{CKey, CIV, CHP} = nquic_keys:derive_packet_protection(ClientHSSecret, Cipher, Version),
{SKey, SIV, SHP} = nquic_keys:derive_packet_protection(ServerHSSecret, Cipher, Version),
Keys = #{
client_secret => ClientHSSecret,
server_secret => ServerHSSecret,
client_key => CKey,
client_iv => CIV,
client_hp => CHP,
server_key => SKey,
server_iv => SIV,
server_hp => SHP,
handshake_secret => HandshakeSecret,
transcript_ctx => Ctx2,
remote_params => RemoteTP,
cipher => Cipher,
quic_version => Version
},
State0 = #{
priv_key => ServerPrivKey,
role => server,
transport_params => TransportParams,
client_secret => ClientHSSecret,
server_secret => ServerHSSecret,
handshake_secret => HandshakeSecret,
transcript_ctx => Ctx2,
remote_params => RemoteTP,
selected_alpn => SelectedALPN,
cipher => Cipher,
quic_version => Version
},
State =
case PSKInfo of
undefined ->
State0;
_ ->
State0#{
psk_info => PSKInfo,
client_hello_bin => ClientHelloBin
}
end,
{ok, ServerHelloBin, Keys, State}
end
catch
error:Reason -> {error, {processing_failed, Reason}}
end.
-doc """
Validate a PSK offer from a ClientHello against the server's ticket.
Decrypts the first matching identity, verifies the binder, and returns
the PSK and whether 0-RTT should be accepted.
StaticKey is the server's ticket encryption key.
ClientHelloBin is the raw ClientHello (needed for binder verification).
""".
-spec validate_psk_offer(map(), binary(), binary(), atom()) ->
{ok, binary(), atom(), boolean(), binary()} | {error, term()}.
validate_psk_offer(PSKInfo, ClientHelloBin, StaticKey, NegCipher) ->
#{identities := Identities, binders := Binders, early_data := HasEarlyData} = PSKInfo,
validate_psk_identities(
Identities, Binders, ClientHelloBin, StaticKey, NegCipher, HasEarlyData
).
-doc "Verify the client Finished message using the client handshake traffic secret.".
-spec verify_client_finished(binary(), binary(), crypto:hash_state()) ->
ok | {error, nquic_error:any_reason()}.
verify_client_finished(Data, ClientSecret, TranscriptCtx) ->
verify_client_finished(Data, ClientSecret, TranscriptCtx, aes_128_gcm).
-doc "Verify the client Finished message with an explicit cipher suite.".
-spec verify_client_finished(
binary(), binary(), crypto:hash_state(), aes_128_gcm | aes_256_gcm | chacha20_poly1305
) -> ok | {error, term()}.
verify_client_finished(Data, ClientSecret, TranscriptCtx, Cipher) ->
try
Hash = nquic_keys:cipher_to_hash(Cipher),
HashLen = nquic_tls:hash_length(Hash),
<<20:8, Len:24, VerifyData:Len/binary>> = Data,
FinishedKey = nquic_keys:qhkdf_expand(ClientSecret, <<"finished">>, <<>>, HashLen),
TranscriptHash = crypto:hash_final(TranscriptCtx),
ExpectedData = crypto:mac(hmac, Hash, FinishedKey, TranscriptHash),
if
VerifyData =:= ExpectedData -> ok;
true -> {error, client_finished_verification_failed}
end
catch
error:{badmatch, _} -> {error, malformed_finished}
end.
%%%-----------------------------------------------------------------------------
%% INTERNAL SERVERHELLO ENCODING
%%%-----------------------------------------------------------------------------
-spec encode_extensions(map()) -> binary().
encode_extensions(Exts) ->
V =
case maps:get(server_hello_versions, Exts, undefined) of
#server_hello_versions{versions = {Maj, Min}} ->
<<0, 43, 0, 2, Maj, Min>>;
_ ->
<<>>
end,
K =
case maps:get(key_share, Exts, undefined) of
#key_share_server_hello{
server_share = #key_share_entry{group = x25519, key_exchange = Key}
} ->
Group = 16#001d,
KLen = byte_size(Key),
Entry = <<Group:16, KLen:16, Key/binary>>,
ExtLen = byte_size(Entry),
<<0, 51, ExtLen:16, Entry/binary>>;
_ ->
<<>>
end,
PSK =
case maps:get(pre_shared_key, Exts, undefined) of
undefined ->
<<>>;
SelectedIndex when is_integer(SelectedIndex) ->
<<0, 41, 0, 2, SelectedIndex:16>>
end,
<<V/binary, K/binary, PSK/binary>>.
-spec manual_encode_server_hello(#server_hello{}) -> binary().
manual_encode_server_hello(SH) ->
#server_hello{
server_version = {Major, Minor},
random = Random,
session_id = SessionID,
cipher_suite = CipherSuite,
compression_method = CompMethod,
extensions = ExtensionsMap
} = SH,
VerBin = <<Major, Minor>>,
SIDLen = byte_size(SessionID),
SIDBin = <<SIDLen:8, SessionID/binary>>,
CSBin = CipherSuite,
CompBin = <<CompMethod:8>>,
ExtsBin = encode_extensions(ExtensionsMap),
ExtsLen = byte_size(ExtsBin),
ExtsLenBin = <<ExtsLen:16>>,
Body =
<<VerBin/binary, Random/binary, SIDBin/binary, CSBin/binary, CompBin/binary,
ExtsLenBin/binary, ExtsBin/binary>>,
Type = 2,
Len = byte_size(Body),
<<Type:8, Len:24, Body/binary>>.
%%%-----------------------------------------------------------------------------
%% INTERNAL CERTIFICATE / CERTIFICATEVERIFY
%%%-----------------------------------------------------------------------------
-spec encode_cert_entries([binary()]) -> iolist().
encode_cert_entries([]) ->
[];
encode_cert_entries([CertDER | Rest]) ->
CertLen = byte_size(CertDER),
[<<CertLen:24, CertDER/binary, 0:16>> | encode_cert_entries(Rest)].
-spec make_certificate_message(binary(), [binary()]) -> binary().
make_certificate_message(LeafDER, ChainDERs) ->
Entries = encode_cert_entries([LeafDER | ChainDERs]),
ListLen = iolist_size(Entries),
Body = [<<0:8, ListLen:24>>, Entries],
BodyBin = iolist_to_binary(Body),
MsgLen = byte_size(BodyBin),
<<11:8, MsgLen:24, BodyBin/binary>>.
-spec make_certificate_verify_message(public_key:private_key(), binary()) ->
{ok, binary()} | {error, nquic_error:any_reason()}.
make_certificate_verify_message(PrivKey, Hash) ->
Pad = binary:copy(<<16#20>>, 64),
Context = <<"TLS 1.3, server CertificateVerify">>,
Input = <<Pad/binary, Context/binary, 0:8, Hash/binary>>,
maybe
{ok, {Alg, Sig}} ?= sign(PrivKey, Input),
SigLen = byte_size(Sig),
Body = <<Alg:16, SigLen:16, Sig/binary>>,
MsgLen = byte_size(Body),
{ok, <<15:8, MsgLen:24, Body/binary>>}
end.
-spec sign(public_key:private_key(), binary()) ->
{ok, {non_neg_integer(), binary()}} | {error, nquic_error:any_reason()}.
sign(#'RSAPrivateKey'{} = Key, Data) ->
Sig = public_key:sign(Data, sha256, Key, [
{rsa_padding, rsa_pkcs1_pss_padding}, {rsa_pss_saltlen, -1}
]),
{ok, {16#0804, Sig}};
sign(#'ECPrivateKey'{parameters = {namedCurve, {1, 2, 840, 10045, 3, 1, 7}}} = Key, Data) ->
Sig = public_key:sign(Data, sha256, Key),
{ok, {16#0403, Sig}};
sign(_, _) ->
{error, unsupported_key_type}.
%%%-----------------------------------------------------------------------------
%% INTERNAL CLIENTHELLO PARSING
%%%-----------------------------------------------------------------------------
-spec client_hello_key_share(binary() | undefined) ->
{ok, binary()} | {error, nquic_error:any_reason()}.
client_hello_key_share(undefined) ->
{error, key_share_not_found};
client_hello_key_share(Bin) ->
find_x25519(Bin).
-spec client_hello_quic_params(
{ok, nquic_transport:params()} | {error, nquic_error:any_reason()}
) ->
{ok, nquic_transport:params()} | {error, nquic_error:any_reason()}.
client_hello_quic_params({ok, P}) ->
{ok, P};
client_hello_quic_params({error, {tls_alert, _} = Alert}) ->
{error, Alert};
client_hello_quic_params({error, Reason}) ->
{error, {transport_parameter_error, Reason}}.
-spec find_alpn(map()) -> [binary()] | undefined.
find_alpn(ExtMap) ->
case maps:get(16, ExtMap, undefined) of
undefined -> undefined;
Bin -> parse_alpn_list(Bin)
end.
-spec find_cipher_match([atom()], [atom()]) -> atom() | undefined.
find_cipher_match([], _) ->
undefined;
find_cipher_match([C | Rest], ClientCiphers) ->
case lists:member(C, ClientCiphers) of
true -> C;
false -> find_cipher_match(Rest, ClientCiphers)
end.
-spec find_match([binary()], [binary()]) -> binary() | undefined.
find_match([], _) ->
undefined;
find_match([P | Rest], ServerProtos) ->
case lists:member(P, ServerProtos) of
true -> P;
false -> find_match(Rest, ServerProtos)
end.
-spec find_x25519(binary()) ->
{ok, binary()} | {error, nquic_error:any_reason()}.
find_x25519(<<VecLen:16, Rest/binary>>) when byte_size(Rest) == VecLen ->
find_x25519_in_vector(Rest);
find_x25519(_) ->
{error, invalid_key_share_format}.
-spec find_x25519_in_vector(binary()) ->
{ok, binary()} | {error, nquic_error:any_reason()}.
find_x25519_in_vector(<<>>) ->
{error, x25519_not_found};
find_x25519_in_vector(<<Group:16, Len:16, Key:Len/binary, Rest/binary>>) ->
case Group of
16#001d -> {ok, Key};
_ -> find_x25519_in_vector(Rest)
end.
-spec parse_alpn_items(binary()) -> [binary()].
parse_alpn_items(<<>>) -> [];
parse_alpn_items(<<Len:8, Proto:Len/binary, Rest/binary>>) -> [Proto | parse_alpn_items(Rest)].
-spec parse_alpn_list(binary()) -> [binary()].
parse_alpn_list(<<Len:16, Rest/binary>>) when byte_size(Rest) == Len ->
parse_alpn_items(Rest).
-spec parse_cipher_suites(binary()) -> [aes_128_gcm | aes_256_gcm | chacha20_poly1305].
parse_cipher_suites(<<>>) ->
[];
parse_cipher_suites(<<Suite:2/binary, Rest/binary>>) ->
case Suite of
<<19, 1>> -> [aes_128_gcm | parse_cipher_suites(Rest)];
<<19, 2>> -> [aes_256_gcm | parse_cipher_suites(Rest)];
<<19, 3>> -> [chacha20_poly1305 | parse_cipher_suites(Rest)];
_ -> parse_cipher_suites(Rest)
end.
-spec parse_client_hello(binary()) ->
{ok, binary(), binary(), nquic_transport:params(), [binary()] | undefined,
[aes_128_gcm | aes_256_gcm | chacha20_poly1305], map() | undefined}
| {error, nquic_error:any_reason()}.
parse_client_hello(<<Type:8, Len:24, Body:Len/binary>>) when Type =:= 1 ->
<<_Version:2/binary, _Random:32/binary, Rest1/binary>> = Body,
{SessionID, Rest2} = nquic_tls:parse_vec8(Rest1),
{CipherSuitesBin, Rest3} = nquic_tls:parse_vec16(Rest2),
{_CompMethods, Rest4} = nquic_tls:parse_vec8(Rest3),
<<ExtLen:16, ExtData:ExtLen/binary>> = Rest4,
ExtMap = nquic_tls:parse_extensions_recursive(ExtData),
maybe
{ok, KeyShare} ?= client_hello_key_share(maps:get(51, ExtMap, undefined)),
{ok, TP} ?= client_hello_quic_params(nquic_tls:find_quic_params(ExtMap, client)),
ALPN = find_alpn(ExtMap),
Ciphers = parse_cipher_suites(CipherSuitesBin),
PSKData = nquic_tls:parse_psk_extension(ExtMap),
HasDHEKE = nquic_tls:has_psk_dhe_ke_mode(ExtMap),
HasEarlyData = maps:is_key(42, ExtMap),
PSKInfo =
case {PSKData, HasDHEKE} of
{{ok, Identities, Binders}, true} ->
#{identities => Identities, binders => Binders, early_data => HasEarlyData};
_ ->
undefined
end,
{ok, SessionID, KeyShare, TP, ALPN, Ciphers, PSKInfo}
end;
parse_client_hello(_) ->
{error, invalid_client_hello}.
-spec select_alpn([binary()] | undefined, [binary()] | undefined) ->
{ok, binary() | undefined} | {error, nquic_error:any_reason()}.
select_alpn(_, undefined) ->
{ok, undefined};
select_alpn(undefined, _ServerProtos) ->
{error, {tls_alert, no_application_protocol}};
select_alpn([], _ServerProtos) ->
{error, {tls_alert, no_application_protocol}};
select_alpn(ClientProtos, ServerProtos) ->
case find_match(ClientProtos, ServerProtos) of
undefined ->
{error, {tls_alert, no_application_protocol}};
Match ->
{ok, Match}
end.
-spec select_cipher([aes_128_gcm | aes_256_gcm | chacha20_poly1305]) ->
{ok, aes_128_gcm | aes_256_gcm | chacha20_poly1305}
| {error, nquic_error:any_reason()}.
select_cipher(ClientCiphers) ->
select_cipher(ClientCiphers, undefined).
-spec select_cipher(
[aes_128_gcm | aes_256_gcm | chacha20_poly1305],
[aes_128_gcm | aes_256_gcm | chacha20_poly1305] | undefined
) ->
{ok, aes_128_gcm | aes_256_gcm | chacha20_poly1305}
| {error, nquic_error:any_reason()}.
select_cipher(ClientCiphers, undefined) ->
select_cipher(ClientCiphers, [aes_128_gcm, aes_256_gcm, chacha20_poly1305]);
select_cipher(ClientCiphers, Preferred) when is_list(Preferred), Preferred =/= [] ->
case find_cipher_match(Preferred, ClientCiphers) of
undefined -> {error, {tls_alert, handshake_failure}};
Cipher -> {ok, Cipher}
end.
%%%-----------------------------------------------------------------------------
%% INTERNAL PSK IDENTITY VALIDATION
%%%-----------------------------------------------------------------------------
-spec compute_binders_wire_len([binary()]) -> non_neg_integer().
compute_binders_wire_len(Binders) ->
ContentLen = lists:foldl(fun(B, Acc) -> Acc + 1 + byte_size(B) end, 0, Binders),
erlang:floor(2 + ContentLen).
-spec validate_psk_identities(
[{binary(), non_neg_integer()}],
[binary()],
binary(),
binary(),
atom(),
boolean()
) -> {ok, binary(), atom(), boolean(), binary()} | {error, term()}.
validate_psk_identities([], _, _, _, _, _) ->
{error, no_matching_psk};
validate_psk_identities(
[{Identity, _ObfAge} | RestIds],
[Binder | RestBinders],
ClientHelloBin,
StaticKey,
NegCipher,
HasEarlyData
) ->
case nquic_tls:decrypt_ticket(Identity, StaticKey) of
{ok, PSK, TicketCipher} ->
case TicketCipher =:= NegCipher of
false ->
validate_psk_identities(
RestIds,
RestBinders,
ClientHelloBin,
StaticKey,
NegCipher,
HasEarlyData
);
true ->
Hash = nquic_keys:cipher_to_hash(NegCipher),
HashLen = nquic_tls:hash_length(Hash),
BindersLen = compute_binders_wire_len(
[Binder | RestBinders]
),
PartialCH = nquic_tls:extract_partial_client_hello(
ClientHelloBin, BindersLen
),
case nquic_tls:verify_psk_binder(PSK, PartialCH, Binder, Hash, HashLen) of
ok ->
{ok, PSK, TicketCipher, HasEarlyData, Identity};
{error, _} ->
{error, binder_verification_failed}
end
end;
{error, _} ->
validate_psk_identities(
RestIds,
RestBinders,
ClientHelloBin,
StaticKey,
NegCipher,
HasEarlyData
)
end;
validate_psk_identities(_, _, _, _, _, _) ->
{error, psk_identity_binder_mismatch}.