Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5e8aadb
Add the DTLS 1.3 ack content type (RFC 9147 7.1), relates to github #…
mondain Sep 11, 2026
92a868c
Add DTLS 1.3 record number encryption mask primitive for AES and ChaC…
mondain Sep 11, 2026
db62603
Add DTLS 1.3 record protection to TlsAEADCipher via a new TlsDTLS13Ci…
mondain Sep 11, 2026
dda1d91
Add DTLS 1.3 unified header codec and sequence number reconstruction …
mondain Sep 11, 2026
d5a8bd2
Add DTLS 1.3 record layer send/receive paths and epoch switching (RFC…
mondain Sep 11, 2026
405626e
Use the "dtls13" HKDF-Expand-Label prefix for the DTLS 1.3 key schedu…
JonathanLennox Sep 14, 2026
5643cc6
Add the DTLS 1.3 reliable handshake and pack handshake flights into d…
mondain Sep 12, 2026
97c5666
Refuse to send a DTLS handshake message whose message_seq would wrap …
mondain Sep 20, 2026
0422371
Offer DTLS 1.3 from the DTLS client, relates to github #1468.
mondain Sep 12, 2026
2c8201a
Complete the client half of a DTLS 1.3 handshake, relates to github #…
mondain Sep 12, 2026
aef782f
Hash DTLS 1.3 handshake transcripts without the DTLS message header, …
mondain Sep 12, 2026
9ae9157
Fail loudly if a DTLS handshake transcript is read while undecided, r…
mondain Sep 12, 2026
a31400a
Complete the server half of a DTLS 1.3 handshake, relates to github #…
mondain Sep 12, 2026
c559800
Retain the DTLS 1.3 handshake epoch after the handshake completes, re…
mondain Sep 12, 2026
67c687d
Add an end-to-end DTLS 1.3 handshake test, relates to github #1468.
mondain Sep 12, 2026
79a6111
Bound the DTLS 1.3 epochs retained after the handshake, relates to gi…
mondain Sep 12, 2026
c49456e
Answer only a retransmission of the peer's final flight with an ACK, …
mondain Sep 12, 2026
c6b3583
Test the DTLS 1.3 retransmitted-ACK path with a reordered final fligh…
mondain Sep 12, 2026
2c9600a
Re-arm the DTLS 1.3 retained-epoch application data test, relates to …
mondain Sep 12, 2026
8135076
Add the DTLS 1.3 HelloRetryRequest and its cookie, relates to github …
mondain Sep 12, 2026
45d5c71
Refuse a DTLS 1.2 HelloVerifyRequest on a DTLS 1.3 handshake, relates…
mondain Sep 12, 2026
0939f56
Support DTLS 1.3 client authentication during the handshake, relates …
mondain Sep 12, 2026
13a2b77
Cover the DTLS 1.3 exporter, use_srtp and version fallback, relates t…
mondain Sep 12, 2026
7c7c568
Restore the DTLS 1.3 no-CertificateRequest and authenticated-flight c…
mondain Sep 12, 2026
b35d4b0
Drop the DTLS 1.3 authenticated flight deterministically and prove it…
mondain Sep 12, 2026
9148c41
Gate the DTLS secure renegotiation notification on the selected versi…
mondain Sep 12, 2026
90d77f7
Assert the HelloRetryRequest synthetic transcript bytes directly, rel…
mondain Sep 12, 2026
9b8d088
Derive the retained handshake epoch and document the DTLS 1.3 epoch r…
mondain Sep 12, 2026
c4418f4
Refuse to negotiate DTLS 1.3 behind a HelloVerifyRequest front end, r…
mondain Sep 12, 2026
4ce8020
Leave generate13EncryptedExtensions without an unused state parameter…
mondain Sep 12, 2026
d748767
Assert the server's own refusal and the second ClientHello's message_…
mondain Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions tls/src/main/java/org/bouncycastle/tls/AbstractTlsContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,9 @@ else if (!TlsUtils.isValidUint16(context.length))
TlsHash exporterHash = getCrypto().createHash(cryptoHashAlgorithm);
byte[] emptyTranscriptHash = exporterHash.calculateHash();

TlsSecret exporterSecret = TlsUtils.deriveSecret(getSecurityParametersConnection(), secret, asciiLabel,
emptyTranscriptHash);
SecurityParameters sp = getSecurityParametersConnection();

TlsSecret exporterSecret = TlsUtils.deriveSecret(sp, secret, asciiLabel, emptyTranscriptHash);

byte[] exporterContext = emptyTranscriptHash;
if (context.length > 0)
Expand All @@ -299,8 +300,12 @@ else if (!TlsUtils.isValidUint16(context.length))
exporterContext = exporterHash.calculateHash();
}

// RFC 9147 5.9. DTLS 1.3 derives with the "dtls13" label prefix rather than TLS 1.3's "tls13 ".
boolean isDTLS = sp.getNegotiatedVersion().isDTLS();

return TlsCryptoUtils
.hkdfExpandLabel(exporterSecret, cryptoHashAlgorithm, "exporter", exporterContext, length).extract();
.hkdfExpandLabel(exporterSecret, cryptoHashAlgorithm, "exporter", exporterContext, length, isDTLS)
.extract();
}
catch (IOException e)
{
Expand Down
6 changes: 5 additions & 1 deletion tls/src/main/java/org/bouncycastle/tls/ContentType.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class ContentType
public static final short application_data = 23;
public static final short heartbeat = 24;
public static final short tls12_cid = 25;
/** RFC 9147 7.1 */
public static final short ack = 26;

public static String getName(short contentType)
{
Expand All @@ -27,7 +29,9 @@ public static String getName(short contentType)
case heartbeat:
return "heartbeat";
case tls12_cid:
return "tls12_cid";
return "tls12_cid";
case ack:
return "ack";
default:
return "UNKNOWN";
}
Expand Down
162 changes: 162 additions & 0 deletions tls/src/main/java/org/bouncycastle/tls/DTLS13FlightTracker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package org.bouncycastle.tls;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
* RFC 9147 7.2. Tracks which record carried which handshake fragment of the current outbound flight, so
* that an ACK retires exactly the fragments it covers and a retransmission resends only what is left.
* <p>
* A fragment may be registered more than once, under a different record number each time it is sent. It
* is acknowledged as soon as any one of those records is acknowledged.
* </p>
*/
class DTLS13FlightTracker
{
/** One handshake fragment of the current outbound flight. */
static final class Fragment
{
private final int messageSeq;
private final int fragmentOffset;
private final int fragmentLength;

/*
* RFC 9147 5.8.1. The epoch the fragment was first sent at, which is the epoch it must be retransmitted
* at: a DTLS 1.3 flight straddles an epoch change, and once the handshake completes the write epoch has
* moved on past the keys a peer that is still retransmitting can read. Negative until a record number
* is registered, which means nothing was actually written.
*/
private int epoch = -1;

boolean acknowledged = false;

Fragment(int messageSeq, int fragmentOffset, int fragmentLength)
{
this.messageSeq = messageSeq;
this.fragmentOffset = fragmentOffset;
this.fragmentLength = fragmentLength;
}

int getEpoch()
{
return epoch;
}

int getMessageSeq()
{
return messageSeq;
}

int getFragmentOffset()
{
return fragmentOffset;
}

int getFragmentLength()
{
return fragmentLength;
}

private String key()
{
return messageSeq + ":" + fragmentOffset + ":" + fragmentLength;
}
}

// record number -> Fragment
private Hashtable carriers = new Hashtable();
// fragment key -> Fragment, so the same fragment sent twice is one entry
private Hashtable fragments = new Hashtable();
// fragments in registration order, for deterministic retransmission
private Vector order = new Vector();

void reset()
{
carriers = new Hashtable();
fragments = new Hashtable();
order = new Vector();
}

void register(DTLSRecordNumber recordNumber, int messageSeq, int fragmentOffset, int fragmentLength)
{
Fragment fragment = new Fragment(messageSeq, fragmentOffset, fragmentLength);
String key = fragment.key();

Fragment existing = (Fragment)fragments.get(key);
if (null == existing)
{
fragments.put(key, fragment);
order.addElement(fragment);
existing = fragment;
}

if (null != recordNumber)
{
carriers.put(recordNumber, existing);

if (existing.epoch < 0)
{
existing.epoch = (int)recordNumber.getEpoch();
}
}
}

void acknowledge(Vector recordNumbers)
{
for (int i = 0; i < recordNumbers.size(); ++i)
{
Fragment fragment = (Fragment)carriers.get(recordNumbers.elementAt(i));
if (null != fragment)
{
fragment.acknowledged = true;
}
}
}

boolean isEmpty()
{
return order.isEmpty();
}

/**
* @return true if fragments were registered and every one of them has been acknowledged.
*/
boolean isComplete()
{
if (order.isEmpty())
{
return false;
}

Enumeration e = order.elements();
while (e.hasMoreElements())
{
if (!((Fragment)e.nextElement()).acknowledged)
{
return false;
}
}
return true;
}

/**
* @return the fragments not yet acknowledged, in the order they were first registered.
*/
Vector getOutstanding()
{
Vector outstanding = new Vector();

Enumeration e = order.elements();
while (e.hasMoreElements())
{
Fragment fragment = (Fragment)e.nextElement();
if (!fragment.acknowledged)
{
outstanding.addElement(fragment);
}
}

return outstanding;
}
}
152 changes: 152 additions & 0 deletions tls/src/main/java/org/bouncycastle/tls/DTLS13UnifiedHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package org.bouncycastle.tls;

/**
* RFC 9147 4. The DTLS 1.3 unified header for DTLSCiphertext records.
* <pre>
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |0|0|1|C|S|L|E E|
* +-+-+-+-+-+-+-+-+
* </pre>
* C: connection ID present, S: 16-bit (1) or 8-bit (0) sequence number, L: length present, EE: low two bits of
* the epoch. Records written by this implementation always use the full form (S = 1, L = 1).
*/
class DTLS13UnifiedHeader
{
static final int FIXED_BITS = 0x20;
static final int FIXED_BITS_MASK = 0xE0;
static final int FLAG_CID = 0x10;
static final int FLAG_SEQ16 = 0x08;
static final int FLAG_LENGTH = 0x04;
static final int EPOCH_BITS_MASK = 0x03;

/** RFC 9147 4.2.3. Record number encryption needs at least 16 bytes of ciphertext. */
static final int MIN_CIPHERTEXT_LENGTH = 16;

private static final long MAX_SEQUENCE_NUMBER = (1L << 48) - 1;

static boolean isCiphertextRecord(int firstByte)
{
return (firstByte & FIXED_BITS_MASK) == FIXED_BITS;
}

static boolean hasConnectionID(int firstByte)
{
return (firstByte & FLAG_CID) != 0;
}

static boolean hasSeq16(int firstByte)
{
return (firstByte & FLAG_SEQ16) != 0;
}

static boolean hasLength(int firstByte)
{
return (firstByte & FLAG_LENGTH) != 0;
}

static boolean matchesEpoch(int firstByte, int epoch)
{
return (firstByte & EPOCH_BITS_MASK) == (epoch & EPOCH_BITS_MASK);
}

static int getSequenceNumberLength(int firstByte)
{
return hasSeq16(firstByte) ? 2 : 1;
}

static int getHeaderLength(int firstByte, int connectionIDLength)
{
return 1 + connectionIDLength + getSequenceNumberLength(firstByte) + (hasLength(firstByte) ? 2 : 0);
}

static int getWriteHeaderLength(int connectionIDLength)
{
return 1 + connectionIDLength + 2 + 2;
}

/**
* The smallest conforming header a peer may send per RFC 9147 4: first byte, connection ID, and an 8-bit
* sequence number, with no length field (S = 0, L = 0). Since a peer is free to use that compact form,
* this is what the receive limit must budget for; assuming our own (full) write form would under-report the
* plaintext limit and reject legal records.
*
* @return the minimum length of a header that may be received.
*/
static int getMinReadHeaderLength(int connectionIDLength)
{
return 1 + connectionIDLength + 1;
}

/**
* Write a full-form header (16-bit sequence number, length present). The length field is left zero for the
* cipher to fill in once the ciphertext length is known.
*
* @return the header length.
*/
static int writeHeader(int epoch, long sequenceNumber, byte[] connectionID, byte[] buf, int off)
{
int cidLength = null == connectionID ? 0 : connectionID.length;

int firstByte = FIXED_BITS | FLAG_SEQ16 | FLAG_LENGTH | (epoch & EPOCH_BITS_MASK);
if (cidLength > 0)
{
firstByte |= FLAG_CID;
}

int pos = off;
buf[pos++] = (byte)firstByte;
if (cidLength > 0)
{
System.arraycopy(connectionID, 0, buf, pos, cidLength);
pos += cidLength;
}
TlsUtils.writeUint16((int)(sequenceNumber & 0xFFFFL), buf, pos);
pos += 2;
TlsUtils.writeUint16(0, buf, pos);
pos += 2;
return pos - off;
}

/**
* RFC 9147 4.2.2. Reconstruct the full sequence number as the value numerically closest to 'expected' (one
* plus the highest successfully deprotected sequence number) whose low 'seqBitCount' bits equal 'seqBits'.
*/
static long reconstructSequenceNumber(long expected, int seqBits, int seqBitCount)
{
long modulus = 1L << seqBitCount;
long lowMask = modulus - 1;

long candidate = (expected & ~lowMask) | (seqBits & lowMask);
long best = candidate;
long bestDistance = distance(candidate, expected);

long lower = candidate - modulus;
if (lower >= 0)
{
long d = distance(lower, expected);
if (d < bestDistance)
{
best = lower;
bestDistance = d;
}
}

long upper = candidate + modulus;
if (upper <= MAX_SEQUENCE_NUMBER)
{
long d = distance(upper, expected);
if (d < bestDistance)
{
best = upper;
}
}

return best;
}

private static long distance(long a, long b)
{
return a > b ? a - b : b - a;
}
}
Loading