Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
7c9e771
Offer DTLS 1.3 from the DTLS client, relates to github #1468.
mondain Sep 12, 2026
a46bc93
Complete the client half of a DTLS 1.3 handshake, relates to github #…
mondain Sep 12, 2026
57c8d20
Hash DTLS 1.3 handshake transcripts without the DTLS message header, …
mondain Sep 12, 2026
ca35039
Fail loudly if a DTLS handshake transcript is read while undecided, r…
mondain Sep 12, 2026
9408d20
Complete the server half of a DTLS 1.3 handshake, relates to github #…
mondain Sep 12, 2026
f9ddbff
Retain the DTLS 1.3 handshake epoch after the handshake completes, re…
mondain Sep 12, 2026
f415e49
Add an end-to-end DTLS 1.3 handshake test, relates to github #1468.
mondain Sep 12, 2026
8c3899d
Bound the DTLS 1.3 epochs retained after the handshake, relates to gi…
mondain Sep 12, 2026
c8ed172
Answer only a retransmission of the peer's final flight with an ACK, …
mondain Sep 12, 2026
af96b84
Test the DTLS 1.3 retransmitted-ACK path with a reordered final fligh…
mondain Sep 12, 2026
b440ece
Re-arm the DTLS 1.3 retained-epoch application data test, relates to …
mondain Sep 12, 2026
3d00eff
Add the DTLS 1.3 HelloRetryRequest and its cookie, relates to github …
mondain Sep 12, 2026
74b9e36
Refuse a DTLS 1.2 HelloVerifyRequest on a DTLS 1.3 handshake, relates…
mondain Sep 12, 2026
58122ec
Support DTLS 1.3 client authentication during the handshake, relates …
mondain Sep 12, 2026
f53fca4
Cover the DTLS 1.3 exporter, use_srtp and version fallback, relates t…
mondain Sep 12, 2026
f15ff71
Restore the DTLS 1.3 no-CertificateRequest and authenticated-flight c…
mondain Sep 12, 2026
696b934
Drop the DTLS 1.3 authenticated flight deterministically and prove it…
mondain Sep 12, 2026
db273e0
Gate the DTLS secure renegotiation notification on the selected versi…
mondain Sep 12, 2026
d92f674
Assert the HelloRetryRequest synthetic transcript bytes directly, rel…
mondain Sep 12, 2026
a7d4c99
Derive the retained handshake epoch and document the DTLS 1.3 epoch r…
mondain Sep 12, 2026
f22a101
Refuse to negotiate DTLS 1.3 behind a HelloVerifyRequest front end, r…
mondain Sep 12, 2026
e2098c6
Leave generate13EncryptedExtensions without an unused state parameter…
mondain Sep 12, 2026
49d7433
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