Perform initial AEAD validation before creating a channel

We let port to create qrx object and use it for
packet validation. If packet validates, we then
create channel and pass pre-created qrx to channel's
constructor.

Co-authored-by: Andrew Dinh <andrewd@openssl.org>

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/26610)
This commit is contained in:
Alexandr Nedvedicky 2025-02-02 17:40:25 +01:00 committed by Neil Horman
parent 029d85869f
commit c82c1dbbbb
7 changed files with 372 additions and 42 deletions

View File

@ -116,6 +116,7 @@ typedef struct quic_channel_args_st {
QUIC_LCIDM *lcidm;
/* SRTM to register SRTs with. */
QUIC_SRTM *srtm;
OSSL_QRX *qrx;
int is_server;
SSL *tls;
@ -123,6 +124,8 @@ typedef struct quic_channel_args_st {
/* Whether to use qlog. */
int use_qlog;
int is_tserver_ch;
/* Title to use for the qlog session, or NULL. */
const char *qlog_title;
} QUIC_CHANNEL_ARGS;
@ -177,6 +180,7 @@ typedef struct quic_terminate_cause_st {
*/
QUIC_CHANNEL *ossl_quic_channel_alloc(const QUIC_CHANNEL_ARGS *args);
int ossl_quic_channel_init(QUIC_CHANNEL *ch);
void ossl_quic_channel_bind_qrx(QUIC_CHANNEL *tserver_ch, OSSL_QRX *qrx);
/* No-op if ch is NULL. */

View File

@ -31,6 +31,7 @@ typedef struct quic_reactor_st QUIC_REACTOR;
typedef struct quic_reactor_wait_ctx_st QUIC_REACTOR_WAIT_CTX;
typedef struct ossl_statm_st OSSL_STATM;
typedef struct quic_demux_st QUIC_DEMUX;
typedef struct ossl_qrx_st OSSL_QRX;
typedef struct ossl_qrx_pkt_st OSSL_QRX_PKT;
typedef struct ossl_qtx_pkt_st OSSL_QTX_PKT;
typedef struct quic_tick_result_st QUIC_TICK_RESULT;

View File

@ -23,7 +23,6 @@
* QUIC Record Layer - RX
* ======================
*/
typedef struct ossl_qrx_st OSSL_QRX;
typedef struct ossl_qrx_args_st {
OSSL_LIB_CTX *libctx;
@ -321,6 +320,8 @@ int ossl_qrx_set_late_validation_cb(OSSL_QRX *qrx,
* establish a new connection.
*/
void ossl_qrx_inject_urxe(OSSL_QRX *qrx, QUIC_URXE *e);
int ossl_qrx_validate_initial_packet(OSSL_QRX *qrx, QUIC_URXE *urxe,
const QUIC_CONN_ID *dcid);
/*
* Decryption of 1-RTT packets must be explicitly enabled by calling this

View File

@ -295,23 +295,44 @@ static int ch_init(QUIC_CHANNEL *ch)
ossl_quic_tx_packetiser_set_ack_tx_cb(ch->txp, ch_on_txp_ack_tx, ch);
qrx_args.libctx = ch->port->engine->libctx;
qrx_args.demux = ch->port->demux;
qrx_args.short_conn_id_len = rx_short_dcid_len;
qrx_args.max_deferred = 32;
/*
* qrx does not exist yet, then we must be dealing with client channel
* (QUIC connection initiator).
* If qrx exists already, then we are dealing with server channel which
* qrx gets created by port_default_packet_handler() before
* port_default_packet_handler() accepts connection and creates channel
* for it.
* The exception here is tserver which always creates channel,
* before the first packet is ever seen.
*/
if (ch->qrx == NULL && ch->is_tserver_ch == 0) {
/* we are regular client, create channel */
qrx_args.libctx = ch->port->engine->libctx;
qrx_args.demux = ch->port->demux;
qrx_args.short_conn_id_len = rx_short_dcid_len;
qrx_args.max_deferred = 32;
if ((ch->qrx = ossl_qrx_new(&qrx_args)) == NULL)
goto err;
if ((ch->qrx = ossl_qrx_new(&qrx_args)) == NULL)
goto err;
}
if (!ossl_qrx_set_late_validation_cb(ch->qrx,
rx_late_validate,
ch))
goto err;
if (ch->qrx != NULL) {
/*
* callbacks for channels associated with tserver's port
* are set up later when we call ossl_quic_channel_bind_qrx()
* in port_default_packet_handler()
*/
if (!ossl_qrx_set_late_validation_cb(ch->qrx,
rx_late_validate,
ch))
goto err;
if (!ossl_qrx_set_key_update_cb(ch->qrx,
rxku_detected,
ch))
goto err;
}
if (!ossl_qrx_set_key_update_cb(ch->qrx,
rxku_detected,
ch))
goto err;
for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) {
ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL, 0);
@ -426,6 +447,17 @@ int ossl_quic_channel_init(QUIC_CHANNEL *ch)
return ch_init(ch);
}
void ossl_quic_channel_bind_qrx(QUIC_CHANNEL *tserver_ch, OSSL_QRX *qrx)
{
if (tserver_ch->qrx == NULL && tserver_ch->is_tserver_ch == 1) {
tserver_ch->qrx = qrx;
ossl_qrx_set_late_validation_cb(tserver_ch->qrx, rx_late_validate,
tserver_ch);
ossl_qrx_set_key_update_cb(tserver_ch->qrx, rxku_detected,
tserver_ch);
}
}
QUIC_CHANNEL *ossl_quic_channel_alloc(const QUIC_CHANNEL_ARGS *args)
{
QUIC_CHANNEL *ch = NULL;
@ -433,11 +465,13 @@ QUIC_CHANNEL *ossl_quic_channel_alloc(const QUIC_CHANNEL_ARGS *args)
if ((ch = OPENSSL_zalloc(sizeof(*ch))) == NULL)
return NULL;
ch->port = args->port;
ch->is_server = args->is_server;
ch->tls = args->tls;
ch->lcidm = args->lcidm;
ch->srtm = args->srtm;
ch->port = args->port;
ch->is_server = args->is_server;
ch->tls = args->tls;
ch->lcidm = args->lcidm;
ch->srtm = args->srtm;
ch->qrx = args->qrx;
ch->is_tserver_ch = args->is_tserver_ch;
#ifndef OPENSSL_NO_QLOG
ch->use_qlog = args->use_qlog;
@ -570,7 +604,7 @@ err:
size_t ossl_quic_channel_get_short_header_conn_id_len(QUIC_CHANNEL *ch)
{
return ossl_qrx_get_short_hdr_conn_id_len(ch->qrx);
return ossl_quic_port_get_rx_short_dcid_len(ch->port);
}
QUIC_STREAM *ossl_quic_channel_get_stream_by_id(QUIC_CHANNEL *ch,
@ -3655,12 +3689,15 @@ static int ch_on_new_conn_common(QUIC_CHANNEL *ch, const BIO_ADDR *peer,
ossl_qtx_set_qlog_cb(ch->qtx, ch_get_qlog_cb, ch);
ossl_quic_tx_packetiser_set_qlog_cb(ch->txp, ch_get_qlog_cb, ch);
/* Plug in secrets for the Initial EL. */
/*
* Plug in secrets for the Initial EL. secrets for QRX were created in
* port_default_packet_handler() already.
*/
if (!ossl_quic_provide_initial_secret(ch->port->engine->libctx,
ch->port->engine->propq,
&ch->init_dcid,
/*is_server=*/1,
ch->qrx, ch->qtx))
NULL, ch->qtx))
return 0;
/* Register the peer ODCID in the LCIDM. */
@ -3976,7 +4013,12 @@ void ossl_quic_channel_set_msg_callback(QUIC_CHANNEL *ch,
ossl_qtx_set_msg_callback(ch->qtx, msg_callback, msg_callback_ssl);
ossl_quic_tx_packetiser_set_msg_callback(ch->txp, msg_callback,
msg_callback_ssl);
ossl_qrx_set_msg_callback(ch->qrx, msg_callback, msg_callback_ssl);
/*
* postpone msg callback setting for tserver until port calls
* port_bind_channel().
*/
if (ch->is_tserver_ch == 0)
ossl_qrx_set_msg_callback(ch->qrx, msg_callback, msg_callback_ssl);
}
void ossl_quic_channel_set_msg_callback_arg(QUIC_CHANNEL *ch,
@ -3985,7 +4027,13 @@ void ossl_quic_channel_set_msg_callback_arg(QUIC_CHANNEL *ch,
ch->msg_callback_arg = msg_callback_arg;
ossl_qtx_set_msg_callback_arg(ch->qtx, msg_callback_arg);
ossl_quic_tx_packetiser_set_msg_callback_arg(ch->txp, msg_callback_arg);
ossl_qrx_set_msg_callback_arg(ch->qrx, msg_callback_arg);
/*
* postpone msg callback setting for tserver until port calls
* port_bind_channel().
*/
if (ch->is_tserver_ch == 0)
ossl_qrx_set_msg_callback_arg(ch->qrx, msg_callback_arg);
}
void ossl_quic_channel_set_txku_threshold_override(QUIC_CHANNEL *ch,

View File

@ -452,6 +452,9 @@ struct quic_channel_st {
/* Has qlog been requested? */
unsigned int use_qlog : 1;
/* Has qlog been requested? */
unsigned int is_tserver_ch : 1;
/* Saved error stack in case permanent error was encountered */
ERR_STATE *err_state;

View File

@ -498,15 +498,18 @@ static SSL *port_new_handshake_layer(QUIC_PORT *port, QUIC_CHANNEL *ch)
return tls;
}
static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, int is_server)
static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
int is_server, int is_tserver)
{
QUIC_CHANNEL_ARGS args = {0};
QUIC_CHANNEL *ch;
args.port = port;
args.is_server = is_server;
args.lcidm = port->lcidm;
args.srtm = port->srtm;
args.port = port;
args.is_server = is_server;
args.lcidm = port->lcidm;
args.srtm = port->srtm;
args.qrx = qrx;
args.is_tserver_ch = is_tserver;
/*
* Creating a a new channel is made a bit tricky here as there is a
@ -556,7 +559,8 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, int is_server)
QUIC_CHANNEL *ossl_quic_port_create_outgoing(QUIC_PORT *port, SSL *tls)
{
return port_make_channel(port, tls, /*is_server=*/0);
return port_make_channel(port, tls, NULL, /* is_server= */ 0,
/* is_tserver= */ 0);
}
QUIC_CHANNEL *ossl_quic_port_create_incoming(QUIC_PORT *port, SSL *tls)
@ -565,7 +569,12 @@ QUIC_CHANNEL *ossl_quic_port_create_incoming(QUIC_PORT *port, SSL *tls)
assert(port->tserver_ch == NULL);
ch = port_make_channel(port, tls, /*is_server=*/1);
/*
* pass -1 for qrx to indicate port will create qrx
* later in port_default_packet_handler() when calling port_bind_channel().
*/
ch = port_make_channel(port, tls, NULL, /* is_server= */ 1,
/* is_tserver_ch */ 1);
port->tserver_ch = ch;
port->allow_incoming = 1;
return ch;
@ -703,7 +712,8 @@ static void port_rx_pre(QUIC_PORT *port)
*/
static void port_bind_channel(QUIC_PORT *port, const BIO_ADDR *peer,
const QUIC_CONN_ID *scid, const QUIC_CONN_ID *dcid,
const QUIC_CONN_ID *odcid, QUIC_CHANNEL **new_ch)
const QUIC_CONN_ID *odcid, OSSL_QRX *qrx,
QUIC_CHANNEL **new_ch)
{
QUIC_CHANNEL *ch;
@ -714,8 +724,13 @@ static void port_bind_channel(QUIC_PORT *port, const BIO_ADDR *peer,
if (port->tserver_ch != NULL) {
ch = port->tserver_ch;
port->tserver_ch = NULL;
ossl_quic_channel_bind_qrx(ch, qrx);
ossl_qrx_set_msg_callback(ch->qrx, ch->msg_callback,
ch->msg_callback_ssl);
ossl_qrx_set_msg_callback_arg(ch->qrx, ch->msg_callback_arg);
} else {
ch = port_make_channel(port, NULL, /* is_server= */1);
ch = port_make_channel(port, NULL, qrx, /* is_server= */ 1,
/* is_tserver */ 0);
}
if (ch == NULL)
@ -1437,6 +1452,8 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
QUIC_CHANNEL *ch = NULL, *new_ch = NULL;
QUIC_CONN_ID odcid, scid;
uint8_t gen_new_token = 0;
OSSL_QRX *qrx = NULL;
OSSL_QRX_ARGS qrx_args = {0};
uint64_t cause_flags = 0;
/* Don't handle anything if we are no longer running. */
@ -1521,6 +1538,31 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
odcid.id_len = 0;
/*
* Create qrx now so we can check integrity of packet
* which does not belong to any channel.
*/
qrx_args.libctx = port->engine->libctx;
qrx_args.demux = port->demux;
qrx_args.short_conn_id_len = dcid->id_len;
qrx_args.max_deferred = 32;
qrx = ossl_qrx_new(&qrx_args);
if (qrx == NULL)
goto undesirable;
/*
* Derive secrets for qrx only.
*/
if (!ossl_quic_provide_initial_secret(port->engine->libctx,
port->engine->propq,
&hdr.dst_conn_id,
/* is_server */ 1,
qrx, NULL))
goto undesirable;
if (ossl_qrx_validate_initial_packet(qrx, e, (const QUIC_CONN_ID *)dcid) == 0)
goto undesirable;
/*
* TODO(QUIC FUTURE): there should be some logic similar to accounting half-open
* states in TCP. If we reach certain threshold, then we want to
@ -1528,6 +1570,13 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
*/
if (port->validate_addr == 1 && hdr.token == NULL) {
port_send_retry(port, &e->peer, &hdr);
/*
* This is a kind of bummer because we forget secrets for initial
* level encryption. The secrets costs us CPU to compute. What we can
* do here is to store them within retry token. Then we can retrieve them
* from initial packet which will carry our retry token to validate
* client's address.
*/
goto undesirable;
}
@ -1553,13 +1602,24 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
* the request is valid
*/
if (port->validate_addr == 1) {
/*
* Again: we should consider saving initial encryption level
* secrets to token here to save some CPU cycles.
*/
port_send_retry(port, &e->peer, &hdr);
goto undesirable;
}
}
port_bind_channel(port, &e->peer, &scid, &hdr.dst_conn_id,
&odcid, &new_ch);
&odcid, qrx, &new_ch);
/*
* if packet validates it gets moved to channel, we've just bound
* to port.
*/
if (new_ch == NULL)
goto undesirable;
/*
* Generate a token for sending in a later NEW_TOKEN frame
@ -1568,16 +1628,25 @@ static void port_default_packet_handler(QUIC_URXE *e, void *arg,
generate_new_token(new_ch, &e->peer);
/*
* The channel will do all the LCID registration needed, but as an
* optimization inject this packet directly into the channel's QRX for
* processing without going through the DEMUX again.
* The qrx belongs to channel now, so don't free it.
*/
qrx = NULL;
/*
* If function reaches this place, then packet got validated in
* ossl_qrx_validate_initial_packet(). Keep in mind the function
* ossl_qrx_validate_initial_packet() decrypts the packet to validate it.
* If packet validation was successful (and it was because we are here),
* then the function puts the packet to qrx->rx_pending. We must not call
* ossl_qrx_inject_urxe() here now, because we don't want to insert
* the packet to qrx->urx_pending which keeps packet waiting for decryption.
*
* We are going to call ossl_quic_demux_release_urxe() to dispose buffer
* which still holds encrypted data.
*/
if (new_ch != NULL) {
ossl_qrx_inject_urxe(new_ch->qrx, e);
return;
}
undesirable:
ossl_qrx_free(qrx);
ossl_quic_demux_release_urxe(port->demux, e);
}

View File

@ -173,6 +173,24 @@ struct ossl_qrx_st {
SSL *msg_callback_ssl;
};
static RXE *qrx_ensure_free_rxe(OSSL_QRX *qrx, size_t alloc_len);
static int qrx_validate_hdr_early(OSSL_QRX *qrx, RXE *rxe,
const QUIC_CONN_ID *first_dcid);
static int qrx_relocate_buffer(OSSL_QRX *qrx, RXE **prxe, size_t *pi,
const unsigned char **pptr, size_t buf_len);
static int qrx_validate_hdr(OSSL_QRX *qrx, RXE *rxe);
static RXE *qrx_reserve_rxe(RXE_LIST *rxl, RXE *rxe, size_t n);
static int qrx_decrypt_pkt_body(OSSL_QRX *qrx, unsigned char *dst,
const unsigned char *src,
size_t src_len, size_t *dec_len,
const unsigned char *aad, size_t aad_len,
QUIC_PN pn, uint32_t enc_level,
unsigned char key_phase_bit,
uint64_t *rx_key_epoch);
static int qrx_validate_hdr_late(OSSL_QRX *qrx, RXE *rxe);
static uint32_t rxe_determine_pn_space(RXE *rxe);
static void ignore_res(int x);
OSSL_QRX *ossl_qrx_new(const OSSL_QRX_ARGS *args)
{
OSSL_QRX *qrx;
@ -253,6 +271,192 @@ void ossl_qrx_inject_urxe(OSSL_QRX *qrx, QUIC_URXE *urxe)
qrx->msg_callback_arg);
}
/*
* qrx_validate_initial_pkt() is derived from qrx_process_pkt(). Unlike
* qrx_process_pkt() the qrx_validate_initial_pkt() function can process
* initial packet only. All other packets should be discarded. This allows
* port_default_packet_handler() to validate incoming packet. If packet
* is not valid, then port_default_packet_handler() must discard the
* packet instead of creating a new channel for it.
*/
static int qrx_validate_initial_pkt(OSSL_QRX *qrx, QUIC_URXE *urxe,
const QUIC_CONN_ID *first_dcid,
size_t datagram_len)
{
PACKET pkt, orig_pkt;
RXE *rxe;
size_t i = 0, aad_len = 0, dec_len = 0;
const unsigned char *sop;
unsigned char *dst;
QUIC_PKT_HDR_PTRS ptrs;
uint32_t pn_space;
OSSL_QRL_ENC_LEVEL *el = NULL;
uint64_t rx_key_epoch = UINT64_MAX;
if (!PACKET_buf_init(&pkt, ossl_quic_urxe_data(urxe), urxe->data_len))
return 0;
orig_pkt = pkt;
sop = PACKET_data(&pkt);
/*
* Get a free RXE. If we need to allocate a new one, use the packet length
* as a good ballpark figure.
*/
rxe = qrx_ensure_free_rxe(qrx, PACKET_remaining(&pkt));
if (rxe == NULL)
return 0;
/*
* we expect INITIAL packet only, therefore it is OK to pass
* short_conn_id_len as 0.
*/
if (!ossl_quic_wire_decode_pkt_hdr(&pkt,
0, /* short_conn_id_len */
1, /* need second decode */
0, /* nodata -> want to read data */
&rxe->hdr, &ptrs,
NULL))
goto malformed;
if (rxe->hdr.type != QUIC_PKT_TYPE_INITIAL)
goto malformed;
if (!qrx_validate_hdr_early(qrx, rxe, NULL))
goto malformed;
if (ossl_qrl_enc_level_set_have_el(&qrx->el_set, QUIC_ENC_LEVEL_INITIAL) != 1)
goto malformed;
if (rxe->hdr.type == QUIC_PKT_TYPE_INITIAL) {
const unsigned char *token = rxe->hdr.token;
/*
* This may change the value of rxe and change the value of the token
* pointer as well. So we must make a temporary copy of the pointer to
* the token, and then copy it back into the new location of the rxe
*/
if (!qrx_relocate_buffer(qrx, &rxe, &i, &token, rxe->hdr.token_len))
goto malformed;
rxe->hdr.token = token;
}
pkt = orig_pkt;
el = ossl_qrl_enc_level_set_get(&qrx->el_set, QUIC_ENC_LEVEL_INITIAL, 1);
assert(el != NULL); /* Already checked above */
if (!ossl_quic_hdr_protector_decrypt(&el->hpr, &ptrs))
goto malformed;
/*
* We have removed header protection, so don't attempt to do it again if
* the packet gets deferred and processed again.
*/
pkt_mark(&urxe->hpr_removed, 0);
/* Decode the now unprotected header. */
if (ossl_quic_wire_decode_pkt_hdr(&pkt, 0,
0, 0, &rxe->hdr, NULL, NULL) != 1)
goto malformed;
/* Validate header and decode PN. */
if (!qrx_validate_hdr(qrx, rxe))
goto malformed;
/*
* The AAD data is the entire (unprotected) packet header including the PN.
* The packet header has been unprotected in place, so we can just reuse the
* PACKET buffer. The header ends where the payload begins.
*/
aad_len = rxe->hdr.data - sop;
/* Ensure the RXE buffer size is adequate for our payload. */
if ((rxe = qrx_reserve_rxe(&qrx->rx_free, rxe, rxe->hdr.len + i)) == NULL)
goto malformed;
/*
* We decrypt the packet body to immediately after the token at the start of
* the RXE buffer (where present).
*
* Do the decryption from the PACKET (which points into URXE memory) to our
* RXE payload (single-copy decryption), then fixup the pointers in the
* header to point to our new buffer.
*
* If decryption fails this is considered a permanent error; we defer
* packets we don't yet have decryption keys for above, so if this fails,
* something has gone wrong with the handshake process or a packet has been
* corrupted.
*/
dst = (unsigned char *)rxe_data(rxe) + i;
if (!qrx_decrypt_pkt_body(qrx, dst, rxe->hdr.data, rxe->hdr.len,
&dec_len, sop, aad_len, rxe->pn, QUIC_ENC_LEVEL_INITIAL,
rxe->hdr.key_phase, &rx_key_epoch))
goto malformed;
/*
* -----------------------------------------------------
* IMPORTANT: ANYTHING ABOVE THIS LINE IS UNVERIFIED
* AND MUST BE TIMING-CHANNEL SAFE.
* -----------------------------------------------------
*
* At this point, we have successfully authenticated the AEAD tag and no
* longer need to worry about exposing the PN, PN length or Key Phase bit in
* timing channels. Invoke any configured validation callback to allow for
* rejection of duplicate PNs.
*/
if (!qrx_validate_hdr_late(qrx, rxe))
goto malformed;
pkt_mark(&urxe->processed, 0);
/*
* Update header to point to the decrypted buffer, which may be shorter
* due to AEAD tags, block padding, etc.
*/
rxe->hdr.data = dst;
rxe->hdr.len = dec_len;
rxe->data_len = dec_len;
rxe->datagram_len = datagram_len;
rxe->key_epoch = rx_key_epoch;
/* We processed the PN successfully, so update largest processed PN. */
pn_space = rxe_determine_pn_space(rxe);
if (rxe->pn > qrx->largest_pn[pn_space])
qrx->largest_pn[pn_space] = rxe->pn;
/* Copy across network addresses and RX time from URXE to RXE. */
rxe->peer = urxe->peer;
rxe->local = urxe->local;
rxe->time = urxe->time;
rxe->datagram_id = urxe->datagram_id;
/*
* The packet is decrypted, we are going to move it from
* rx_pending queue where it waits to be further processed
* by ch_rx().
*/
ossl_list_rxe_remove(&qrx->rx_free, rxe);
ossl_list_rxe_insert_tail(&qrx->rx_pending, rxe);
return 1;
malformed:
/* caller (port_default_packet_handler()) should discard urxe */
return 0;
}
int ossl_qrx_validate_initial_packet(OSSL_QRX *qrx, QUIC_URXE *urxe,
const QUIC_CONN_ID *dcid)
{
urxe->processed = 0;
urxe->hpr_removed = 0;
urxe->deferred = 0;
return qrx_validate_initial_pkt(qrx, urxe, dcid, urxe->data_len);
}
static void qrx_requeue_deferred(OSSL_QRX *qrx)
{
QUIC_URXE *e;