Current section

Files

Jump to
quic src quic_keys.erl
Raw

src/quic_keys.erl

%%% -*- erlang -*-
%%%
%%% QUIC Key Derivation
%%% RFC 9001 Section 5 - Packet Protection
%%%
%%% Copyright (c) 2024-2026 Benoit Chesneau
%%% Apache License 2.0
%%%
%%% @doc QUIC key derivation for packet protection.
%%%
%%% This module derives the cryptographic keys used for QUIC packet
%%% protection at each encryption level (Initial, Handshake, Application).
%%%
%%% == Initial Keys ==
%%%
%%% Initial keys are derived from the Destination Connection ID using
%%% a fixed salt defined in RFC 9001.
%%%
%%% == Key Derivation ==
%%%
%%% Keys are derived using HKDF-Expand-Label with the following labels:
%%% - "quic key" for the AEAD key
%%% - "quic iv" for the AEAD IV/nonce
%%% - "quic hp" for header protection key
%%%
-module(quic_keys).
-include("quic.hrl").
-export([
derive_initial_secret/1,
derive_initial_secret/2,
derive_initial_client/1,
derive_initial_client/2,
derive_initial_server/1,
derive_initial_server/2,
derive_keys/2,
derive_traffic_keys/1,
%% Key Update (RFC 9001 Section 6)
derive_updated_secret/2,
derive_updated_keys/2
]).
-export_type([keys/0]).
-type keys() :: {Key :: binary(), IV :: binary(), HP :: binary()}.
%%====================================================================
%% API
%%====================================================================
%% @doc Derive the initial secret from DCID using QUIC v1 salt.
-spec derive_initial_secret(binary()) -> binary().
derive_initial_secret(DCID) ->
derive_initial_secret(DCID, ?QUIC_VERSION_1).
%% @doc Derive the initial secret from DCID.
%% Version determines which salt to use.
-spec derive_initial_secret(binary(), non_neg_integer()) -> binary().
derive_initial_secret(DCID, Version) ->
Salt = initial_salt(Version),
quic_hkdf:extract(Salt, DCID).
%% @doc Derive initial client keys from DCID.
%% Returns {Key, IV, HP} for client Initial packets.
-spec derive_initial_client(binary()) -> keys().
derive_initial_client(DCID) ->
derive_initial_client(DCID, ?QUIC_VERSION_1).
%% @doc Derive initial client keys from DCID with version.
-spec derive_initial_client(binary(), non_neg_integer()) -> keys().
derive_initial_client(DCID, Version) ->
InitialSecret = derive_initial_secret(DCID, Version),
ClientSecret = quic_hkdf:expand_label(InitialSecret, ?QUIC_LABEL_CLIENT_IN, <<>>, 32),
derive_traffic_keys(ClientSecret).
%% @doc Derive initial server keys from DCID.
%% Returns {Key, IV, HP} for server Initial packets.
-spec derive_initial_server(binary()) -> keys().
derive_initial_server(DCID) ->
derive_initial_server(DCID, ?QUIC_VERSION_1).
%% @doc Derive initial server keys from DCID with version.
-spec derive_initial_server(binary(), non_neg_integer()) -> keys().
derive_initial_server(DCID, Version) ->
InitialSecret = derive_initial_secret(DCID, Version),
ServerSecret = quic_hkdf:expand_label(InitialSecret, ?QUIC_LABEL_SERVER_IN, <<>>, 32),
derive_traffic_keys(ServerSecret).
%% @doc Derive keys from a traffic secret.
%% Returns {Key, IV, HP} for the given secret.
-spec derive_keys(binary(), aes_128_gcm | aes_256_gcm | chacha20_poly1305) -> keys().
derive_keys(Secret, Cipher) ->
{Hash, KeyLen, HPLen} = cipher_params(Cipher),
Key = quic_hkdf:expand_label(Hash, Secret, ?QUIC_LABEL_QUIC_KEY, <<>>, KeyLen),
IV = quic_hkdf:expand_label(Hash, Secret, ?QUIC_LABEL_QUIC_IV, <<>>, 12),
HP = quic_hkdf:expand_label(Hash, Secret, ?QUIC_LABEL_QUIC_HP, <<>>, HPLen),
{Key, IV, HP}.
%% @doc Derive traffic keys (Key, IV, HP) from a traffic secret.
%% Uses AES-128-GCM key sizes (16-byte key, 12-byte IV, 16-byte HP).
%% This is used for Initial and Handshake encryption levels.
-spec derive_traffic_keys(binary()) -> keys().
derive_traffic_keys(Secret) ->
derive_keys(Secret, aes_128_gcm).
%% @doc Derive an updated application secret for key update (RFC 9001 Section 6).
%% The next application secret is computed as:
%% updated_secret = HKDF-Expand-Label(current_secret, "quic ku", "", hash_len)
%% where "quic ku" is the label for key update.
-spec derive_updated_secret(binary(), aes_128_gcm | aes_256_gcm | chacha20_poly1305) -> binary().
derive_updated_secret(CurrentSecret, aes_256_gcm) ->
%% AES-256-GCM uses SHA-384, so secret length is 48 bytes
quic_hkdf:expand_label(sha384, CurrentSecret, <<"quic ku">>, <<>>, 48);
derive_updated_secret(CurrentSecret, _Cipher) ->
%% AES-128-GCM and ChaCha20-Poly1305 use SHA-256, so secret length is 32 bytes
quic_hkdf:expand_label(sha256, CurrentSecret, <<"quic ku">>, <<>>, 32).
%% @doc Derive updated keys from an updated application secret.
%% This performs the full key update: derives the new secret and then derives keys.
%% Returns {UpdatedSecret, {Key, IV, HP}}.
-spec derive_updated_keys(binary(), aes_128_gcm | aes_256_gcm | chacha20_poly1305) ->
{UpdatedSecret :: binary(), keys()}.
derive_updated_keys(CurrentSecret, Cipher) ->
UpdatedSecret = derive_updated_secret(CurrentSecret, Cipher),
Keys = derive_keys(UpdatedSecret, Cipher),
{UpdatedSecret, Keys}.
%%====================================================================
%% Internal Functions
%%====================================================================
%% Get cipher parameters: {Hash, KeyLen, HPLen}
cipher_params(aes_128_gcm) -> {sha256, 16, 16};
cipher_params(aes_256_gcm) -> {sha384, 32, 32};
cipher_params(chacha20_poly1305) -> {sha256, 32, 32}.
%% Get the initial salt for a QUIC version
initial_salt(?QUIC_VERSION_1) ->
?QUIC_V1_INITIAL_SALT;
initial_salt(?QUIC_VERSION_2) ->
?QUIC_V2_INITIAL_SALT;
initial_salt(_) ->
%% Default to v1 salt for unknown versions
?QUIC_V1_INITIAL_SALT.