Commit 9a4ce25036 for openssl.org

commit 9a4ce25036c817346075e60f873ce4b21c445864
Author: Ryan Hooper <ryanh@openssl.foundation>
Date:   Mon Oct 19 11:40:59 2026 -0400

    DTLS 1.3: Accept an ACK while the server awaits the client's flight

    After sending its first flight, a DTLS 1.3 server waits in
    TLS_ST_EARLY_DATA for the client's Certificate or Finished. A client
    that has received only part of that flight may ACK what it did receive
    (RFC 9147 section 7.1), and the server is then meant to retransmit only
    the unacknowledged part (section 7.2). The read transition did not
    accept an ACK in this state, so the server aborted the handshake with
    an unexpected_message alert.

    Accept DTLS13_MT_ACK in TLS_ST_EARLY_DATA and process it through
    TLS_ST_SR_ACK, as is already done for the KeyUpdate, ticket and
    post-handshake authentication states. It is not accepted while a
    HelloRetryRequest is pending, where only the second ClientHello is
    valid.

    A client may also ACK the whole flight before it has sent Finished.
    dtls_process_ack() would then finish the read, and write_transition()
    would move to TLS_ST_OK without the client's Finished ever being read.
    Keep reading in that case, as is already done when an ACK interrupts a
    post-handshake authentication response.

    Add two tests. In the first, the server's first flight is split over
    several datagrams, the middle one is lost, and the client ACKs the
    ServerHello and the Finished through its own record layer. The server
    must accept the ACK, retire exactly those two messages, retransmit
    only the others, and complete the handshake. In the second, the client
    has received the whole flight but is held back from replying while it
    produces its client certificate, and ACKs every record. The server must
    keep waiting for the client's flight instead of completing the
    handshake.

    Fixes: #33242
    Assisted-by: Claude Code:claude-sonnet-5-5
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Fri Oct 30 17:05:28 2026
    Merged-from: https://github.com/openssl/openssl/pull/33267

diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index 8f0cf1c6eb..c955d2b627 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -137,8 +137,8 @@ void dtls1_clear_received_buffer(SSL_CONNECTION *s)
 /*
  * Called once the DTLS 1.3 hellos are done. Our handshake read keys are only
  * installed when the peer's first epoch 2 record arrives (dtls1_read_bytes()),
- * so until then nothing but a plaintext alert may be read. Anything already
- * reassembled or buffered arrived unprotected, so discard it.
+ * so until then only plaintext alerts and ACKs may be read. Handshake messages
+ * already reassembled or buffered arrived unprotected, so discard them.
  */
 void dtls13_hellos_done(SSL_CONNECTION *s)
 {
@@ -202,7 +202,8 @@ void dtls1_acknowledge_sent_buffer(SSL_CONNECTION *s, uint64_t before_epoch)
 void dtls1_clear_sent_buffer(SSL_CONNECTION *s, int keep_unacked_msgs)
 {
     pitem *item = NULL;
-    pqueue *remaining_sent_messages = pqueue_new();
+    pqueue remaining_sent_messages_buf = { NULL, 0 };
+    pqueue *remaining_sent_messages = &remaining_sent_messages_buf;
     pqueue *sent_messages = &s->d1->sent_messages;

     while ((item = pqueue_pop(sent_messages)) != NULL) {
@@ -238,8 +239,6 @@ void dtls1_clear_sent_buffer(SSL_CONNECTION *s, int keep_unacked_msgs)
     if (SSL_CONNECTION_IS_DTLS13(s))
         while ((item = pqueue_pop(remaining_sent_messages)) != NULL)
             pqueue_insert(&s->d1->sent_messages, item);
-
-    pqueue_free(remaining_sent_messages);
 }

 /*
diff --git a/ssl/record/methods/dtls_meth.c b/ssl/record/methods/dtls_meth.c
index ff5ff6986d..809e94e58b 100644
--- a/ssl/record/methods/dtls_meth.c
+++ b/ssl/record/methods/dtls_meth.c
@@ -465,17 +465,19 @@ int dtls_prev_epoch_allows_type(const OSSL_RECORD_LAYER *crypto_rl, int type)
 }

 /*
- * In DTLS 1.3 only the hellos are sent unprotected. Once they are done the
- * read layer still stays in epoch 0 until the peer's first epoch 2 record
- * turns up. libssl marks that point by allowing plaintext alerts, and from
- * then on an alert is the only thing that may still arrive unprotected.
+ * In DTLS 1.3 only the hello handshake messages are sent unprotected. Once
+ * they are done, the read layer stays in epoch 0 until the peer's first
+ * epoch 2 record arrives. libssl marks this phase by allowing plaintext
+ * alerts. Only alerts and ACKs may arrive unprotected in this phase;
+ * dtls_process_ack() checks the epoch authority of each ACK entry.
  */
 static int dtls13_plaintext_allows_type(const OSSL_RECORD_LAYER *rl, int type)
 {
     return rl->version != DTLS1_3_VERSION
         || rl->epoch != 0
         || !rl->allow_plain_alerts
-        || type == SSL3_RT_ALERT;
+        || type == SSL3_RT_ALERT
+        || (type == SSL3_RT_ACK && rl->role == OSSL_RECORD_ROLE_SERVER);
 }

 /*-
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index c79d914c76..6563a37087 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -4527,7 +4527,7 @@ CON_FUNC_RETURN tls_construct_client_compressed_certificate(SSL_CONNECTION *sc,
     if (SSL_IS_FIRST_HANDSHAKE(sc)
         && !SSL_IS_QUIC_HANDSHAKE(sc)
         && (sc->early_data_state != SSL_EARLY_DATA_NONE
-            || (sc->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0)
+            || SSL_CONNECTION_MIDDLEBOX_IS_ENABLED(sc))
         && (!ssl->method->ssl3_enc->change_cipher_state(sc,
             SSL3_CC_HANDSHAKE | SSL3_CHANGE_CIPHER_CLIENT_WRITE | SSL3_CC_COMP_CERT))) {
         /*
diff --git a/ssl/statem/statem_dtls.c b/ssl/statem/statem_dtls.c
index 3fca0ed5da..9759e5d1df 100644
--- a/ssl/statem/statem_dtls.c
+++ b/ssl/statem/statem_dtls.c
@@ -880,6 +880,19 @@ int dtls_record_from_retained_epoch(SSL_CONNECTION *s)
         && s->s3.tmp.record_epoch != dtls1_get_epoch(s, SSL3_CC_READ);
 }

+/*
+ * Server read states where an ACK must not finish the client's flight.
+ * The certificate states are also used during post-handshake authentication.
+ * Callers admitting ACKs must exclude a pending HelloRetryRequest.
+ */
+int dtls13_server_waiting_for_client_flight(OSSL_HANDSHAKE_STATE state)
+{
+    return state == TLS_ST_EARLY_DATA
+        || state == TLS_ST_SR_CERT
+        || state == TLS_ST_SR_COMP_CERT
+        || state == TLS_ST_SR_CERT_VRFY;
+}
+
 int dtls1_process_out_of_seq_message(SSL_CONNECTION *s,
     const struct hm_header_st *msg_hdr)
 {
@@ -1413,6 +1426,16 @@ MSG_PROCESS_RETURN dtls_process_ack(SSL_CONNECTION *s, PACKET *pkt)
             return MSG_PROCESS_ERROR;
         }

+        /*
+         * RFC 9147 section 7: during the initial handshake, an ACK must be
+         * carried in an epoch at least as high as the record it acknowledges.
+         * Ignore unauthorized entries before changing retransmission state.
+         * After the handshake, sending and receiving epochs can differ.
+         */
+        if (SSL_IS_FIRST_HANDSHAKE(s)
+            && epoch > s->s3.tmp.record_epoch)
+            continue;
+
         /*
          * Epoch 2 is the fixed handshake epoch and is never used again once
          * epoch 3 (the first application epoch) is installed. An ACK that
@@ -1488,6 +1511,26 @@ MSG_PROCESS_RETURN dtls_process_ack(SSL_CONNECTION *s, PACKET *pkt)
     if (dtls_any_sent_messages_are_missing_acknowledge(s))
         return MSG_PROCESS_CONTINUE_READING;

+    /*
+     * This ACK interrupted a server still reading the client's flight.
+     * A complete ACK of our flight does not replace the remaining client
+     * messages. It may arrive before Certificate or be reordered after
+     * Certificate or CertificateVerify. Finishing the read sub-state-machine
+     * here would let write_transition() move to TLS_ST_OK and skip them.
+     */
+    if (s->server
+        && dtls13_server_waiting_for_client_flight(s->statem.pre_ack_hand_state)) {
+        /*
+         * We already know above that nothing is left needing
+         * retransmission, so this only resets timeout state and clears the
+         * (already-empty) sent buffer -- it leaves the read state alone, so
+         * the client's flight, including Finished, is still processed on
+         * return.
+         */
+        dtls1_stop_timer(s);
+        return MSG_PROCESS_CONTINUE_READING;
+    }
+
     return MSG_PROCESS_FINISHED_READING;
 }

diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h
index 744518d89f..0394dfd679 100644
--- a/ssl/statem/statem_local.h
+++ b/ssl/statem/statem_local.h
@@ -129,6 +129,7 @@ __owur int tls_get_message_body(SSL_CONNECTION *s, size_t *len);
 __owur int dtls_get_message(SSL_CONNECTION *s, int *mt);
 __owur int dtls_get_message_body(SSL_CONNECTION *s, size_t *len);
 int dtls_record_from_retained_epoch(SSL_CONNECTION *s);
+int dtls13_server_waiting_for_client_flight(OSSL_HANDSHAKE_STATE state);
 int dtls1_process_out_of_seq_message(SSL_CONNECTION *s,
     const struct hm_header_st *msg_hdr);
 __owur int tls_common_finish_mac(SSL_CONNECTION *s);
diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c
index 52770e12d7..9cb923217e 100644
--- a/ssl/statem/statem_srvr.c
+++ b/ssl/statem/statem_srvr.c
@@ -81,6 +81,19 @@ static int ossl_statem_server13_read_transition(SSL_CONNECTION *s, int mt)
             return 1;
     }

+    /*
+     * An ACK may arrive before or between messages of the client's flight.
+     * Preserve the interrupted read state so CertificateVerify and Finished
+     * are still required, even if the ACK covers our entire flight.
+     */
+    if (SSL_CONNECTION_IS_DTLS13(s) && mt == DTLS13_MT_ACK
+        && s->hello_retry_request != SSL_HRR_PENDING
+        && dtls13_server_waiting_for_client_flight(st->hand_state)) {
+        st->pre_ack_hand_state = st->hand_state;
+        st->hand_state = TLS_ST_SR_ACK;
+        return 1;
+    }
+
     /*
      * Note: There is no case for TLS_ST_BEFORE because at that stage we have
      * not negotiated TLSv1.3 yet, so that case is handled by
diff --git a/test/dtls13_internal_test.c b/test/dtls13_internal_test.c
index bfb88fd44b..ce5d152b7f 100644
--- a/test/dtls13_internal_test.c
+++ b/test/dtls13_internal_test.c
@@ -2384,12 +2384,14 @@ static int test_dtls13_hrr_lost_server_hello(int idx)
             &sctx, &cctx, cert, privkey))
         || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0)))
         goto end;
+
     SSL_CTX_set_options(sctx, SSL_OP_COOKIE_EXCHANGE);
     SSL_CTX_set_cookie_generate_cb(sctx, generate_cookie_cb);
     SSL_CTX_set_cookie_verify_cb(sctx, verify_cookie_cb);
     if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
             NULL, NULL)))
         goto end;
+
     sc = SSL_CONNECTION_FROM_SSL(server);
     bio = SSL_get_rbio(client);

@@ -2401,9 +2403,11 @@ static int test_dtls13_hrr_lost_server_hello(int idx)
     ret = SSL_connect(client);
     if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
         goto end;
+
     ret = SSL_accept(server);
     if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ))
         goto end;
+
     ret = SSL_connect(client);
     if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
         goto end;
@@ -2432,6 +2436,7 @@ static int test_dtls13_hrr_lost_server_hello(int idx)
         continue;
     if (!TEST_int_gt(first_enc, 0) || !TEST_int_lt(first_enc, ndg))
         goto end;
+
     for (i = first_enc; i < ndg; i++)
         if (!TEST_int_eq(dg[i][0] & 0xe0, 0x20))
             goto end;
@@ -2458,6 +2463,7 @@ static int test_dtls13_hrr_lost_server_hello(int idx)
                                  dglen[i], -1, INJECT_PACKET_IGNORE_REC_SEQ),
                     dglen[i]))
                 goto end;
+
     } else {
         /*
          * The original ServerHello datagrams remain lost. Expire the server's
@@ -2467,6 +2473,7 @@ static int test_dtls13_hrr_lost_server_hello(int idx)
         if (!TEST_size_t_gt(pqueue_size(&sc->d1->sent_messages), 0)
             || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout)))
             goto end;
+
         sc->d1->next_timeout = ossl_time_subtract(ossl_time_now(),
             ossl_seconds2time(1));
         if (!TEST_int_gt(DTLSv1_handle_timeout(server), 0)
@@ -2477,6 +2484,427 @@ static int test_dtls13_hrr_lost_server_hello(int idx)
     if (!connect_while_readable(client, bio)
         || !TEST_int_eq(SSL_accept(server), 1))
         goto end;
+
+    ret = SSL_read(client, buf, sizeof(buf));
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(client), TLS_ST_OK))
+        goto end;
+
+    /* Application data flows both ways. */
+    if (!TEST_int_eq(SSL_write(server, "s", 1), 1)
+        || !TEST_int_eq(SSL_read(client, buf, 1), 1)
+        || !TEST_uchar_eq(buf[0], 's')
+        || !TEST_int_eq(SSL_write(client, "c", 1), 1)
+        || !TEST_int_eq(SSL_read(server, buf, 1), 1)
+        || !TEST_uchar_eq(buf[0], 'c'))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(server);
+    SSL_free(client);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
+/*
+ * Count how many of SSL_CONNECTION's buffered handshake messages have been fully
+ * acknowledged, and how many record numbers the rest still have outstanding.
+ */
+static void count_flight_acks(SSL_CONNECTION *sc, size_t *nmsgs, size_t *nacked,
+    size_t *unacked_recs)
+{
+    piterator iter = pqueue_iterator(&sc->d1->sent_messages);
+    pitem *item;
+
+    *nmsgs = *nacked = *unacked_recs = 0;
+    while ((item = pqueue_next(&iter)) != NULL) {
+        dtls_sent_msg *msg = item->data;
+
+        (*nmsgs)++;
+        if (ossl_list_record_number_is_empty(&msg->rec_nums))
+            (*nacked)++;
+        else
+            *unacked_recs += ossl_list_record_number_num(&msg->rec_nums);
+    }
+}
+
+/*
+ * Count the records in a datagram sent in epoch 0 and in epoch 2. The sequence
+ * number of a protected DTLS 1.3 record is encrypted on the wire, but records
+ * are numbered consecutively from 0 within each epoch, so a receiver that has
+ * seen every record up to one knows its number by counting.
+ */
+static int count_dgram_records(const unsigned char *dg, size_t len,
+    size_t *epoch0, size_t *epoch2)
+{
+    size_t hdrlen, reclen;
+
+    *epoch0 = *epoch2 = 0;
+    while (len > 0) {
+        if ((dg[0] & DTLS13_UNI_HDR_FIX_BITS_MASK) == DTLS13_UNI_HDR_FIX_BITS) {
+            /* Unified header, 001CSLEE: the epoch is in the low two bits. */
+            if ((dg[0] & DTLS13_UNI_HDR_CID_BIT) != 0
+                || (dg[0] & DTLS13_UNI_HDR_EPOCH_BITS_MASK) != 2)
+                return 0;
+            hdrlen = 1 + ((dg[0] & DTLS13_UNI_HDR_SEQ_BIT) != 0 ? 2 : 1);
+            if ((dg[0] & DTLS13_UNI_HDR_LEN_BIT) != 0) {
+                if (len < hdrlen + 2)
+                    return 0;
+                reclen = ((size_t)dg[hdrlen] << 8) | dg[hdrlen + 1];
+                hdrlen += 2;
+            } else {
+                /* No length: the record runs to the end of the datagram. */
+                reclen = len - hdrlen;
+            }
+            (*epoch2)++;
+        } else {
+            /* Plaintext record: 13 byte header, epoch in bytes 3 and 4. */
+            if (len < DTLS1_RT_HEADER_LENGTH || dg[3] != 0 || dg[4] != 0)
+                return 0;
+            hdrlen = DTLS1_RT_HEADER_LENGTH;
+            reclen = ((size_t)dg[11] << 8) | dg[12];
+            (*epoch0)++;
+        }
+        if (len < hdrlen + reclen)
+            return 0;
+        dg += hdrlen + reclen;
+        len -= hdrlen + reclen;
+    }
+    return 1;
+}
+
+/*
+ * Send an ACK from |cc| for every epoch 0 record numbered below |n0| and
+ * every epoch 2 record from |first2| up to, but not including, |n2|.
+ */
+static int send_client_ack(SSL_CONNECTION *cc, size_t n0, size_t first2,
+    size_t n2)
+{
+    unsigned char ack[2 + 16 * 64];
+    WPACKET pkt;
+    size_t acklen, written, j;
+
+    if (!TEST_true(WPACKET_init_static_len(&pkt, ack, sizeof(ack), 2)))
+        goto err;
+    for (j = 0; j < n0; j++)
+        if (!TEST_true(WPACKET_put_bytes_u64(&pkt, 0))
+            || !TEST_true(WPACKET_put_bytes_u64(&pkt, j)))
+            goto err;
+    for (j = first2; j < n2; j++)
+        if (!TEST_true(WPACKET_put_bytes_u64(&pkt, 2))
+            || !TEST_true(WPACKET_put_bytes_u64(&pkt, j)))
+            goto err;
+    if (!TEST_true(WPACKET_finish(&pkt))
+        || !TEST_true(WPACKET_get_total_written(&pkt, &acklen)))
+        goto err;
+    WPACKET_cleanup(&pkt);
+
+    /*
+     * Mid-handshake the client writes through its buffering BIO, which
+     * SSL_get_wbio() looks past, so flush cc->wbio itself.
+     */
+    return TEST_int_eq(dtls1_write_bytes(cc, SSL3_RT_ACK, ack, acklen, &written), 1)
+        && TEST_size_t_eq(written, acklen)
+        && TEST_int_gt(BIO_flush(cc->wbio), 0);
+err:
+    WPACKET_cleanup(&pkt);
+    return 0;
+}
+
+/*
+ * rfc9147 section 7.1: a client that receives only part of the server's
+ * flight may ACK what it did receive, and section 7.2 says the server should
+ * then retransmit only what was not acknowledged. The server is in
+ * TLS_ST_EARLY_DATA, waiting for the client's Finished, when that ACK
+ * arrives (its write transition moves on from TLS_ST_SW_FINISHED to
+ * TLS_ST_EARLY_DATA); it must accept it and carry on with the handshake.
+ *
+ * The server's flight is split over several datagrams by a small MTU and the
+ * ones in the middle are dropped. The client ACKs the ServerHello and the
+ * Finished it did receive, through its real write path, in the handshake
+ * epoch.
+ */
+static int test_dtls13_server_flight_partial_ack(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *server = NULL, *client = NULL;
+    SSL_CONNECTION *sc, *cc;
+    dtls_sent_msg *first, *last = NULL;
+    pitem *item;
+    piterator iter;
+    BIO *bio;
+    unsigned char dg[16][1500], buf[2048];
+    int dglen[16];
+    size_t nmsgs, nacked, unacked_recs, unacked_recs_after;
+    size_t n0 = 0, n2 = 0, d0, d2;
+    int ndg = 0, first_enc, i, ret, testresult = 0;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey))
+        || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0))
+        || !TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
+            NULL, NULL)))
+        goto end;
+    sc = SSL_CONNECTION_FROM_SSL(server);
+    cc = SSL_CONNECTION_FROM_SSL(client);
+    bio = SSL_get_rbio(client);
+
+    /* Make the server's first flight span several datagrams. */
+    SSL_set_options(server, SSL_OP_NO_QUERY_MTU);
+    if (!TEST_long_gt(SSL_set_mtu(server, 400), 0))
+        goto end;
+
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+        goto end;
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(server), TLS_ST_EARLY_DATA))
+        goto end;
+
+    /* Capture the flight, delivering none of it yet. */
+    while ((ret = BIO_read(bio, dg[ndg], sizeof(dg[0]))) > 0) {
+        dglen[ndg++] = ret;
+        if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg)))
+            goto end;
+    }
+    if (!TEST_int_ge(ndg, 3))
+        goto end;
+
+    /* The flight must still be buffered for retransmission. */
+    count_flight_acks(sc, &nmsgs, &nacked, &unacked_recs);
+    if (!TEST_size_t_ge(nmsgs, 3) || !TEST_size_t_eq(nacked, 0))
+        goto end;
+    first = pqueue_peek(&sc->d1->sent_messages)->data;
+    iter = pqueue_iterator(&sc->d1->sent_messages);
+    while ((item = pqueue_next(&iter)) != NULL)
+        last = item->data;
+    if (!TEST_ptr(first) || !TEST_ptr(last)
+        || !TEST_int_eq(first->msg_info.msg_type, SSL3_MT_SERVER_HELLO)
+        || !TEST_int_eq(last->msg_info.msg_type, SSL3_MT_FINISHED))
+        goto end;
+
+    /*
+     * Work out the record numbers the client would ACK from the datagrams
+     * alone, without asking the server what it sent. The ServerHello records
+     * are the plaintext ones, epoch 0 from sequence number 0. The Finished is
+     * the last record in epoch 2, which is also numbered from 0. Every record
+     * counts, including those that will be lost below.
+     */
+    for (i = 0; i < ndg; i++) {
+        if (!TEST_true(count_dgram_records(dg[i], dglen[i], &d0, &d2)))
+            goto end;
+        n0 += d0;
+        n2 += d2;
+    }
+    if (!TEST_size_t_gt(n0, 0) || !TEST_size_t_gt(n2, 0))
+        goto end;
+
+    /*
+     * The ServerHello is sent in the clear and can itself span several
+     * datagrams (a post-quantum key share is over a kilobyte); everything
+     * after it is encrypted, in the handshake epoch. The client only moves to
+     * the handshake epoch once it has the whole ServerHello and an epoch 2
+     * record, so deliver every plaintext datagram, the first encrypted one,
+     * and the last one (which carries the Finished). Lose the encrypted ones
+     * in between.
+     */
+    for (first_enc = 0; first_enc < ndg && dg[first_enc][0] == 0x16; first_enc++)
+        continue;
+    if (!TEST_int_gt(first_enc, 0)
+        || !TEST_int_le(first_enc + 2, ndg - 1))
+        goto end;
+    for (i = 0; i < ndg; i++) {
+        if (i > first_enc && i < ndg - 1)
+            continue;
+        if (!TEST_int_eq(mempacket_test_inject(bio, (const char *)dg[i],
+                             dglen[i], -1, INJECT_PACKET_IGNORE_REC_SEQ),
+                dglen[i]))
+            goto end;
+    }
+
+    /* The client has the handshake keys now but cannot finish. */
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+        goto end;
+
+    /*
+     * ACK the ServerHello and the Finished, and nothing else: every epoch 0
+     * record, which is all of the ServerHello, and the last epoch 2 record,
+     * which is the Finished. The numbers come from the datagrams, not from
+     * the server; the server retiring exactly those two messages below shows
+     * that it recorded the same ones.
+     */
+    if (!TEST_true(send_client_ack(cc, n0, n2 - 1, n2)))
+        goto end;
+
+    /* Keep the retransmit timer out of the way while the ACK is processed. */
+    sc->d1->next_timeout = ossl_time_add(ossl_time_now(), ossl_seconds2time(3600));
+
+    /* The ACK is not fatal: the server keeps waiting for the client's flight. */
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(server), TLS_ST_EARLY_DATA))
+        goto end;
+
+    /* Only the two acknowledged messages are retired. */
+    count_flight_acks(sc, &nmsgs, &nacked, &unacked_recs);
+    if (!TEST_size_t_eq(nacked, 2) || !TEST_size_t_gt(unacked_recs, 0))
+        goto end;
+
+    /*
+     * The retransmit timer fires: the unacknowledged messages go out again,
+     * the acknowledged ones do not.
+     */
+    sc->d1->next_timeout = ossl_time_subtract(ossl_time_now(), ossl_seconds2time(1));
+    if (!TEST_true(SSL_handle_events(server)))
+        goto end;
+    count_flight_acks(sc, &nmsgs, &nacked, &unacked_recs_after);
+    if (!TEST_size_t_eq(nacked, 2)
+        || !TEST_size_t_gt(unacked_recs_after, unacked_recs)
+        || !TEST_size_t_gt(BIO_ctrl_pending(bio), 0))
+        goto end;
+
+    /* The client now has the whole flight and the handshake completes. */
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_accept(server), 1))
+        goto end;
+    ret = SSL_read(client, buf, sizeof(buf));
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(client), TLS_ST_OK))
+        goto end;
+
+    /* Application data flows both ways. */
+    if (!TEST_int_eq(SSL_write(server, "s", 1), 1)
+        || !TEST_int_eq(SSL_read(client, buf, 1), 1)
+        || !TEST_uchar_eq(buf[0], 's')
+        || !TEST_int_eq(SSL_write(client, "c", 1), 1)
+        || !TEST_int_eq(SSL_read(server, buf, 1), 1)
+        || !TEST_uchar_eq(buf[0], 'c'))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(server);
+    SSL_free(client);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
+/* Stand in for a certificate that takes a while to produce. */
+static int cert_cb_wait_once(SSL *ssl, void *arg)
+{
+    int *calls = arg;
+
+    return (*calls)++ == 0 ? -1 : 1;
+}
+
+/*
+ * rfc9147 section 7.1: a client may ACK a complete flight when it cannot
+ * respond to it immediately, for example because producing its client
+ * certificate takes a while. That ACK arrives while the server is waiting for
+ * the client's Certificate, and it must not be taken to finish the handshake.
+ *
+ * The client is held at its certificate callback after reading the whole
+ * flight, ACKs every record, and only then goes on to send its Certificate
+ * and Finished.
+ */
+static int test_dtls13_server_flight_full_ack(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *server = NULL, *client = NULL;
+    SSL_CONNECTION *sc, *cc;
+    BIO *bio;
+    unsigned char dg[16][1500], buf[2048];
+    int dglen[16];
+    size_t nmsgs, nacked, unacked_recs;
+    size_t n0 = 0, n2 = 0, d0, d2;
+    int calls = 0, ndg = 0, i, ret, testresult = 0;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey))
+        || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0)))
+        goto end;
+    /* The server asks for a certificate; the client has none to send. */
+    SSL_CTX_set_verify(sctx, SSL_VERIFY_PEER, NULL);
+    SSL_CTX_set_cert_cb(cctx, cert_cb_wait_once, &calls);
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL, NULL)))
+        goto end;
+    sc = SSL_CONNECTION_FROM_SSL(server);
+    cc = SSL_CONNECTION_FROM_SSL(client);
+    bio = SSL_get_rbio(client);
+
+    SSL_set_options(server, SSL_OP_NO_QUERY_MTU);
+    if (!TEST_long_gt(SSL_set_mtu(server, 400), 0))
+        goto end;
+
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+        goto end;
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(server), TLS_ST_EARLY_DATA))
+        goto end;
+
+    /* Capture the flight and deliver all of it. */
+    while ((ret = BIO_read(bio, dg[ndg], sizeof(dg[0]))) > 0) {
+        dglen[ndg++] = ret;
+        if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg)))
+            goto end;
+    }
+    if (!TEST_int_gt(ndg, 0))
+        goto end;
+    for (i = 0; i < ndg; i++) {
+        if (!TEST_true(count_dgram_records(dg[i], dglen[i], &d0, &d2))
+            || !TEST_int_eq(mempacket_test_inject(bio, (const char *)dg[i],
+                                dglen[i], -1, INJECT_PACKET_IGNORE_REC_SEQ),
+                dglen[i]))
+            goto end;
+        n0 += d0;
+        n2 += d2;
+    }
+
+    /* The client has read the whole flight and is waiting for its certificate. */
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_X509_LOOKUP)
+        || !TEST_int_eq(calls, 1))
+        goto end;
+
+    /* ACK every record of the flight, in epochs 0 and 2, before replying. */
+    if (!TEST_true(send_client_ack(cc, n0, 0, n2)))
+        goto end;
+
+    /* Keep the retransmit timer out of the way while the ACK is processed. */
+    sc->d1->next_timeout = ossl_time_add(ossl_time_now(), ossl_seconds2time(3600));
+
+    /*
+     * Everything has been acknowledged, but that is not the client's flight:
+     * the server must carry on waiting for the Certificate and the Finished
+     * and not move on to complete the handshake.
+     */
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(server), TLS_ST_EARLY_DATA))
+        goto end;
+    count_flight_acks(sc, &nmsgs, &nacked, &unacked_recs);
+    if (!TEST_size_t_eq(nmsgs, 0)
+        || !TEST_size_t_eq(unacked_recs, 0)
+        || !TEST_true(ossl_time_is_zero(sc->d1->next_timeout))
+        || !TEST_false(SSL_is_init_finished(server)))
+        goto end;
+
+    /* The client's certificate is ready: it replies and the handshake completes. */
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(calls, 2)
+        || !TEST_int_eq(SSL_accept(server), 1))
+        goto end;
     ret = SSL_read(client, buf, sizeof(buf));
     if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
         || !TEST_int_eq(SSL_get_state(client), TLS_ST_OK))
@@ -2499,6 +2927,634 @@ end:
     SSL_CTX_free(cctx);
     return testresult;
 }
+/*
+ * Withhold the server flight and inject an epoch-0 ACK using record numbers
+ * derived from wire record counts. Check that it cannot acknowledge encrypted
+ * records or stop the retransmission timer, while valid epoch-0 entries are
+ * still processed.
+ *
+ * idx 0: empty ACK; nothing is acknowledged.
+ * idx 1: ACK only ServerHello; the encrypted flight remains outstanding.
+ * idx 2: also claim the epoch-2 records; those claims must not retire them.
+ * The legal epoch-2 full-flight control is test_dtls13_server_flight_full_ack.
+ */
+static int test_dtls13_plaintext_ack_epoch(int idx)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *server = NULL, *client = NULL;
+    SSL_CONNECTION *sc;
+    unsigned char dg[2048], ack[2048] = { 0 };
+    size_t n0 = 0, n2 = 0, d0, d2, nmsgs, nacked, nrecs;
+    size_t before_msgs, before_recs, acklen, j;
+    size_t remaining2 = 0;
+    piterator iter;
+    pitem *item;
+    WPACKET pkt;
+    OSSL_TIME timeout;
+    int ret, testresult = 0;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey))
+        || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0))
+        || !TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
+            NULL, NULL)))
+        goto end;
+    sc = SSL_CONNECTION_FROM_SSL(server);
+
+    SSL_set_options(server, SSL_OP_NO_QUERY_MTU);
+    if (!TEST_long_gt(SSL_set_mtu(server, 400), 0))
+        goto end;
+
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+        goto end;
+
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(server), TLS_ST_EARLY_DATA)
+        || !TEST_uint64_t_eq(dtls1_get_epoch(sc, SSL3_CC_READ), 0))
+        goto end;
+
+    /* Discard the flight, counting records independently of server state. */
+    while ((ret = BIO_read(SSL_get_rbio(client), dg, sizeof(dg))) > 0) {
+        if (!TEST_true(count_dgram_records(dg, ret, &d0, &d2)))
+            goto end;
+        n0 += d0;
+        n2 += d2;
+    }
+
+    count_flight_acks(sc, &before_msgs, &nacked, &before_recs);
+    if (!TEST_size_t_gt(n0, 0) || !TEST_size_t_gt(n2, 0)
+        || !TEST_size_t_eq(nacked, 0)
+        || !TEST_size_t_ge(before_recs, n0)
+        || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout)))
+        goto end;
+
+    if (!TEST_true(WPACKET_init_static_len(&pkt, ack + DTLS1_RT_HEADER_LENGTH,
+            sizeof(ack) - DTLS1_RT_HEADER_LENGTH, 2)))
+        goto end;
+
+    for (j = 0; idx != 0 && j < n0; j++)
+        if (!TEST_true(WPACKET_put_bytes_u64(&pkt, 0))
+            || !TEST_true(WPACKET_put_bytes_u64(&pkt, j)))
+            goto packet_err;
+
+    for (j = 0; idx == 2 && j < n2; j++)
+        if (!TEST_true(WPACKET_put_bytes_u64(&pkt, 2))
+            || !TEST_true(WPACKET_put_bytes_u64(&pkt, j)))
+            goto packet_err;
+
+    if (!TEST_true(WPACKET_finish(&pkt))
+        || !TEST_true(WPACKET_get_total_written(&pkt, &acklen)))
+        goto packet_err;
+    WPACKET_cleanup(&pkt);
+
+    ack[0] = SSL3_RT_ACK;
+    ack[1] = (unsigned char)(DTLS1_2_VERSION >> 8);
+    ack[2] = (unsigned char)DTLS1_2_VERSION;
+    /* Use a fresh epoch-0 sequence even if ClientHello was fragmented. */
+    ack[9] = 0xff;
+    ack[10] = 0xff;
+    ack[11] = (unsigned char)(acklen >> 8);
+    ack[12] = (unsigned char)acklen;
+    acklen += DTLS1_RT_HEADER_LENGTH;
+    if (!TEST_int_eq(mempacket_test_inject(SSL_get_rbio(server),
+                         (const char *)ack, (int)acklen, -1,
+                         INJECT_PACKET_IGNORE_REC_SEQ),
+            (int)acklen))
+        goto end;
+
+    /* Only ACK processing, not a timer event, may change the sent flight. */
+    timeout = sc->d1->next_timeout = ossl_time_add(ossl_time_now(), ossl_seconds2time(3600));
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(server), TLS_ST_EARLY_DATA)
+        || !TEST_false(SSL_is_init_finished(server))
+        || !TEST_int_eq(ossl_time_compare(sc->d1->next_timeout, timeout), 0))
+        goto end;
+
+    count_flight_acks(sc, &nmsgs, &nacked, &nrecs);
+    /* No encrypted record may be retired, even by a mixed-epoch ACK. */
+    iter = pqueue_iterator(&sc->d1->sent_messages);
+    while ((item = pqueue_next(&iter)) != NULL) {
+        dtls_sent_msg *msg = item->data;
+        DTLS1_RECORD_NUMBER *rec;
+
+        for (rec = ossl_list_record_number_head(&msg->rec_nums);
+            rec != NULL; rec = ossl_list_record_number_next(rec)) {
+            if (rec->epoch == 2)
+                remaining2++;
+        }
+    }
+
+    if (!TEST_size_t_eq(nmsgs, before_msgs)
+        || !TEST_size_t_eq(remaining2, n2))
+        goto end;
+    /* Valid epoch-0 entries still apply, even in the mixed-epoch ACK. */
+    if (!TEST_size_t_eq(nacked, idx == 0 ? 0 : 1)
+        || !TEST_size_t_eq(nrecs, idx == 0 ? before_recs : before_recs - n0))
+        goto end;
+
+    testresult = 1;
+    goto end;
+packet_err:
+    WPACKET_cleanup(&pkt);
+end:
+    SSL_free(server);
+    SSL_free(client);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
+/*
+ * A server that cannot immediately respond to ClientHello may send an epoch-0
+ * ACK (RFC 9147 section 7.1). Reordering can deliver that ACK after ServerHello
+ * but before the first protected record. It must not abort the handshake.
+ *
+ * idx 0/1: empty/ClientHello ACK before the encrypted flight.
+ * idx 2/3: the same ACKs after the encrypted flight, closing the epoch-0 window.
+ * Adapted from Mounir Idrassi's public-API delayed-server-ACK reproducer.
+ */
+static int test_dtls13_delayed_server_ack(int idx)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *server = NULL, *client = NULL;
+    SSL_CONNECTION *sc, *cc;
+    BIO *bio;
+    unsigned char dg[32][2048], ack[DTLS1_RT_HEADER_LENGTH + 2 + 16] = { 0 };
+    unsigned char buf;
+    int dglen[32];
+    int ndg = 0, i, j, ret, calls = 0, first_enc = -1, testresult = 0;
+    int with_entry = idx % 2, control = idx >= 2;
+    int acklen = DTLS1_RT_HEADER_LENGTH + 2 + (with_entry ? 16 : 0);
+    size_t off, reclen, enc_off = 0, n0, n2;
+    uint64_t seq = 0;
+    OSSL_HANDSHAKE_STATE state;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey))
+        || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0)))
+        goto end;
+
+    SSL_CTX_set_cert_cb(sctx, cert_cb_wait_once, &calls);
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL, NULL)))
+        goto end;
+
+    sc = SSL_CONNECTION_FROM_SSL(server);
+    cc = SSL_CONNECTION_FROM_SSL(client);
+    SSL_set_options(server, SSL_OP_NO_QUERY_MTU);
+    if (!TEST_long_gt(SSL_set_mtu(server, 1200), 0))
+        goto end;
+
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+        goto end;
+
+    /* Capture ClientHello so the one-entry ACK names an actual wire record. */
+    bio = SSL_get_rbio(server);
+    while (BIO_ctrl_pending(bio) > 0) {
+        if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg))
+            || !TEST_size_t_le(BIO_ctrl_pending(bio), sizeof(dg[0]))
+            || !TEST_int_gt(dglen[ndg] = BIO_read(bio, dg[ndg], sizeof(dg[0])), 0)
+            || !TEST_true(count_dgram_records(dg[ndg], dglen[ndg], &n0, &n2))
+            || !TEST_size_t_eq(n2, 0))
+            goto end;
+
+        ndg++;
+    }
+    if (!TEST_int_gt(ndg, 0)
+        || !TEST_int_gt(dglen[0], DTLS1_RT_HEADER_LENGTH)
+        || !TEST_uchar_eq(dg[0][0], SSL3_RT_HANDSHAKE)
+        || !TEST_uchar_eq(dg[0][DTLS1_RT_HEADER_LENGTH], SSL3_MT_CLIENT_HELLO))
+        goto end;
+
+    if (with_entry) {
+        /* The epoch is zero; right-align the 48-bit sequence in its u64. */
+        for (j = 0; j < 6; j++)
+            ack[DTLS1_RT_HEADER_LENGTH + 2 + 8 + 2 + j] = dg[0][5 + j];
+    }
+
+    for (i = 0; i < ndg; i++)
+        if (!TEST_int_eq(mempacket_test_inject(bio, (const char *)dg[i],
+                             dglen[i], -1, INJECT_PACKET_IGNORE_REC_SEQ),
+                dglen[i]))
+            goto end;
+
+    /* Pause where a peer could generate its ACK, then produce the flight. */
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_X509_LOOKUP)
+        || !TEST_int_eq(calls, 1))
+        goto end;
+
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(calls, 2))
+        goto end;
+
+    /* Keep retransmission from supplying records deliberately withheld below. */
+    sc->d1->next_timeout = ossl_time_add(ossl_time_now(), ossl_seconds2time(3600));
+    cc->d1->next_timeout = sc->d1->next_timeout;
+
+    bio = SSL_get_rbio(client);
+    ndg = 0;
+    while (BIO_ctrl_pending(bio) > 0) {
+        if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg))
+            || !TEST_size_t_le(BIO_ctrl_pending(bio), sizeof(dg[0]))
+            || !TEST_int_gt(dglen[ndg] = BIO_read(bio, dg[ndg], sizeof(dg[0])), 0)
+            || !TEST_true(count_dgram_records(dg[ndg], dglen[ndg], &n0, &n2)))
+            goto end;
+
+        ndg++;
+    }
+
+    /*
+     * Deliver just the plaintext prefix, splitting a mixed datagram at the
+     * first protected record without changing any record bytes. This also
+     * handles a ServerHello fragmented across several records or datagrams.
+     */
+    for (i = 0; i < ndg; i++) {
+        for (off = 0; off < (size_t)dglen[i]; off += reclen) {
+            const unsigned char *r = dg[i] + off;
+            size_t remaining = (size_t)dglen[i] - off;
+
+            if (DTLS13_UNI_HDR_FIX_BITS_IS_SET(r[0])) {
+                first_enc = i;
+                enc_off = off;
+                break;
+            }
+            if (!TEST_size_t_ge(remaining, DTLS1_RT_HEADER_LENGTH)
+                || !TEST_uchar_eq(r[0], SSL3_RT_HANDSHAKE))
+                goto end;
+
+            reclen = DTLS1_RT_HEADER_LENGTH + ((size_t)r[11] << 8) + r[12];
+            if (!TEST_size_t_le(reclen, remaining))
+                goto end;
+
+            seq = 0;
+            for (j = 5; j <= 10; j++)
+                seq = (seq << 8) | r[j];
+        }
+        if (off > 0
+            && !TEST_int_eq(mempacket_test_inject(bio, (const char *)dg[i],
+                                (int)off, -1, INJECT_PACKET_IGNORE_REC_SEQ),
+                (int)off))
+            goto end;
+
+        if (first_enc >= 0)
+            break;
+    }
+    if (!TEST_int_ge(first_enc, 0))
+        goto end;
+
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(SSL_get_state(client), TLS_ST_CR_SRVR_HELLO)
+        || !TEST_uint64_t_eq(dtls1_get_epoch(cc, SSL3_CC_READ), 0))
+        goto end;
+
+    /* Use a fresh server record number, not a replay of ServerHello. */
+    if (!TEST_uint64_t_lt(seq, (UINT64_C(1) << 48) - 1))
+        goto end;
+
+    seq++;
+    ack[0] = SSL3_RT_ACK;
+    ack[1] = (unsigned char)(DTLS1_2_VERSION >> 8);
+    ack[2] = (unsigned char)DTLS1_2_VERSION;
+    for (j = 10; j >= 5; j--, seq >>= 8)
+        ack[j] = (unsigned char)seq;
+    ack[12] = (unsigned char)(acklen - DTLS1_RT_HEADER_LENGTH);
+    ack[14] = with_entry ? 16 : 0;
+
+    /* Run both delivery orders with exactly the same ACK and flight bytes. */
+    for (j = 0; j < 2; j++) {
+        if (j == control) {
+            state = SSL_get_state(client);
+            if (!TEST_int_eq(mempacket_test_inject(bio, (const char *)ack,
+                                 acklen, -1, INJECT_PACKET_IGNORE_REC_SEQ),
+                    acklen))
+                goto end;
+
+            ret = SSL_connect(client);
+            if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+                || !TEST_int_eq(SSL_get_state(client), state)
+                || !TEST_size_t_eq(BIO_ctrl_pending(bio), 0))
+                goto end;
+
+        } else {
+            for (i = first_enc; i < ndg; i++) {
+                off = i == first_enc ? enc_off : 0;
+                ret = dglen[i] - (int)off;
+                if (!TEST_int_eq(mempacket_test_inject(bio,
+                                     (const char *)dg[i] + off, ret, -1,
+                                     INJECT_PACKET_IGNORE_REC_SEQ),
+                        ret))
+                    goto end;
+            }
+            ret = SSL_connect(client);
+            if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+                || !TEST_uint64_t_eq(dtls1_get_epoch(cc, SSL3_CC_READ), 3))
+                goto end;
+        }
+    }
+
+    if (!TEST_true(create_ssl_connection(server, client, SSL_ERROR_NONE))
+        || !TEST_true(SSL_is_init_finished(server))
+        || !TEST_true(SSL_is_init_finished(client))
+        || !TEST_int_eq(SSL_write(server, "s", 1), 1)
+        || !TEST_int_eq(SSL_read(client, &buf, 1), 1)
+        || !TEST_uchar_eq(buf, 's')
+        || !TEST_int_eq(SSL_write(client, "c", 1), 1)
+        || !TEST_int_eq(SSL_read(server, &buf, 1), 1)
+        || !TEST_uchar_eq(buf, 'c'))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(server);
+    SSL_free(client);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
+/* Bypass fixture chain validation only for this client-authentication test. */
+static int reordered_ack_verify_cb(int ok, X509_STORE_CTX *ctx)
+{
+    return 1;
+}
+
+#if !defined(OPENSSL_NO_ZLIB) || !defined(OPENSSL_NO_BROTLI) || !defined(OPENSSL_NO_ZSTD)
+#define DTLS13_HAVE_CERT_COMP
+#endif
+
+#ifdef DTLS13_HAVE_CERT_COMP
+static void comp_cert_seen_cb(const SSL *s, int where, int ret)
+{
+    int *seen = SSL_get_app_data(s);
+
+    /* TLS_ST_SR_COMP_CERT */
+    if (seen != NULL && strcmp(SSL_state_string(s), "TRCCC") == 0)
+        *seen = 1;
+}
+#endif
+
+/*
+ * Delay a genuine epoch-2 ACK until part of the client's final flight arrives.
+ * idx 0: empty Certificate, TLS_ST_SR_CERT.
+ * idx 1: nonempty Certificate and CertificateVerify, TLS_ST_SR_CERT_VRFY.
+ * idx 2: nonempty Certificate, TLS_ST_SR_CERT.
+ * idx 3: compressed Certificate. The client ACK sent before it must not have
+ *        its epoch 2 write keys reinstalled, which would reuse record number
+ *        (2, 0) for the Certificate. The ACK is not delayed in this variant.
+ */
+static int test_dtls13_reordered_ack(int idx)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *server = NULL, *client = NULL;
+    SSL_CONNECTION *sc, *cc;
+    unsigned char dg[32][2048], ack[2048], buf;
+    int dglen[32];
+    int ndg = 0, acklen, ret, calls = 0, i, ack_delivered = 0;
+    int testresult = 0, comp_seen = 0;
+    size_t n0 = 0, n2 = 0, d0, d2, off, hdrlen, reclen;
+    OSSL_HANDSHAKE_STATE target = idx == 1 ? TLS_ST_SR_CERT_VRFY : TLS_ST_SR_CERT;
+
+#ifndef DTLS13_HAVE_CERT_COMP
+    if (idx == 3) {
+        TEST_info("No certificate compression algorithm configured");
+        return 1;
+    }
+#endif
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+            &sctx, &cctx, cert, privkey))
+        || !TEST_true(SSL_CTX_set_num_tickets(sctx, 0)))
+        goto end;
+
+    SSL_CTX_set_verify(sctx, SSL_VERIFY_PEER, reordered_ack_verify_cb);
+    SSL_CTX_set_cert_cb(cctx, cert_cb_wait_once, &calls);
+    /* Exercise ordinary Certificate states unless compression is under test. */
+    if (idx != 3)
+        SSL_CTX_set_options(cctx, SSL_OP_NO_TX_CERTIFICATE_COMPRESSION);
+
+    if (idx != 0
+        && (!TEST_true(SSL_CTX_use_certificate_file(cctx, cert, SSL_FILETYPE_PEM))
+            || !TEST_true(SSL_CTX_use_PrivateKey_file(cctx, privkey,
+                SSL_FILETYPE_PEM))))
+        goto end;
+
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL, NULL)))
+        goto end;
+    sc = SSL_CONNECTION_FROM_SSL(server);
+    cc = SSL_CONNECTION_FROM_SSL(client);
+#ifdef DTLS13_HAVE_CERT_COMP
+    if (idx == 3) {
+        if (!TEST_true(SSL_set_app_data(server, &comp_seen)))
+            goto end;
+        SSL_set_info_callback(server, comp_cert_seen_cb);
+    }
+#endif
+
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+        goto end;
+
+    ret = SSL_accept(server);
+    if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ))
+        goto end;
+
+    /* Count and deliver the whole server flight, using the default groups. */
+    while ((ret = BIO_read(SSL_get_rbio(client), dg[ndg], sizeof(dg[0]))) > 0) {
+        dglen[ndg++] = ret;
+        if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg)))
+            goto end;
+    }
+
+    for (i = 0; i < ndg; i++) {
+        if (!TEST_true(count_dgram_records(dg[i], dglen[i], &d0, &d2))
+            || !TEST_int_eq(mempacket_test_inject(SSL_get_rbio(client),
+                                (const char *)dg[i], dglen[i], -1,
+                                INJECT_PACKET_IGNORE_REC_SEQ),
+                dglen[i]))
+            goto end;
+        n0 += d0;
+        n2 += d2;
+    }
+
+    if (!TEST_size_t_gt(n0, 0) || !TEST_size_t_gt(n2, 0))
+        goto end;
+
+    /* Pause for the certificate and hold the ACK produced by the record layer. */
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_X509_LOOKUP)
+        || !TEST_int_eq(calls, 1)
+        || !TEST_uint64_t_eq(dtls1_get_epoch(cc, SSL3_CC_WRITE), 2)
+        || !TEST_true(send_client_ack(cc, n0, 0, n2)))
+        goto end;
+
+    acklen = BIO_read(SSL_get_rbio(server), ack, sizeof(ack));
+    if (!TEST_int_gt(acklen, 0)
+        || !TEST_true(count_dgram_records(ack, acklen, &d0, &d2))
+        || !TEST_size_t_eq(d0, 0)
+        || !TEST_size_t_eq(d2, 1)
+        || !TEST_size_t_eq(BIO_ctrl_pending(SSL_get_rbio(server)), 0))
+        goto end;
+
+    /* Resume the client and capture its entire final flight. */
+    ret = SSL_connect(client);
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_int_eq(calls, 2)
+        || !TEST_false(SSL_is_init_finished(client)))
+        goto end;
+
+    ndg = 0;
+    while ((ret = BIO_read(SSL_get_rbio(server), dg[ndg], sizeof(dg[0]))) > 0) {
+        dglen[ndg++] = ret;
+        if (!TEST_int_lt(ndg, (int)OSSL_NELEM(dg)))
+            goto end;
+    }
+    if (!TEST_int_gt(ndg, 0))
+        goto end;
+
+    if (idx == 3) {
+        pitem *item;
+        dtls_sent_msg *msg;
+        DTLS1_RECORD_NUMBER *rec;
+
+        /*
+         * The ACK above took the first epoch 2 record number. The compressed
+         * Certificate, the first message of the flight, must follow it rather
+         * than restart at (2, 0) by installing the same write keys again.
+         */
+        if (!TEST_true(comp_seen == 0)
+            || !TEST_ptr(item = pqueue_peek(&cc->d1->sent_messages))
+            || !TEST_ptr(msg = item->data)
+            || !TEST_ptr(rec = ossl_list_record_number_head(&msg->rec_nums))
+            || !TEST_uint64_t_eq(rec->epoch, 2)
+            || !TEST_uint64_t_gt(rec->seqnum, 0))
+            goto end;
+
+        /* Deliver the whole flight; the held ACK is not part of this variant. */
+        for (i = 0; i < ndg; i++)
+            if (!TEST_int_eq(mempacket_test_inject(SSL_get_rbio(server),
+                                 (const char *)dg[i], dglen[i], -1,
+                                 INJECT_PACKET_IGNORE_REC_SEQ),
+                    dglen[i]))
+                goto end;
+        ndg = 0;
+        ret = SSL_accept(server);
+        /* Prove the compressed path ran, then that the flight was accepted. */
+        if (!TEST_true(comp_seen)
+            || !TEST_int_eq(ret, 1))
+            goto end;
+        ack_delivered = 1;
+    }
+
+    /* Keep retransmission from interfering with record-by-record delivery. */
+    sc->d1->next_timeout = ossl_time_add(ossl_time_now(), ossl_seconds2time(3600));
+    for (i = 0; i < ndg; i++) {
+        for (off = 0; off < (size_t)dglen[i]; off += hdrlen + reclen) {
+            const unsigned char *r = dg[i] + off;
+            size_t remaining = (size_t)dglen[i] - off;
+            int final_record;
+
+            /* The loop guarantees r[0] exists; reject unsupported headers. */
+            if (!TEST_int_eq(r[0] & DTLS13_UNI_HDR_FIX_BITS_MASK,
+                    DTLS13_UNI_HDR_FIX_BITS)
+                || !TEST_int_eq(r[0] & DTLS13_UNI_HDR_CID_BIT, 0)
+                || !TEST_int_eq(r[0] & DTLS13_UNI_HDR_EPOCH_BITS_MASK, 2))
+                goto end;
+
+            hdrlen = 1 + ((r[0] & DTLS13_UNI_HDR_SEQ_BIT) != 0 ? 2 : 1);
+            if (!TEST_size_t_ge(remaining, hdrlen))
+                goto end;
+            if ((r[0] & DTLS13_UNI_HDR_LEN_BIT) != 0) {
+                if (!TEST_size_t_ge(remaining, hdrlen + 2))
+                    goto end;
+                reclen = ((size_t)r[hdrlen] << 8) | r[hdrlen + 1];
+                hdrlen += 2;
+            } else {
+                reclen = remaining - hdrlen;
+            }
+
+            if (!TEST_size_t_le(reclen, remaining - hdrlen)
+                || !TEST_false(SSL_is_init_finished(server)))
+                goto end;
+
+            final_record = i == ndg - 1 && hdrlen + reclen == remaining;
+            if (!TEST_int_eq(mempacket_test_inject(SSL_get_rbio(server),
+                                 (const char *)r, (int)(hdrlen + reclen), -1,
+                                 INJECT_PACKET_IGNORE_REC_SEQ),
+                    (int)(hdrlen + reclen)))
+                goto end;
+
+            ret = SSL_accept(server);
+            if (final_record) {
+                if (!TEST_true(ack_delivered) || !TEST_int_eq(ret, 1))
+                    goto end;
+            } else if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+                || !TEST_false(SSL_is_init_finished(server))) {
+                goto end;
+            }
+
+            if (!ack_delivered && SSL_get_state(server) == target) {
+                if (!TEST_int_eq(SSL_get0_peer_certificate(server) != NULL, idx != 0)
+                    || !TEST_size_t_gt(pqueue_size(&sc->d1->sent_messages), 0)
+                    || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout))
+                    || !TEST_int_eq(mempacket_test_inject(SSL_get_rbio(server),
+                                        (const char *)ack, acklen, -1,
+                                        INJECT_PACKET_IGNORE_REC_SEQ),
+                        acklen))
+                    goto end;
+
+                ret = SSL_accept(server);
+                if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+                    || !TEST_int_eq(SSL_get_state(server), target)
+                    || !TEST_false(SSL_is_init_finished(server))
+                    || !TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 0)
+                    || !TEST_true(ossl_time_is_zero(sc->d1->next_timeout)))
+                    goto end;
+
+                ack_delivered = 1;
+            }
+        }
+    }
+
+    if (!TEST_true(ack_delivered)
+        || !TEST_true(SSL_is_init_finished(server))
+        || !TEST_size_t_eq(BIO_ctrl_pending(SSL_get_rbio(server)), 0)
+        || !TEST_false(SSL_is_init_finished(client)))
+        goto end;
+
+    /* Consume the final ACK and complete the client's handshake. */
+    ret = SSL_read(client, &buf, sizeof(buf));
+    if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ)
+        || !TEST_true(SSL_is_init_finished(client))
+        || !TEST_int_eq(SSL_get_state(client), TLS_ST_OK)
+        || !TEST_size_t_eq(pqueue_size(&cc->d1->sent_messages), 0)
+        || !TEST_true(ossl_time_is_zero(cc->d1->next_timeout)))
+        goto end;
+
+    if (!TEST_int_eq(SSL_write(server, "s", 1), 1)
+        || !TEST_int_eq(SSL_read(client, &buf, 1), 1)
+        || !TEST_uchar_eq(buf, 's')
+        || !TEST_int_eq(SSL_write(client, "c", 1), 1)
+        || !TEST_int_eq(SSL_read(server, &buf, 1), 1)
+        || !TEST_uchar_eq(buf, 'c'))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(server);
+    SSL_free(client);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}

 #endif /* OPENSSL_NO_DTLS1_3 */

@@ -2533,6 +3589,11 @@ int setup_tests(void)
     ADD_TEST(test_dtls13_retained_epoch_app_data_rejected);
     ADD_TEST(test_dtls13_retained_epoch_ack_authority);
     ADD_ALL_TESTS(test_dtls13_hrr_lost_server_hello, 2);
+    ADD_TEST(test_dtls13_server_flight_partial_ack);
+    ADD_TEST(test_dtls13_server_flight_full_ack);
+    ADD_ALL_TESTS(test_dtls13_plaintext_ack_epoch, 3);
+    ADD_ALL_TESTS(test_dtls13_delayed_server_ack, 4);
+    ADD_ALL_TESTS(test_dtls13_reordered_ack, 4);
 #endif
     return 1;
 }