Skip to content
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
144 changes: 144 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,144 @@
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;

boolean acknowledged = false;

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

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);
}
}

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