]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Perform initial AEAD validation before creating a channel
authorAlexandr Nedvedicky <sashan@openssl.org>
Sun, 2 Feb 2025 16:40:25 +0000 (17:40 +0100)
committerNeil Horman <nhorman@openssl.org>
Mon, 17 Feb 2025 16:27:34 +0000 (11:27 -0500)
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)

include/internal/quic_channel.h
include/internal/quic_predef.h
include/internal/quic_record_rx.h
ssl/quic/quic_channel.c
ssl/quic/quic_channel_local.h
ssl/quic/quic_port.c
ssl/quic/quic_record_rx.c

index 543a5900b906f4f87a11372ad7fa929c4a53a0ba..b782f0d66b74058acdecdf67c1814d7693ddcf8e 100644 (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. */
index 21cc074a741239ec9698ef8a1e273c7355590a3f..7d9385d950e01753e015215499287ba0ce467653 100644 (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;
index 10b2e0def4ea4b8bc84935a286402ceca5075317..54f3a6321b1bed2f12b5b699fb029726d98b5182 100644 (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
index 4c52fd49499968f4e61ef0a22dfc62afde58570b..262962973fe0e72bfe18229630ec03c315c99825 100644 (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 (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_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,
index 6ba6366f4b58b4cda629592648a0f77ca5034821..cdd0969586dff3b4e4cbde5b45720fc25fa974e1 100644 (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;
 
index a56d119a12c005e5a0f3474c4f735445034f1ca7..8f03bd89b6c5fd5433aaf0c18a403ffaa3352068 100644 (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);
 }
 
index 666b37f38bd3a01fb2adddda7ce42c5205173b9b..63aa9aff8fa1545cdf520aedd65de1d71f40d8e0 100644 (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;