Commit 4d91a49e55 for openssl.org
commit 4d91a49e557ebe2d3f81ad5517dfaf8933dddfb0
Author: Ryan Hooper <ryanh@openssl.foundation>
Date: Wed Sep 23 11:32:46 2026 -0400
DTLS 1.3: Retain the previous read epoch to recover from lost ACKs
After processing the client's Finished or a peer KeyUpdate, the read
record layer advanced to the new epoch and the old one was freed
immediately. If the ACK we sent for that message was lost, the peer's
retransmission arrived at the now-discarded epoch: it could not be
authenticated, so no replacement ACK was ever produced and the peer
exhausted its retransmission budget.
RFC9147 section 5.8.1 requires the finished peer to ACK retransmissions
of the other side's final flight, and section 8 requires receive keys
from before a KeyUpdate to be retained until decryption with the new
keys succeeds. Retain exactly one prior read epoch's record layer
instead of freeing it, add a record-layer dispatch hook so the DTLS
method can take ownership of it, and authenticate a record at that
epoch with its own retained keys and replay window when the current
epoch's epoch-bits check would otherwise just drop it. This isn't
limited to replacing lost ACKs: a previous *application* epoch (from
KeyUpdate recovery) can also legitimately supply application data that
arrives reordered relative to the epoch change.
Successful authentication with the retained keys doesn't by itself
prove a record is a retransmission, so let it reach the handshake
layer instead of deciding that here: the DTLS 1.2-only Finished
special case (retransmit our own flight) is skipped for DTLS 1.3, and
the existing out-of-sequence duplicate-ACK path is widened from
post-handshake epochs only to include the epoch a Finished is sent
at -- the sequence and epoch checks in that path are what actually
decide whether a retained-epoch record can be acknowledged.
Fixes: #32891
Assisted-by: Claude:claude-sonnet-5
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Merge-date: Fri Oct 2 06:36:27 2026
Merged-from: https://github.com/openssl/openssl/pull/32963
diff --git a/include/internal/recordmethod.h b/include/internal/recordmethod.h
index a3b30f22e7..21f82e6bdc 100644
--- a/include/internal/recordmethod.h
+++ b/include/internal/recordmethod.h
@@ -360,6 +360,14 @@ struct ossl_record_method_st {
* data. Buffers are automatically reallocated on next read/write.
*/
int (*free_buffers)(OSSL_RECORD_LAYER *rl);
+
+ /*
+ * DTLS only. Instead of freeing previous record layer hand it over
+ * so that a retransmission arriving for the previous epoch can still
+ * be processed correctly. The current record layer takes ownership
+ * of the previous record layer.
+ */
+ int (*set_prev_epoch_rl)(OSSL_RECORD_LAYER *rl, OSSL_RECORD_LAYER *prev);
};
/* Standard built-in record methods */
diff --git a/ssl/record/methods/dtls_meth.c b/ssl/record/methods/dtls_meth.c
index 96bba977b5..8286b4e5a9 100644
--- a/ssl/record/methods/dtls_meth.c
+++ b/ssl/record/methods/dtls_meth.c
@@ -453,6 +453,19 @@ uint64_t dtls13_reconstruct_seq_num(uint64_t max_seq_num, uint64_t truncated,
return best;
}
+/*
+ * Epoch 2 is always the fixed DTLS 1.3 handshake epoch: no compliant peer
+ * ever sends application data there (unlike epoch 1's early data). A record
+ * that only authenticates because of that epoch's retained keys must never
+ * be delivered as application data -- retained *application* epochs (3+,
+ * from KeyUpdate recovery) are unaffected, since they can legitimately
+ * carry reordered application traffic.
+ */
+int dtls_prev_epoch_allows_type(const OSSL_RECORD_LAYER *crypto_rl, int type)
+{
+ return !(crypto_rl->epoch == 2 && type == SSL3_RT_APPLICATION_DATA);
+}
+
/*-
* Call this to get a new input record.
* It will return <= 0 if more data is needed, normally due to an error
@@ -474,6 +487,7 @@ int dtls_get_more_records(OSSL_RECORD_LAYER *rl)
size_t rechdrlen = 0;
size_t recseqnumoffs = 0;
int buffered_record = 0;
+ OSSL_RECORD_LAYER *crypto_rl;
rl->num_recs = 0;
rl->curr_rec = 0;
@@ -489,6 +503,7 @@ int dtls_get_more_records(OSSL_RECORD_LAYER *rl)
}
again:
+ crypto_rl = rl;
memset(recseqnum, 0, sizeof(recseqnum));
/* get something from the wire */
@@ -607,6 +622,18 @@ again:
*/
if (eebits == 2 && (epoch64 == 1 || epoch64 == 0)) {
epoch64 = 2;
+ } else if (rl->prev_epoch_rl != NULL
+ && (rl->prev_epoch_rl->epoch
+ & DTLS13_UNI_HDR_EPOCH_BITS_MASK)
+ == eebits) {
+ /*
+ * This may be a retransmission at the epoch we have just
+ * moved on from, sent because the ACK we gave it was
+ * lost. Authenticate it with that epoch's own retained
+ * keys and replay window instead of dropping it.
+ */
+ epoch64 = rl->prev_epoch_rl->epoch;
+ crypto_rl = rl->prev_epoch_rl;
} else {
rr->length = 0;
rl->packet_length = 0;
@@ -726,9 +753,9 @@ again:
&& rl->version == DTLS1_3_VERSION
&& !(rl->in_init && rl->epoch == 0)
&& ((rl->packet_length < rechdrlen + DTLS13_CIPHERTEXT_MINSIZE)
- || (rl->sn_enc_ctx == NULL && rl->mac_ctx == NULL)
- || (rl->sn_enc_ctx != NULL
- && !dtls_crypt_sequence_number(rl->sn_enc_ctx,
+ || (crypto_rl->sn_enc_ctx == NULL && crypto_rl->mac_ctx == NULL)
+ || (crypto_rl->sn_enc_ctx != NULL
+ && !dtls_crypt_sequence_number(crypto_rl->sn_enc_ctx,
recseqnum + recseqnumoffs,
recseqnumlen,
rl->packet + rechdrlen)))) {
@@ -738,7 +765,7 @@ again:
goto again;
}
- if (rl->version == DTLS1_3_VERSION && rr->epoch == rl->epoch
+ if (rl->version == DTLS1_3_VERSION && rr->epoch == crypto_rl->epoch
&& DTLS13_UNI_HDR_FIX_BITS_IS_SET(rr->type)) {
/* Reconstruct current-epoch unified records using its replay window. */
uint64_t truncated = 0;
@@ -753,7 +780,7 @@ again:
for (i = 0; i < recseqnumlen; i++)
truncated = (truncated << 8) | recseqnum[recseqnumoffs + i];
- rl->sequence = dtls13_reconstruct_seq_num(rl->bitmap.max_seq_num,
+ crypto_rl->sequence = dtls13_reconstruct_seq_num(crypto_rl->bitmap.max_seq_num,
truncated, recseqnumlen);
} else {
/*
@@ -770,7 +797,7 @@ again:
}
/* match epochs. NULL means the packet is dropped on the floor */
- bitmap = dtls_get_bitmap(rl, rr, &is_next_epoch);
+ bitmap = dtls_get_bitmap(crypto_rl, rr, &is_next_epoch);
if (bitmap == NULL && !is_next_epoch) {
rr->length = 0;
rl->packet_length = 0; /* dump this record */
@@ -823,12 +850,24 @@ again:
goto again;
}
+ /*
+ * dtls_record_replay_check() and dtls_process_record() read and write
+ * whichever OSSL_RECORD_LAYER they are passed, not rr. When crypto_rl is
+ * the retained previous epoch, seed its record slot from rr so they
+ * have this record's data to work with; a no-op when crypto_rl == rl.
+ */
+ if (crypto_rl != rl) {
+ crypto_rl->packet = rl->packet;
+ crypto_rl->packet_length = rl->packet_length;
+ crypto_rl->rrec[0] = *rr;
+ }
+
#ifndef OPENSSL_NO_SCTP
/* Only do replay check if no SCTP bio (also check for NULL bio) */
if (rl->bio == NULL || !BIO_dgram_is_sctp(rl->bio)) {
#endif
/* Check whether this is a repeat, or aged record. */
- if (!dtls_record_replay_check(rl, bitmap)) {
+ if (!dtls_record_replay_check(crypto_rl, bitmap)) {
rr->length = 0;
rl->packet_length = 0; /* dump this record */
goto again; /* get another record */
@@ -841,9 +880,10 @@ again:
if (rr->length == 0)
goto again;
- if (!dtls_process_record(rl, bitmap)) {
- if (rl->alert != SSL_AD_NO_ALERT) {
+ if (!dtls_process_record(crypto_rl, bitmap)) {
+ if (crypto_rl->alert != SSL_AD_NO_ALERT) {
/* dtls_process_record() called RLAYERfatal */
+ rl->alert = crypto_rl->alert;
return OSSL_RECORD_RETURN_FATAL;
}
rr->length = 0;
@@ -851,11 +891,22 @@ again:
goto again; /* get another record */
}
+ if (crypto_rl != rl) {
+ *rr = crypto_rl->rrec[0];
+ rl->packet_length = 0;
+ }
+
if (rl->funcs->post_process_record && !rl->funcs->post_process_record(rl, rr)) {
/* RLAYERfatal already called */
return OSSL_RECORD_RETURN_FATAL;
}
+ /* rr->type is only decoded after post_process_record() above. */
+ if (crypto_rl != rl && !dtls_prev_epoch_allows_type(crypto_rl, rr->type)) {
+ rr->length = 0;
+ goto again;
+ }
+
if (rr->length == 0) {
/* No payload data in this record. Dump it */
rl->packet_length = 0;
@@ -866,22 +917,23 @@ again:
return OSSL_RECORD_RETURN_SUCCESS;
}
-static int dtls_free(OSSL_RECORD_LAYER *rl)
+/*
+ * Push any unread buffered bytes and any records already buffered in
+ * unprocessed_rcds (see is_next_epoch in dtls_get_more_records()) forward
+ * into rl->next, so a pending handshake epoch transition still completes.
+ * Must run when rl stops being the active read layer, not deferred until
+ * it is eventually freed -- see dtls_free() and dtls_set_prev_epoch_rl().
+ */
+static int dtls_forward_pending_records(OSSL_RECORD_LAYER *rl)
{
- TLS_BUFFER *rbuf;
+ TLS_BUFFER *rbuf = &rl->rbuf;
size_t left, written;
pitem *item;
DTLS_RLAYER_RECORD_DATA *rdata;
int ret = 1;
- rbuf = &rl->rbuf;
-
left = rbuf->left;
if (left > 0) {
- /*
- * This record layer is closing but we still have data left in our
- * buffer. It must be destined for the next epoch - so push it there.
- */
ret = BIO_write_ex(rl->next, rbuf->buf + rbuf->offset, left, &written);
rbuf->left = 0;
}
@@ -889,7 +941,6 @@ static int dtls_free(OSSL_RECORD_LAYER *rl)
while ((item = pqueue_pop(&rl->unprocessed_rcds)) != NULL) {
rdata = (DTLS_RLAYER_RECORD_DATA *)item->data;
- /* Push to the next record layer */
ret &= BIO_write_ex(rl->next, rdata->packet, rdata->packet_length,
&written);
OPENSSL_free(rdata->packet);
@@ -897,9 +948,47 @@ static int dtls_free(OSSL_RECORD_LAYER *rl)
pitem_free(item);
}
+ return ret;
+}
+
+static int dtls_free(OSSL_RECORD_LAYER *rl)
+{
+ int ret = dtls_forward_pending_records(rl);
+
+ if (rl->prev_epoch_rl != NULL) {
+ ret &= dtls_free(rl->prev_epoch_rl);
+ rl->prev_epoch_rl = NULL;
+ }
+
return tls_free(rl) && ret;
}
+/*
+ * Take ownership of the previous record layer, just superseded by the
+ * record layer, instead of the caller freeing it.
+ */
+static int dtls_set_prev_epoch_rl(OSSL_RECORD_LAYER *rl, OSSL_RECORD_LAYER *prev)
+{
+ int ret = dtls_forward_pending_records(prev);
+
+ if (prev->prev_epoch_rl != NULL) {
+ ret &= dtls_free(prev->prev_epoch_rl);
+ prev->prev_epoch_rl = NULL;
+ }
+
+ /*
+ * prev's own read buffer is never used again: authenticating a
+ * retransmission at this epoch reuses the active layer's packet buffer
+ * (see the crypto_rl handling in dtls_get_more_records()). Release it
+ * now instead of leaving it allocated until the whole layer chain is
+ * torn down or the caller happens to call SSL_free_buffers().
+ */
+ ret &= tls_release_read_buffer(prev);
+
+ rl->prev_epoch_rl = prev;
+ return ret;
+}
+
static int
dtls_new_record_layer(OSSL_LIB_CTX *libctx, const char *propq, int vers,
int role, int direction, int level, uint64_t epoch,
@@ -1155,5 +1244,6 @@ const OSSL_RECORD_METHOD ossl_dtls_record_method = {
dtls_set_curr_mtu,
dtls_unprocessed_records,
tls_alloc_buffers,
- tls_free_buffers
+ tls_free_buffers,
+ dtls_set_prev_epoch_rl
};
diff --git a/ssl/record/methods/ktls_meth.c b/ssl/record/methods/ktls_meth.c
index f09acd987e..8e39c17c34 100644
--- a/ssl/record/methods/ktls_meth.c
+++ b/ssl/record/methods/ktls_meth.c
@@ -614,5 +614,6 @@ const OSSL_RECORD_METHOD ossl_ktls_record_method = {
NULL,
NULL,
ktls_alloc_buffers,
- tls_free_buffers
+ tls_free_buffers,
+ NULL /* set_prev_epoch_rl: DTLS only */
};
diff --git a/ssl/record/methods/recmethod_local.h b/ssl/record/methods/recmethod_local.h
index 6363f82695..6efb4424e1 100644
--- a/ssl/record/methods/recmethod_local.h
+++ b/ssl/record/methods/recmethod_local.h
@@ -353,6 +353,15 @@ struct ossl_record_layer_st {
/* records being received in the current epoch */
DTLS_BITMAP bitmap;
+ /*
+ * DTLS 1.3 read layers only. The immediately preceding read epoch's
+ * record layer, retained (instead of freed) across an epoch bump so a
+ * retransmission of the message that caused the bump can still be
+ * authenticated with its own now-superseded keys and bitmap, if the
+ * ACK we sent for it was lost.
+ */
+ OSSL_RECORD_LAYER *prev_epoch_rl;
+
/* DTLS curr mtu size */
size_t curr_mtu;
@@ -469,6 +478,7 @@ size_t dtls_get_rec_header_size(uint8_t hdr_first_byte);
int dtls_crypt_sequence_number(EVP_CIPHER_CTX *ctx, unsigned char *seq, size_t seqlen,
unsigned char *rec_data);
int dtls_get_more_records(OSSL_RECORD_LAYER *rl);
+int dtls_prev_epoch_allows_type(const OSSL_RECORD_LAYER *crypto_rl, int type);
int dtls_prepare_record_header(OSSL_RECORD_LAYER *rl,
WPACKET *thispkt,
@@ -526,6 +536,7 @@ int tls_set_options(OSSL_RECORD_LAYER *rl, const OSSL_PARAM *options);
const COMP_METHOD *tls_get_compression(OSSL_RECORD_LAYER *rl);
void tls_set_max_frag_len(OSSL_RECORD_LAYER *rl, size_t max_frag_len);
int tls_setup_read_buffer(OSSL_RECORD_LAYER *rl);
+int tls_release_read_buffer(OSSL_RECORD_LAYER *rl);
int tls_setup_write_buffer(OSSL_RECORD_LAYER *rl, size_t numwpipes,
size_t firstlen, size_t nextlen);
diff --git a/ssl/record/methods/tls_common.c b/ssl/record/methods/tls_common.c
index 180993bf20..5541b49feb 100644
--- a/ssl/record/methods/tls_common.c
+++ b/ssl/record/methods/tls_common.c
@@ -264,15 +264,18 @@ int tls_setup_read_buffer(OSSL_RECORD_LAYER *rl)
return 1;
}
-static int tls_release_read_buffer(OSSL_RECORD_LAYER *rl)
+int tls_release_read_buffer(OSSL_RECORD_LAYER *rl)
{
TLS_BUFFER *b;
b = &rl->rbuf;
+ if (b->buf == NULL)
+ return 1;
if ((rl->options & SSL_OP_CLEANSE_PLAINTEXT) != 0)
OPENSSL_cleanse(b->buf, b->len);
OPENSSL_free(b->buf);
b->buf = NULL;
+ b->len = 0;
rl->packet = NULL;
rl->packet_length = 0;
return 1;
@@ -2326,5 +2329,6 @@ const OSSL_RECORD_METHOD ossl_tls_record_method = {
NULL,
NULL,
tls_alloc_buffers,
- tls_free_buffers
+ tls_free_buffers,
+ NULL /* set_prev_epoch_rl: DTLS only */
};
diff --git a/ssl/record/rec_layer_d1.c b/ssl/record/rec_layer_d1.c
index 5199ac1d5a..52a12a8a13 100644
--- a/ssl/record/rec_layer_d1.c
+++ b/ssl/record/rec_layer_d1.c
@@ -374,7 +374,8 @@ start:
return 0;
}
- if (rr->type == SSL3_RT_HANDSHAKE && SSL_CONNECTION_IS_DTLS13(sc)) {
+ if ((rr->type == SSL3_RT_HANDSHAKE || rr->type == SSL3_RT_ACK)
+ && SSL_CONNECTION_IS_DTLS13(sc)) {
sc->s3.tmp.record_epoch = rr->epoch;
sc->s3.tmp.record_seq_num = rr->seq_num;
}
@@ -570,9 +571,25 @@ start:
/*
* This may just be a stale retransmit. Also sanity check that we have
- * at least enough record bytes for a message header
+ * at least enough record bytes for a message header.
+ *
+ * For DTLS 1.3, a record at exactly the previous read epoch is not
+ * necessarily stale: it authenticated (see dtls_get_more_records()'s
+ * retained prev_epoch_rl handling), but that alone doesn't prove it's
+ * a retransmission. Let it through to the normal handshake-message
+ * path below instead of discarding it here -- the sequence and epoch
+ * checks there (dtls_record_from_retained_epoch(),
+ * dtls_prev_epoch_allows_type()) are what actually decide whether it
+ * can be acknowledged. This retained-epoch exception is for handshake
+ * records only (rr->type == SSL3_RT_HANDSHAKE above); an ACK record
+ * still has to be tied to the epoch that authenticated it before it
+ * can touch d1->sent_messages, which dtls_process_ack() enforces
+ * separately.
*/
- if (rr->epoch != dtls1_get_epoch(sc, SSL3_CC_READ)
+ if ((rr->epoch != dtls1_get_epoch(sc, SSL3_CC_READ)
+ && !(SSL_CONNECTION_IS_DTLS13(sc)
+ && dtls1_get_epoch(sc, SSL3_CC_READ) > 0
+ && rr->epoch == dtls1_get_epoch(sc, SSL3_CC_READ) - 1))
|| rr->length < DTLS1_HM_HEADER_LENGTH) {
if (!ssl_release_record(sc, rr, 0))
return -1;
@@ -584,8 +601,16 @@ start:
/*
* If we are server, we may have a repeated FINISHED of the client
* here, then retransmit our CCS and FINISHED.
+ *
+ * DTLS 1.3 has proper ACK records, so this DTLS 1.2-only fallback
+ * (which infers loss from a bare repeated Finished and reacts by
+ * blindly retransmitting our own flight) is skipped for it. A
+ * repeated DTLS 1.3 Finished instead falls through to the normal
+ * handshake-message path below, which ACKs a message it has
+ * already fully processed without reprocessing it (see the
+ * record_epoch check in statem_dtls.c).
*/
- if (msg_type == SSL3_MT_FINISHED) {
+ if (!SSL_CONNECTION_IS_DTLS13(sc) && msg_type == SSL3_MT_FINISHED) {
if (dtls1_check_timeout_num(sc) < 0) {
/* SSLfatal) already called */
return -1;
diff --git a/ssl/record/rec_layer_s3.c b/ssl/record/rec_layer_s3.c
index c24f467fab..ef7b469b65 100644
--- a/ssl/record/rec_layer_s3.c
+++ b/ssl/record/rec_layer_s3.c
@@ -1584,8 +1584,22 @@ int ssl_set_new_record_layer(SSL_CONNECTION *s, int version,
* case the record layer is still referenced by those buffered messages for
* potential retransmit. Only when those buffered messages get freed do we
* free the record layer object (see dtls1_hm_fragment_free)
+ *
+ * For a DTLS 1.3 read layer, hand the old one to the new one for
+ * retention instead of freeing it: a retransmission of the message that
+ * caused this epoch bump may still arrive at the old epoch if the ACK we
+ * sent for it was lost, and it needs the old epoch's keys and replay
+ * window to be authenticated (see dtls_get_more_records()).
*/
- if (!SSL_CONNECTION_IS_DTLS(s)
+ if (SSL_CONNECTION_IS_DTLS13(s)
+ && direction == OSSL_RECORD_DIRECTION_READ
+ && *thismethod != NULL
+ && meth->set_prev_epoch_rl != NULL) {
+ if (!meth->set_prev_epoch_rl(newrl, *thisrl)) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+ } else if (!SSL_CONNECTION_IS_DTLS(s)
|| direction == OSSL_RECORD_DIRECTION_READ
|| pqueue_peek(&s->d1->sent_messages) == NULL) {
if (*thismethod != NULL && !(*thismethod)->free(*thisrl)) {
diff --git a/ssl/statem/statem_dtls.c b/ssl/statem/statem_dtls.c
index 8f4b20b096..76f52b68f7 100644
--- a/ssl/statem/statem_dtls.c
+++ b/ssl/statem/statem_dtls.c
@@ -859,7 +859,21 @@ err:
return -1;
}
-static int dtls1_process_out_of_seq_message(SSL_CONNECTION *s,
+/*
+ * True if the message currently being processed only authenticated because
+ * the read record layer retained a previous epoch's keys for retransmission
+ * recovery -- i.e. it did not actually arrive at the currently active
+ * read epoch. Such a message must never be treated as new content: it may
+ * only, at most, trigger a replacement ACK for something already fully
+ * processed.
+ */
+int dtls_record_from_retained_epoch(SSL_CONNECTION *s)
+{
+ return SSL_CONNECTION_IS_DTLS13(s)
+ && s->s3.tmp.record_epoch != dtls1_get_epoch(s, SSL3_CC_READ);
+}
+
+int dtls1_process_out_of_seq_message(SSL_CONNECTION *s,
const struct hm_header_st *msg_hdr)
{
int i = -1;
@@ -884,10 +898,17 @@ static int dtls1_process_out_of_seq_message(SSL_CONNECTION *s,
/*
* Discard the message if sequence number was already there, is too far
- * in the future, already in the queue or if we received a FINISHED
- * before the SERVER_HELLO, which then must be a stale retransmit.
+ * in the future, already in the queue, if we received a FINISHED
+ * before the SERVER_HELLO (which then must be a stale retransmit), or
+ * if it only authenticated via a retained previous epoch: such a
+ * message must never be buffered for future reassembly and eventually
+ * processed as new content, no matter what sequence number it claims.
*/
- if (msg_hdr->seq <= s->d1->handshake_read_seq || msg_hdr->seq > s->d1->handshake_read_seq + 10 || item != NULL || (s->d1->handshake_read_seq == 0 && msg_hdr->type == SSL3_MT_FINISHED)) {
+ if (msg_hdr->seq <= s->d1->handshake_read_seq
+ || msg_hdr->seq > s->d1->handshake_read_seq + 10
+ || item != NULL
+ || (s->d1->handshake_read_seq == 0 && msg_hdr->type == SSL3_MT_FINISHED)
+ || dtls_record_from_retained_epoch(s)) {
unsigned char devnull[256];
while (frag_len) {
@@ -899,11 +920,17 @@ static int dtls1_process_out_of_seq_message(SSL_CONNECTION *s,
frag_len -= readbytes;
}
/*
- * A lost ACK can cause an already processed post-handshake message to
- * be retransmitted in a new record. ACK it without processing it again.
+ * A lost ACK can cause an already processed message to be
+ * retransmitted in a new record. ACK it without processing it
+ * again. Epoch 2 is included alongside the post-handshake epochs
+ * (3+) so that a client's Finished, retransmitted after the server
+ * has already moved on to epoch 3, still gets ACKed instead of
+ * silently dropped (see dtls_get_more_records()'s retained
+ * prev_epoch_rl handling, which is what let this record
+ * authenticate at all).
*/
if (SSL_CONNECTION_IS_DTLS13(s)
- && s->s3.tmp.record_epoch >= 3
+ && s->s3.tmp.record_epoch >= 2
&& msg_hdr->seq < s->d1->handshake_read_seq
&& dtls_msg_needs_ack(!s->server, msg_hdr->type)) {
if (!add_record_to_ack_list(s))
@@ -1159,8 +1186,14 @@ redo:
* (or dropped)--no further processing at this time
* While listening, we accept seq 1 (ClientHello with cookie)
* although we're still expecting seq 0 (ClientHello)
+ *
+ * A message that only authenticated via a retained previous epoch must
+ * always be routed to dtls1_process_out_of_seq_message(), even when its
+ * claimed sequence number happens to match the next expected one: that
+ * match does not mean this content is actually new.
*/
- if (msg_hdr.seq != s->d1->handshake_read_seq) {
+ if (msg_hdr.seq != s->d1->handshake_read_seq
+ || dtls_record_from_retained_epoch(s)) {
if (!s->server
|| msg_hdr.seq != 0
|| s->d1->handshake_read_seq != 1
@@ -1373,6 +1406,22 @@ MSG_PROCESS_RETURN dtls_process_ack(SSL_CONNECTION *s, PACKET *pkt)
return MSG_PROCESS_ERROR;
}
+ /*
+ * Epoch 2 is the fixed handshake epoch and is never used again once
+ * epoch 3 (the first application epoch) is installed. An ACK that
+ * only authenticated via a retained epoch-2 layer must not be
+ * trusted to cancel retransmission of a post-handshake (epoch 3+)
+ * flight. Retained epochs 3+ are unrestricted: unlike epoch 2, which
+ * is never reissued, each of those epochs was freshly minted by an
+ * SSL_key_update() call and gets superseded by the next one, so a
+ * legitimate delayed ACK can authenticate behind a message's own
+ * recorded epoch with no protocol violation.
+ */
+ if (dtls_record_from_retained_epoch(s)
+ && s->s3.tmp.record_epoch == 2
+ && epoch > 2)
+ continue;
+
iter = pqueue_iterator(&s->d1->sent_messages);
while ((item = pqueue_next(&iter)) != NULL) {
diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h
index 5cdb953eac..079ff703c2 100644
--- a/ssl/statem/statem_local.h
+++ b/ssl/statem/statem_local.h
@@ -128,6 +128,9 @@ __owur int tls_get_message_header(SSL_CONNECTION *s, int *mt);
__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 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);
/* Message construction and processing functions */
diff --git a/test/dtls13_internal_test.c b/test/dtls13_internal_test.c
index 852052afe3..d7e787750a 100644
--- a/test/dtls13_internal_test.c
+++ b/test/dtls13_internal_test.c
@@ -1505,6 +1505,819 @@ end:
SSL_CTX_free(cctx);
return testresult;
}
+
+/*
+ * Recover from a lost ACK for the client's Finished: the server must still
+ * accept a retransmission of Finished at its original (now superseded) read
+ * epoch and replace the lost ACK, rather than silently discarding it.
+ */
+static int test_dtls13_finished_ack_loss_recovers(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *cc;
+ unsigned char buf, discard[2048];
+ int ret, dropped, 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;
+ cc = SSL_CONNECTION_FROM_SSL(client);
+
+ 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;
+ /* Sends the client's Finished. */
+ ret = SSL_connect(client);
+ if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+ goto end;
+ /* Server processes Finished, completes, and queues its ACK. */
+ if (!TEST_int_eq(SSL_accept(server), 1)
+ || !TEST_size_t_eq(pqueue_size(&cc->d1->sent_messages), 1))
+ goto end;
+
+ /* Drop the server's ACK for the client's Finished. */
+ dropped = 0;
+ while (BIO_read(SSL_get_rbio(client), discard, sizeof(discard)) > 0)
+ dropped++;
+ if (!TEST_int_gt(dropped, 0)
+ || !TEST_size_t_eq(BIO_ctrl_pending(SSL_get_rbio(client)), 0))
+ goto end;
+
+ /* Force the client to retransmit Finished at its original epoch. */
+ cc->d1->next_timeout = ossl_time_subtract(ossl_time_now(),
+ ossl_seconds2time(1));
+ if (!TEST_true(SSL_handle_events(client)))
+ goto end;
+
+ /*
+ * The server has already moved to the next read epoch and discarded the
+ * old one, so it cannot authenticate this retransmission and never
+ * produces a replacement ACK. This is the assertion that must flip once
+ * the previous read epoch is retained: a fresh ACK should appear here.
+ */
+ ret = SSL_read(server, &buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_size_t_gt(BIO_ctrl_pending(SSL_get_rbio(client)), 0))
+ goto end;
+
+ /* The client picks up the replacement ACK and completes. */
+ 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)
+ || !TEST_size_t_eq(pqueue_size(&cc->d1->sent_messages), 0)
+ || !TEST_true(ossl_time_is_zero(cc->d1->next_timeout)))
+ 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, '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;
+}
+
+/*
+ * Recover from a lost ACK for a KeyUpdate: the receiver must still accept a
+ * retransmission of the KeyUpdate at its original (now superseded) read
+ * epoch and replace the lost ACK, rather than silently discarding it and
+ * leaving the initiator retransmitting forever. idx == 0 is the server
+ * initiating (the issue's reported case); idx == 1 is the client initiating
+ * (noted in the issue as sharing the same problem but untested there).
+ */
+static int test_dtls13_keyupdate_ack_loss_recovers(int idx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL, *sender, *receiver;
+ SSL_CONNECTION *sc, *cc, *sender_c;
+ unsigned char buf, discard[2048];
+ int ret, dropped, testresult = 0;
+
+ ticket_count = 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)))
+ goto end;
+ SSL_CTX_set_session_cache_mode(cctx, SSL_SESS_CACHE_CLIENT);
+ SSL_CTX_sess_set_new_cb(cctx, count_ticket);
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL, NULL))
+ || !TEST_true(create_ssl_connection(server, client, SSL_ERROR_NONE)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+ cc = SSL_CONNECTION_FROM_SSL(client);
+
+ /*
+ * Let the handshake ticket ACKs through, so neither side has a pending
+ * flight of its own and anything observed below is unambiguously this
+ * bug, not #32878's separate flight-cancellation problem.
+ */
+ ret = SSL_read(server, &buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_int_eq(ticket_count, 2)
+ || !TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 0)
+ || !TEST_true(ossl_time_is_zero(sc->d1->next_timeout)))
+ goto end;
+
+ sender = idx ? client : server;
+ receiver = idx ? server : client;
+ sender_c = idx ? cc : sc;
+
+ if (!TEST_true(SSL_key_update(sender, SSL_KEY_UPDATE_NOT_REQUESTED)))
+ goto end;
+ ret = SSL_do_handshake(sender);
+ if (!TEST_int_eq(SSL_get_error(sender, ret), SSL_ERROR_WANT_READ)
+ || !TEST_size_t_eq(pqueue_size(&sender_c->d1->sent_messages), 1))
+ goto end;
+
+ /* Receiver processes the KeyUpdate, bumps its read epoch, and ACKs it. */
+ ret = SSL_read(receiver, &buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(receiver, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ /* Drop that ACK. */
+ dropped = 0;
+ while (BIO_read(SSL_get_rbio(sender), discard, sizeof(discard)) > 0)
+ dropped++;
+ if (!TEST_int_gt(dropped, 0)
+ || !TEST_size_t_eq(pqueue_size(&sender_c->d1->sent_messages), 1))
+ goto end;
+
+ /* Force the sender to retransmit the KeyUpdate at its original epoch. */
+ sender_c->d1->next_timeout = ossl_time_subtract(ossl_time_now(),
+ ossl_seconds2time(1));
+ if (!TEST_true(SSL_handle_events(sender)))
+ goto end;
+
+ /*
+ * The receiver has already moved to the next read epoch and discarded
+ * the old one, so it cannot authenticate this retransmission and never
+ * produces a replacement ACK. This is the assertion that must flip once
+ * the previous read epoch is retained: a fresh ACK should appear here.
+ */
+ ret = SSL_read(receiver, &buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(receiver, ret), SSL_ERROR_WANT_READ)
+ || !TEST_size_t_gt(BIO_ctrl_pending(SSL_get_rbio(sender)), 0))
+ goto end;
+
+ /* The sender picks up the replacement ACK and completes. */
+ ret = SSL_read(sender, &buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(sender, ret), SSL_ERROR_WANT_READ)
+ || !TEST_int_eq(SSL_get_state(sender), TLS_ST_OK)
+ || !TEST_size_t_eq(pqueue_size(&sender_c->d1->sent_messages), 0)
+ || !TEST_true(ossl_time_is_zero(sender_c->d1->next_timeout)))
+ goto end;
+
+ /* Application data flows both ways. */
+ if (!TEST_int_eq(SSL_write(sender, "x", 1), 1)
+ || !TEST_int_eq(SSL_read(receiver, &buf, 1), 1)
+ || !TEST_uchar_eq(buf, 'x')
+ || !TEST_int_eq(SSL_write(receiver, "y", 1), 1)
+ || !TEST_int_eq(SSL_read(sender, &buf, 1), 1)
+ || !TEST_uchar_eq(buf, 'y'))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+/*
+ * SSL_free_buffers() must also release the retained previous-epoch read
+ * layer's buffer, not just the active layer's. dtls_get_more_records()
+ * never uses the retained layer's own read buffer after retention -- it
+ * reuses the active layer's packet buffer to authenticate a
+ * previous-epoch record -- so leaving it allocated after SSL_free_buffers()
+ * reports success is a pure leak until the whole layer chain is torn down.
+ */
+static int test_dtls13_prev_epoch_rl_buffer_freed(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ OSSL_RECORD_LAYER *rrl;
+ unsigned char buf;
+ int 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(create_ssl_objects(sctx, cctx, &server, &client,
+ NULL, NULL))
+ || !TEST_true(create_ssl_connection(server, client, SSL_ERROR_NONE)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+ rrl = sc->rlayer.rrl;
+
+ /* The server's epoch-2 read layer must have been retained. */
+ if (!TEST_ptr(rrl->prev_epoch_rl))
+ goto end;
+
+ /* Exchange and consume application data both ways. */
+ if (!TEST_int_eq(SSL_write(client, "c", 1), 1)
+ || !TEST_int_eq(SSL_read(server, &buf, 1), 1)
+ || !TEST_uchar_eq(buf, 'c')
+ || !TEST_int_eq(SSL_write(server, "s", 1), 1)
+ || !TEST_int_eq(SSL_read(client, &buf, 1), 1)
+ || !TEST_uchar_eq(buf, 's'))
+ goto end;
+
+ if (!TEST_true(SSL_free_buffers(server)))
+ goto end;
+
+ if (!TEST_ptr_null(rrl->rbuf.buf))
+ goto end;
+
+ /*
+ * Without releasing the retained previous-epoch layer's buffer in
+ * dtls_set_prev_epoch_rl(), this would stay allocated even though
+ * SSL_free_buffers() reported success above.
+ */
+ if (!TEST_ptr_null(rrl->prev_epoch_rl->rbuf.buf))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+/*
+ * Epoch 2 is always the fixed DTLS 1.3 handshake epoch: no compliant peer
+ * ever sends application data there. A record that only authenticates
+ * because of that epoch's retained keys must never be delivered as
+ * application data -- but a retained *application* epoch (3+, from
+ * KeyUpdate recovery) must be left alone, since it can legitimately carry
+ * reordered application traffic.
+ */
+static int test_dtls_prev_epoch_allows_type(void)
+{
+ OSSL_RECORD_LAYER rl;
+ int testresult = 0;
+
+ memset(&rl, 0, sizeof(rl));
+
+ rl.epoch = 2;
+ if (!TEST_false(dtls_prev_epoch_allows_type(&rl, SSL3_RT_APPLICATION_DATA))
+ || !TEST_true(dtls_prev_epoch_allows_type(&rl, SSL3_RT_HANDSHAKE)))
+ goto end;
+
+ rl.epoch = 3;
+ if (!TEST_true(dtls_prev_epoch_allows_type(&rl, SSL3_RT_APPLICATION_DATA)))
+ goto end;
+
+ testresult = 1;
+end:
+ return testresult;
+}
+
+/*
+ * dtls_record_from_retained_epoch() is what statem_dtls.c's dispatch and
+ * discard/buffer decisions OR into their existing conditions to recognize a
+ * record that only authenticated via the retained previous read epoch, so
+ * it never gets treated as content that genuinely arrived at the currently
+ * active epoch.
+ */
+static int test_dtls_record_from_retained_epoch(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ int testresult = 0;
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, dtlsv1_3_server_method(),
+ dtlsv1_3_client_method(), 0, 0, &sctx, &cctx, cert, privkey))
+ || !TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
+ NULL, NULL)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+
+ sc->rlayer.d->r_conn_epoch = 3;
+
+ /* Record actually arrived at the currently active epoch. */
+ sc->s3.tmp.record_epoch = 3;
+ if (!TEST_false(dtls_record_from_retained_epoch(sc)))
+ goto end;
+
+ /* Record only authenticated via the retained previous epoch. */
+ sc->s3.tmp.record_epoch = 2;
+ if (!TEST_true(dtls_record_from_retained_epoch(sc)))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+/*
+ * dtls1_process_out_of_seq_message() must never let a record that only
+ * authenticated via a retained previous epoch get buffered for future
+ * reassembly -- which would eventually process it as new content -- and
+ * must only ACK it when it genuinely corresponds to something already
+ * fully processed (seq strictly before the next expected one).
+ *
+ * idx 0: already fully processed (seq < expected). The legitimate
+ * retransmission-recovery case from the earlier review round: ACK,
+ * don't buffer.
+ * idx 1: "expected next" seq (seq == expected). Proves this function is
+ * safe on the exact input dtls_get_reassembled_message() must now
+ * route here instead of treating as fresh (see
+ * test_dtls_record_from_retained_epoch() above for that routing
+ * condition) -- must not be buffered or ACKed.
+ * idx 2: looks like a future message (seq > expected). Must not be
+ * buffered for later processing as new content, and must not be
+ * ACKed either.
+ */
+static int test_dtls13_out_of_seq_retained_epoch(int idx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ struct hm_header_st msg_hdr;
+ int testresult = 0;
+ int expect_ack;
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, dtlsv1_3_server_method(),
+ dtlsv1_3_client_method(), 0, 0, &sctx, &cctx, cert, privkey))
+ || !TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
+ NULL, NULL)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+
+ sc->rlayer.d->r_conn_epoch = 3;
+ sc->s3.tmp.record_epoch = 2;
+ sc->d1->handshake_read_seq = 5;
+
+ memset(&msg_hdr, 0, sizeof(msg_hdr));
+ msg_hdr.type = SSL3_MT_KEY_UPDATE;
+
+ switch (idx) {
+ case 0:
+ msg_hdr.seq = 4;
+ expect_ack = 1;
+ break;
+ case 1:
+ msg_hdr.seq = 5;
+ expect_ack = 0;
+ break;
+ case 2:
+ msg_hdr.seq = 6;
+ expect_ack = 0;
+ break;
+ default:
+ goto end;
+ }
+
+ if (!TEST_int_eq(dtls1_process_out_of_seq_message(sc, &msg_hdr),
+ DTLS1_HM_FRAGMENT_RETRY))
+ goto end;
+
+ /* Never buffered for future reassembly/processing as new content. */
+ if (!TEST_size_t_eq(pqueue_size(&sc->d1->rcvd_messages), 0))
+ goto end;
+
+ if (expect_ack) {
+ if (!TEST_ptr(ossl_list_record_number_head(&sc->d1->ack_rec_num)))
+ goto end;
+ } else if (!TEST_ptr_null(ossl_list_record_number_head(&sc->d1->ack_rec_num))) {
+ goto end;
+ }
+
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+/*
+ * Encrypt a handshake message under a retained (stale) epoch's own real
+ * cipher context and inject it straight into the receiver's rbio, exactly
+ * as tls13_cipher() builds a unified-header DTLS 1.3 record. No separate key
+ * material or network write is needed: the retained read layer already
+ * holds the real traffic keys for that epoch, so running its own cipher
+ * context in the encrypt direction for one record produces ciphertext the
+ * same layer's decrypt path will accept.
+ *
+ * Ported from Mounir Idrassi's inject_previous() in his
+ * repro_prev_epoch_delivery.c reproducer for this issue.
+ */
+static int inject_at_retained_epoch(SSL *receiver, unsigned char inner_type,
+ const unsigned char *body, size_t body_len)
+{
+ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL(receiver);
+ OSSL_RECORD_LAYER *prev = sc->rlayer.rrl->prev_epoch_rl;
+ unsigned char plain[64], cipher[64], header[5], nonce[EVP_MAX_IV_LENGTH];
+ unsigned char seq[SEQ_NUM_SIZE], *pseq = seq;
+ uint64_t sequence, truncated;
+ size_t ivlen, offset, i, pktlen;
+ int outl = 0, finl = 0;
+
+ if (prev == NULL || prev->enc_ctx == NULL || prev->iv == NULL
+ || prev->taglen == 0 || body_len + 1 > sizeof(plain)
+ || body_len + 1 + prev->taglen + 5 > sizeof(cipher))
+ return 0;
+
+ sequence = prev->bitmap.max_seq_num + 1;
+ truncated = sequence & 0xffff;
+ memcpy(plain, body, body_len);
+ plain[body_len] = inner_type;
+
+ l2n8(sequence, pseq);
+ ivlen = (size_t)EVP_CIPHER_CTX_get_iv_length(prev->enc_ctx);
+ if (ivlen < SEQ_NUM_SIZE || ivlen > sizeof(nonce))
+ return 0;
+ offset = ivlen - SEQ_NUM_SIZE;
+ memcpy(nonce, prev->iv, offset);
+ for (i = 0; i < SEQ_NUM_SIZE; i++)
+ nonce[offset + i] = prev->iv[offset + i] ^ seq[i];
+
+ header[0] = (unsigned char)(DTLS13_UNI_HDR_FIX_BITS | DTLS13_UNI_HDR_SEQ_BIT
+ | DTLS13_UNI_HDR_LEN_BIT | (prev->epoch & DTLS13_UNI_HDR_EPOCH_BITS_MASK));
+ header[1] = (unsigned char)(truncated >> 8);
+ header[2] = (unsigned char)truncated;
+ header[3] = (unsigned char)((body_len + 1 + prev->taglen) >> 8);
+ header[4] = (unsigned char)(body_len + 1 + prev->taglen);
+
+ if (EVP_CipherInit_ex(prev->enc_ctx, NULL, NULL, NULL, nonce, 1) <= 0
+ || EVP_CipherUpdate(prev->enc_ctx, NULL, &outl, header, sizeof(header)) <= 0
+ || EVP_CipherUpdate(prev->enc_ctx, cipher + 5, &outl, plain,
+ (int)(body_len + 1))
+ <= 0
+ || EVP_CipherFinal_ex(prev->enc_ctx, cipher + 5 + outl, &finl) <= 0
+ || (size_t)outl + (size_t)finl != body_len + 1
+ || EVP_CIPHER_CTX_ctrl(prev->enc_ctx, EVP_CTRL_AEAD_GET_TAG,
+ (int)prev->taglen, cipher + 5 + body_len + 1)
+ <= 0)
+ return 0;
+
+ memcpy(cipher, header, sizeof(header));
+ pktlen = 5 + body_len + 1 + prev->taglen;
+
+ /*
+ * Mask the 16-bit sequence number exactly as a transmitted record does.
+ * dtls_crypt_sequence_number() is reversible, so the receiver recovers
+ * the value selected above.
+ */
+ if (prev->sn_enc_ctx != NULL
+ && !dtls_crypt_sequence_number(prev->sn_enc_ctx, cipher + 1, 2,
+ cipher + 5))
+ return 0;
+
+ return mempacket_test_inject(SSL_get_rbio(receiver), (const char *)cipher,
+ (int)pktlen, -1, INJECT_PACKET_IGNORE_REC_SEQ)
+ == (int)pktlen;
+}
+
+/*
+ * A record that authenticates only via the retained epoch-2
+ * read layer, but whose handshake sequence number matches exactly
+ * what the server is still waiting for, must not be treated as fresh
+ * content: dtls_get_reassembled_message() must still route it to
+ * dtls1_process_out_of_seq_message() via dtls_record_from_retained_epoch(),
+ * even though the plain "seq != expected" check alone would not catch it.
+ *
+ * Unlike test_dtls13_out_of_seq_retained_epoch() above, which calls
+ * dtls1_process_out_of_seq_message() directly, this goes through the real
+ * receive path, so it actually exercises the routing decision
+ * in dtls_get_reassembled_message() instead of assuming it already
+ * happened.
+ */
+static int test_dtls13_retained_epoch_seq_match(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ OSSL_RECORD_LAYER *active, *prev;
+ unsigned char message[DTLS1_HM_HEADER_LENGTH + 1];
+ unsigned char buf;
+ unsigned short expected;
+ uint64_t epoch_before;
+ 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))
+ || !TEST_true(create_ssl_connection(server, client, SSL_ERROR_NONE)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+ active = sc->rlayer.rrl;
+ prev = active->prev_epoch_rl;
+
+ /* The server's epoch-2 (Finished-recovery) read layer must be retained. */
+ if (!TEST_ptr(prev) || !TEST_uint64_t_eq(prev->epoch, 2))
+ goto end;
+
+ epoch_before = active->epoch;
+ expected = sc->d1->handshake_read_seq;
+
+ /* A KeyUpdate claiming exactly the sequence number the server still
+ * expects next. */
+ memset(message, 0, sizeof(message));
+ message[0] = SSL3_MT_KEY_UPDATE;
+ message[3] = 1;
+ message[4] = (unsigned char)(expected >> 8);
+ message[5] = (unsigned char)expected;
+ message[11] = 1;
+ message[DTLS1_HM_HEADER_LENGTH] = SSL_KEY_UPDATE_NOT_REQUESTED;
+
+ if (!TEST_true(inject_at_retained_epoch(server, SSL3_RT_HANDSHAKE,
+ message, sizeof(message))))
+ goto end;
+
+ ret = SSL_read(server, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ /*
+ * Must not have been processed as fresh content: a genuine KeyUpdate
+ * would install a new read epoch, and the server must not have
+ * re-entered handshake processing. Check the connection's *current*
+ * read layer (sc->rlayer.rrl), not "active" -- that pointer was saved
+ * before the read, and a real epoch bump replaces sc->rlayer.rrl with
+ * a new OSSL_RECORD_LAYER while retaining the old one as its
+ * prev_epoch_rl, so active->epoch would still read as unchanged
+ * either way.
+ */
+ if (!TEST_uint64_t_eq(sc->rlayer.rrl->epoch, epoch_before)
+ || !TEST_false(SSL_in_init(server))
+ || !TEST_int_eq(sc->d1->handshake_read_seq, expected))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+/*
+ * Epoch 2 is always the fixed DTLS 1.3 handshake epoch: no compliant peer
+ * ever sends application data there. A record that only authenticates via
+ * those retained keys must never be delivered as application data, unlike a
+ * retained *application* epoch (3+), which can legitimately carry reordered
+ * application traffic -- see test_dtls_prev_epoch_allows_type() above for
+ * that distinction in isolation.
+ */
+static int test_dtls13_retained_epoch_app_data_rejected(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ OSSL_RECORD_LAYER *prev;
+ unsigned char body[1] = { 'P' };
+ unsigned char buf = 0;
+ 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))
+ || !TEST_true(create_ssl_connection(server, client, SSL_ERROR_NONE)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+ prev = sc->rlayer.rrl->prev_epoch_rl;
+
+ /* The server's epoch-2 (Finished-recovery) read layer must be retained. */
+ if (!TEST_ptr(prev) || !TEST_uint64_t_eq(prev->epoch, 2))
+ goto end;
+
+ if (!TEST_true(inject_at_retained_epoch(server, SSL3_RT_APPLICATION_DATA,
+ body, sizeof(body))))
+ goto end;
+
+ /*
+ * The record authenticates, but dtls_prev_epoch_allows_type() must
+ * discard it once decoded rather than deliver it -- it must never reach
+ * here as readable application data.
+ */
+ ret = SSL_read(server, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_uchar_eq(buf, 0))
+ goto end;
+
+ /* Prove it's not just harmlessly stuck. */
+ 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;
+}
+
+/*
+ * An ACK record that only authenticated via the retained epoch-2 read layer
+ * must not be processed beyond the Finished-recovery window: once the
+ * handshake is over, records protected with the retained handshake keys
+ * must not modify the post-handshake retransmission state (d1->sent_messages)
+ * or cancel its retransmission timer.
+ *
+ * The same connection rejects application data authenticated with those
+ * same retained keys (see test_dtls13_retained_epoch_app_data_rejected()
+ * above), but the ACK branch of dtls_get_reassembled_message() returns
+ * before the retained-epoch restriction is applied, dtls1_read_bytes()
+ * records the authenticating epoch for handshake records only, and
+ * dtls_process_ack() removes matching entries from the retransmission queue
+ * without checking which epoch authenticated the ACK. This test fails until
+ * the ACK path enforces the handshake/application protection boundary.
+ */
+static int test_dtls13_retained_epoch_ack_authority(void)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ SSL_CONNECTION *sc;
+ OSSL_RECORD_LAYER *prev;
+ piterator iter;
+ pitem *item;
+ dtls_sent_msg *msg;
+ DTLS1_RECORD_NUMBER *recnum;
+ unsigned char body[2 + 16], discard[2048], buf = 0;
+ uint64_t epoch = 0, seqnum = 0, active_epoch, bitmap_before;
+ size_t off;
+ int ret, dropped, 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))
+ /*
+ * Pin an AEAD whose per-record plaintext length needs no extra
+ * declaration, so inject_at_retained_epoch()'s assumptions hold
+ * regardless of suite priority changes (AES-CCM would need more).
+ */
+ || !TEST_true(SSL_CTX_set_ciphersuites(sctx, "TLS_AES_128_GCM_SHA256"))
+ || !TEST_true(SSL_CTX_set_ciphersuites(cctx, "TLS_AES_128_GCM_SHA256"))
+ || !TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL, NULL))
+ || !TEST_true(create_ssl_connection(server, client, SSL_ERROR_NONE)))
+ goto end;
+ sc = SSL_CONNECTION_FROM_SSL(server);
+ prev = sc->rlayer.rrl->prev_epoch_rl;
+
+ /* The server's epoch-2 (Finished-recovery) read layer must be retained. */
+ if (!TEST_ptr(prev) || !TEST_uint64_t_eq(prev->epoch, 2))
+ goto end;
+
+ /* Let any pending flight ACKs through so the baseline is settled. */
+ ret = SSL_read(server, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 0)
+ || !TEST_true(ossl_time_is_zero(sc->d1->next_timeout)))
+ goto end;
+
+ /* Arm a post-handshake flight: a server KeyUpdate awaiting its ACK. */
+ if (!TEST_true(SSL_key_update(server, SSL_KEY_UPDATE_NOT_REQUESTED)))
+ goto end;
+ ret = SSL_do_handshake(server);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 1)
+ || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout)))
+ goto end;
+
+ /* The client processes it and ACKs; drop that ACK. */
+ ret = SSL_read(client, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+ goto end;
+ dropped = 0;
+ while (BIO_read(SSL_get_rbio(server), discard, sizeof(discard)) > 0)
+ dropped++;
+ if (!TEST_int_gt(dropped, 0)
+ || !TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 1)
+ || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout)))
+ goto end;
+
+ /*
+ * Forge an ACK record at the retained epoch claiming the outstanding
+ * record. The claimed (epoch, sequence) pair is copied from the
+ * server's own retransmission queue; it belongs to the currently active
+ * read epoch, while the record itself only authenticates via epoch 2.
+ */
+ iter = pqueue_iterator(&sc->d1->sent_messages);
+ item = pqueue_next(&iter);
+ if (!TEST_ptr(item))
+ goto end;
+ msg = (dtls_sent_msg *)item->data;
+ recnum = ossl_list_record_number_head(&msg->rec_nums);
+ if (!TEST_ptr(recnum))
+ goto end;
+ epoch = recnum->epoch;
+ seqnum = recnum->seqnum;
+ active_epoch = sc->rlayer.rrl->epoch;
+ if (!TEST_uint64_t_eq(epoch, active_epoch))
+ goto end;
+
+ body[0] = 0;
+ body[1] = 16;
+ for (off = 0; off < 8; off++) {
+ body[2 + off] = (unsigned char)(epoch >> (8 * (7 - off)));
+ body[10 + off] = (unsigned char)(seqnum >> (8 * (7 - off)));
+ }
+ bitmap_before = prev->bitmap.max_seq_num;
+ if (!TEST_true(inject_at_retained_epoch(server, SSL3_RT_ACK, body,
+ sizeof(body))))
+ goto end;
+
+ ret = SSL_read(server, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ /*
+ * The record only authenticated via the retained handshake epoch, so it
+ * must not complete the outstanding application-epoch flight nor stop
+ * its retransmission timer. It did pass the retained layer's own replay
+ * window (only updated after successful decryption) and must not have
+ * moved the active read epoch.
+ */
+ if (!TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 1)
+ || !TEST_false(ossl_time_is_zero(sc->d1->next_timeout))
+ || !TEST_uint64_t_eq(prev->bitmap.max_seq_num, bitmap_before + 1)
+ || !TEST_uint64_t_eq(sc->rlayer.rrl->epoch, active_epoch))
+ goto end;
+
+ /*
+ * The legitimate recovery must still work afterwards: force the
+ * retransmission timer, let the client re-ACK the retransmitted
+ * KeyUpdate, and let the server complete on the replacement ACK.
+ * (The genuine ACK was deliberately dropped above, so the server is
+ * still waiting for it and a bare SSL_write() would first drive the
+ * unfinished handshake and fail with SSL_ERROR_WANT_READ.)
+ */
+ sc->d1->next_timeout = ossl_time_subtract(ossl_time_now(),
+ ossl_seconds2time(1));
+ if (!TEST_int_gt(DTLSv1_handle_timeout(server), 0)
+ || !TEST_size_t_gt(BIO_ctrl_pending(SSL_get_rbio(client)), 0))
+ goto end;
+
+ ret = SSL_read(client, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(client, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ ret = SSL_read(server, &buf, 1);
+ if (!TEST_int_eq(SSL_get_error(server, ret), SSL_ERROR_WANT_READ)
+ || !TEST_size_t_eq(pqueue_size(&sc->d1->sent_messages), 0)
+ || !TEST_true(ossl_time_is_zero(sc->d1->next_timeout)))
+ goto end;
+
+ /* With the flight settled, bidirectional application data flows. */
+ 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 */
int setup_tests(void)
@@ -1528,6 +2341,15 @@ int setup_tests(void)
ADD_ALL_TESTS(test_dtls13_ticket_ack_history_fragmented, 2);
ADD_TEST(test_dtls13_interrupted_retransmit_range);
ADD_TEST(test_dtls13_ack_bitmap_oob);
+ ADD_TEST(test_dtls13_finished_ack_loss_recovers);
+ ADD_ALL_TESTS(test_dtls13_keyupdate_ack_loss_recovers, 2);
+ ADD_TEST(test_dtls13_prev_epoch_rl_buffer_freed);
+ ADD_TEST(test_dtls_prev_epoch_allows_type);
+ ADD_TEST(test_dtls_record_from_retained_epoch);
+ ADD_ALL_TESTS(test_dtls13_out_of_seq_retained_epoch, 3);
+ ADD_TEST(test_dtls13_retained_epoch_seq_match);
+ ADD_TEST(test_dtls13_retained_epoch_app_data_rejected);
+ ADD_TEST(test_dtls13_retained_epoch_ack_authority);
#endif
return 1;
}
diff --git a/test/sslapitest.c b/test/sslapitest.c
index fc43d6af0a..e78ac49e16 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -2418,6 +2418,54 @@ static int test_cleanse_plaintext(void)
return 1;
}
+#if !defined(OSSL_NO_USABLE_DTLS1_3)
+/*
+ * Test that a DTLS 1.3 read epoch bump does not crash when
+ * SSL_MODE_RELEASE_BUFFERS and SSL_OP_CLEANSE_PLAINTEXT are both set: the old
+ * epoch's read buffer may already have been released by
+ * tls_release_record(), and dtls_set_prev_epoch_rl() must not release it
+ * again.
+ */
+static int test_dtls13_release_buffers_cleanse(void)
+{
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL;
+ int testresult = 0;
+
+ if (!TEST_true(create_ssl_ctx_pair(libctx,
+ DTLS_server_method(),
+ DTLS_client_method(),
+ DTLS1_3_VERSION,
+ DTLS1_3_VERSION,
+ &sctx, &cctx, cert,
+ privkey)))
+ goto end;
+
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+ NULL, NULL)))
+ goto end;
+
+ if (!TEST_true(SSL_set_mode(serverssl, SSL_MODE_RELEASE_BUFFERS))
+ || !TEST_true(SSL_set_options(serverssl, SSL_OP_CLEANSE_PLAINTEXT))
+ || !TEST_true(SSL_set_mode(clientssl, SSL_MODE_RELEASE_BUFFERS))
+ || !TEST_true(SSL_set_options(clientssl, SSL_OP_CLEANSE_PLAINTEXT)))
+ goto end;
+
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+
+ return testresult;
+}
+#endif
+
#ifndef OPENSSL_NO_OCSP
static OCSP_RESPONSE *create_ocsp_resp(X509 *ssl_cert, X509 *issuer, int status,
const char *signer_key_files, const char *signer_cert_files)
@@ -18072,6 +18120,9 @@ int setup_tests(void)
#endif
ADD_ALL_TESTS(test_large_app_data, 28);
ADD_TEST(test_cleanse_plaintext);
+#if !defined(OSSL_NO_USABLE_DTLS1_3)
+ ADD_TEST(test_dtls13_release_buffers_cleanse);
+#endif
#ifndef OPENSSL_NO_OCSP
ADD_TEST(test_tlsext_status_type);
#ifndef OSSL_NO_USABLE_TLS1_3