Skip to content

Commit dda1d91

Browse files
committed
Add DTLS 1.3 unified header codec and sequence number reconstruction (RFC 9147 4.2.2), relates to github #1468.
1 parent db62603 commit dda1d91

4 files changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package org.bouncycastle.tls;
2+
3+
/**
4+
* RFC 9147 4. The DTLS 1.3 unified header for DTLSCiphertext records.
5+
* <pre>
6+
* 0 1 2 3 4 5 6 7
7+
* +-+-+-+-+-+-+-+-+
8+
* |0|0|1|C|S|L|E E|
9+
* +-+-+-+-+-+-+-+-+
10+
* </pre>
11+
* C: connection ID present, S: 16-bit (1) or 8-bit (0) sequence number, L: length present, EE: low two bits of
12+
* the epoch. Records written by this implementation always use the full form (S = 1, L = 1).
13+
*/
14+
class DTLS13UnifiedHeader
15+
{
16+
static final int FIXED_BITS = 0x20;
17+
static final int FIXED_BITS_MASK = 0xE0;
18+
static final int FLAG_CID = 0x10;
19+
static final int FLAG_SEQ16 = 0x08;
20+
static final int FLAG_LENGTH = 0x04;
21+
static final int EPOCH_BITS_MASK = 0x03;
22+
23+
/** RFC 9147 4.2.3. Record number encryption needs at least 16 bytes of ciphertext. */
24+
static final int MIN_CIPHERTEXT_LENGTH = 16;
25+
26+
private static final long MAX_SEQUENCE_NUMBER = (1L << 48) - 1;
27+
28+
static boolean isCiphertextRecord(int firstByte)
29+
{
30+
return (firstByte & FIXED_BITS_MASK) == FIXED_BITS;
31+
}
32+
33+
static boolean hasConnectionID(int firstByte)
34+
{
35+
return (firstByte & FLAG_CID) != 0;
36+
}
37+
38+
static boolean hasSeq16(int firstByte)
39+
{
40+
return (firstByte & FLAG_SEQ16) != 0;
41+
}
42+
43+
static boolean hasLength(int firstByte)
44+
{
45+
return (firstByte & FLAG_LENGTH) != 0;
46+
}
47+
48+
static boolean matchesEpoch(int firstByte, int epoch)
49+
{
50+
return (firstByte & EPOCH_BITS_MASK) == (epoch & EPOCH_BITS_MASK);
51+
}
52+
53+
static int getSequenceNumberLength(int firstByte)
54+
{
55+
return hasSeq16(firstByte) ? 2 : 1;
56+
}
57+
58+
static int getHeaderLength(int firstByte, int connectionIDLength)
59+
{
60+
return 1 + connectionIDLength + getSequenceNumberLength(firstByte) + (hasLength(firstByte) ? 2 : 0);
61+
}
62+
63+
static int getWriteHeaderLength(int connectionIDLength)
64+
{
65+
return 1 + connectionIDLength + 2 + 2;
66+
}
67+
68+
/**
69+
* The smallest conforming header a peer may send per RFC 9147 4: first byte, connection ID, and an 8-bit
70+
* sequence number, with no length field (S = 0, L = 0). Since a peer is free to use that compact form,
71+
* this is what the receive limit must budget for; assuming our own (full) write form would under-report the
72+
* plaintext limit and reject legal records.
73+
*
74+
* @return the minimum length of a header that may be received.
75+
*/
76+
static int getMinReadHeaderLength(int connectionIDLength)
77+
{
78+
return 1 + connectionIDLength + 1;
79+
}
80+
81+
/**
82+
* Write a full-form header (16-bit sequence number, length present). The length field is left zero for the
83+
* cipher to fill in once the ciphertext length is known.
84+
*
85+
* @return the header length.
86+
*/
87+
static int writeHeader(int epoch, long sequenceNumber, byte[] connectionID, byte[] buf, int off)
88+
{
89+
int cidLength = null == connectionID ? 0 : connectionID.length;
90+
91+
int firstByte = FIXED_BITS | FLAG_SEQ16 | FLAG_LENGTH | (epoch & EPOCH_BITS_MASK);
92+
if (cidLength > 0)
93+
{
94+
firstByte |= FLAG_CID;
95+
}
96+
97+
int pos = off;
98+
buf[pos++] = (byte)firstByte;
99+
if (cidLength > 0)
100+
{
101+
System.arraycopy(connectionID, 0, buf, pos, cidLength);
102+
pos += cidLength;
103+
}
104+
TlsUtils.writeUint16((int)(sequenceNumber & 0xFFFFL), buf, pos);
105+
pos += 2;
106+
TlsUtils.writeUint16(0, buf, pos);
107+
pos += 2;
108+
return pos - off;
109+
}
110+
111+
/**
112+
* RFC 9147 4.2.2. Reconstruct the full sequence number as the value numerically closest to 'expected' (one
113+
* plus the highest successfully deprotected sequence number) whose low 'seqBitCount' bits equal 'seqBits'.
114+
*/
115+
static long reconstructSequenceNumber(long expected, int seqBits, int seqBitCount)
116+
{
117+
long modulus = 1L << seqBitCount;
118+
long lowMask = modulus - 1;
119+
120+
long candidate = (expected & ~lowMask) | (seqBits & lowMask);
121+
long best = candidate;
122+
long bestDistance = distance(candidate, expected);
123+
124+
long lower = candidate - modulus;
125+
if (lower >= 0)
126+
{
127+
long d = distance(lower, expected);
128+
if (d < bestDistance)
129+
{
130+
best = lower;
131+
bestDistance = d;
132+
}
133+
}
134+
135+
long upper = candidate + modulus;
136+
if (upper <= MAX_SEQUENCE_NUMBER)
137+
{
138+
long d = distance(upper, expected);
139+
if (d < bestDistance)
140+
{
141+
best = upper;
142+
}
143+
}
144+
145+
return best;
146+
}
147+
148+
private static long distance(long a, long b)
149+
{
150+
return a > b ? a - b : b - a;
151+
}
152+
}

‎tls/src/main/java/org/bouncycastle/tls/DTLSReplayWindow.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ boolean reportAuthenticated(long seq)
8282
}
8383
}
8484

85+
/**
86+
* @return the highest sequence number reported authenticated so far, or -1 if none.
87+
*/
88+
long getLatestConfirmedSeq()
89+
{
90+
return latestConfirmedSeq;
91+
}
92+
8593
void reset(long seq)
8694
{
8795
if ((seq & VALID_SEQ_MASK) != seq)

‎tls/src/test/java/org/bouncycastle/tls/AllTests.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static Test suite()
2323
suite.addTestSuite(AbstractTlsServerResetTest.class);
2424
suite.addTestSuite(Add13CertificateStatusTest.class);
2525
suite.addTestSuite(CheckTlsFeaturesExtensionTest.class);
26+
suite.addTestSuite(DTLS13UnifiedHeaderTest.class);
2627
suite.addTestSuite(DTLSReassemblerTest.class);
2728
suite.addTestSuite(DTLSRecordNumberMaskTest.class);
2829
suite.addTestSuite(SpreadCertificateStatusTest.class);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.bouncycastle.tls;
2+
3+
import junit.framework.TestCase;
4+
5+
/**
6+
* RFC 9147 4 unified header (0b001CSLEE) and 4.2.2 sequence number reconstruction.
7+
*/
8+
public class DTLS13UnifiedHeaderTest
9+
extends TestCase
10+
{
11+
public void testFixedBits()
12+
{
13+
assertTrue(DTLS13UnifiedHeader.isCiphertextRecord(0x20));
14+
assertTrue(DTLS13UnifiedHeader.isCiphertextRecord(0x3F));
15+
assertFalse(DTLS13UnifiedHeader.isCiphertextRecord(ContentType.handshake));
16+
assertFalse(DTLS13UnifiedHeader.isCiphertextRecord(ContentType.alert));
17+
assertFalse(DTLS13UnifiedHeader.isCiphertextRecord(ContentType.ack));
18+
assertFalse(DTLS13UnifiedHeader.isCiphertextRecord(0x40));
19+
}
20+
21+
public void testHeaderLengths()
22+
{
23+
assertEquals(2, DTLS13UnifiedHeader.getHeaderLength(0x20, 0)); // minimal: 8-bit seq, no length
24+
assertEquals(3, DTLS13UnifiedHeader.getHeaderLength(0x28, 0)); // 16-bit seq
25+
assertEquals(4, DTLS13UnifiedHeader.getHeaderLength(0x24, 0)); // 8-bit seq + length
26+
assertEquals(5, DTLS13UnifiedHeader.getHeaderLength(0x2C, 0)); // full
27+
assertEquals(9, DTLS13UnifiedHeader.getHeaderLength(0x3C, 4)); // full + 4-byte CID
28+
assertEquals(5, DTLS13UnifiedHeader.getWriteHeaderLength(0));
29+
assertEquals(9, DTLS13UnifiedHeader.getWriteHeaderLength(4));
30+
// the receive limit must budget for the smallest header a peer may send, not for our write form
31+
assertEquals(2, DTLS13UnifiedHeader.getMinReadHeaderLength(0));
32+
assertEquals(6, DTLS13UnifiedHeader.getMinReadHeaderLength(4));
33+
assertEquals(DTLS13UnifiedHeader.getHeaderLength(0x20, 0), DTLS13UnifiedHeader.getMinReadHeaderLength(0));
34+
assertEquals(DTLS13UnifiedHeader.getHeaderLength(0x30, 4), DTLS13UnifiedHeader.getMinReadHeaderLength(4));
35+
assertEquals(1, DTLS13UnifiedHeader.getSequenceNumberLength(0x20));
36+
assertEquals(2, DTLS13UnifiedHeader.getSequenceNumberLength(0x28));
37+
}
38+
39+
public void testWriteHeaderFullForm()
40+
{
41+
byte[] buf = new byte[5];
42+
int len = DTLS13UnifiedHeader.writeHeader(3, 0x123456L, null, buf, 0);
43+
assertEquals(5, len);
44+
assertEquals(0x2F, buf[0] & 0xFF); // 001 0 1 1 11
45+
assertEquals(0x3456, TlsUtils.readUint16(buf, 1)); // low 16 bits of seq
46+
assertEquals(0, TlsUtils.readUint16(buf, 3)); // length left for the cipher
47+
assertTrue(DTLS13UnifiedHeader.hasSeq16(buf[0] & 0xFF));
48+
assertTrue(DTLS13UnifiedHeader.hasLength(buf[0] & 0xFF));
49+
assertFalse(DTLS13UnifiedHeader.hasConnectionID(buf[0] & 0xFF));
50+
assertTrue(DTLS13UnifiedHeader.matchesEpoch(buf[0] & 0xFF, 3));
51+
assertTrue(DTLS13UnifiedHeader.matchesEpoch(buf[0] & 0xFF, 7));
52+
assertFalse(DTLS13UnifiedHeader.matchesEpoch(buf[0] & 0xFF, 2));
53+
54+
byte[] cid = new byte[]{ 1, 2, 3 };
55+
byte[] buf2 = new byte[8];
56+
assertEquals(8, DTLS13UnifiedHeader.writeHeader(2, 5, cid, buf2, 0));
57+
assertEquals(0x3E, buf2[0] & 0xFF); // C bit set, epoch bits 10
58+
assertEquals(1, buf2[1]);
59+
assertEquals(3, buf2[3]);
60+
assertEquals(5, TlsUtils.readUint16(buf2, 4));
61+
}
62+
63+
public void testReconstructSequenceNumber()
64+
{
65+
// fresh epoch: expected 0
66+
assertEquals(5L, DTLS13UnifiedHeader.reconstructSequenceNumber(0, 5, 8));
67+
// exact match in the current window
68+
assertEquals(300L, DTLS13UnifiedHeader.reconstructSequenceNumber(300, 0x2C, 8));
69+
// wrap forward: expected 0x1FE, bits 0x02 -> 0x202 is closer than 0x102
70+
assertEquals(0x202L, DTLS13UnifiedHeader.reconstructSequenceNumber(0x1FE, 0x02, 8));
71+
// wrap backward: expected 0x203, bits 0xFE -> 0x1FE is closer than 0x2FE
72+
assertEquals(0x1FEL, DTLS13UnifiedHeader.reconstructSequenceNumber(0x203, 0xFE, 8));
73+
// 16-bit variants
74+
assertEquals(0x1FFFEL, DTLS13UnifiedHeader.reconstructSequenceNumber(0x20003, 0xFFFE, 16));
75+
assertEquals(0x20002L, DTLS13UnifiedHeader.reconstructSequenceNumber(0x1FFFE, 0x0002, 16));
76+
// never negative
77+
assertEquals(0xFEL, DTLS13UnifiedHeader.reconstructSequenceNumber(3, 0xFE, 8));
78+
// never above 2^48 - 1
79+
long top = (1L << 48) - 3;
80+
assertEquals(top, DTLS13UnifiedHeader.reconstructSequenceNumber(top, (int)(top & 0xFF), 8));
81+
}
82+
83+
public void testReplayWindowExposesLatestConfirmed()
84+
{
85+
DTLSReplayWindow w = new DTLSReplayWindow();
86+
assertEquals(-1L, w.getLatestConfirmedSeq());
87+
assertTrue(w.reportAuthenticated(10));
88+
assertEquals(10L, w.getLatestConfirmedSeq());
89+
assertFalse(w.reportAuthenticated(4));
90+
assertEquals(10L, w.getLatestConfirmedSeq());
91+
}
92+
}

0 commit comments

Comments
 (0)