Skip to content

Commit 6b2dc75

Browse files
committed
Implement DTLS 1.3 record layer encoding
1 parent 1031af5 commit 6b2dc75

7 files changed

Lines changed: 532 additions & 1 deletion

File tree

pkg/protocol/recordlayer/errors.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ var (
2828
errInvalidContentType = &protocol.TemporaryError{
2929
Err: errors.New("invalid content type"), //nolint:err113
3030
}
31+
errInvalidEpoch = &protocol.InternalError{
32+
Err: errors.New("invalid epoch"), //nolint:err113
33+
}
3134
errCIDTooBig = &protocol.InternalError{
3235
Err: errors.New("connection ID size is too big"), //nolint:err113
3336
}

pkg/protocol/recordlayer/header.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,16 @@ func (h *Header) Unmarshal(data []byte) error {
5656
return errBufferTooSmall
5757
}
5858
h.ContentType = protocol.ContentType(data[0])
59+
headerSize := FixedHeaderSize
5960
if h.ContentType == protocol.ContentTypeConnectionID {
6061
// If a CID was expected the ConnectionID should have been initialized.
6162
if len(data) < FixedHeaderSize+len(h.ConnectionID) {
6263
return errBufferTooSmall
6364
}
6465
h.ConnectionID = data[11 : 11+len(h.ConnectionID)]
66+
headerSize += len(h.ConnectionID)
67+
} else {
68+
h.ConnectionID = nil
6569
}
6670

6771
h.Version.Major = data[1]
@@ -72,6 +76,7 @@ func (h *Header) Unmarshal(data []byte) error {
7276
seqCopy := make([]byte, 8)
7377
copy(seqCopy[2:], data[5:11])
7478
h.SequenceNumber = binary.BigEndian.Uint64(seqCopy)
79+
h.ContentLen = binary.BigEndian.Uint16(data[headerSize-2:])
7580

7681
if !h.Version.Equal(protocol.Version1_0) && !h.Version.Equal(protocol.Version1_2) {
7782
return errUnsupportedProtocolVersion

pkg/protocol/recordlayer/inner_plaintext.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func (p *InnerPlaintext) Marshal() ([]byte, error) {
2929

3030
// Unmarshal populates a DTLS InnerPlaintext from binary.
3131
func (p *InnerPlaintext) Unmarshal(data []byte) error {
32+
if len(data) == 0 {
33+
return errBufferTooSmall
34+
}
35+
3236
// Process in reverse
3337
i := len(data) - 1
3438
for i >= 0 {
@@ -39,7 +43,7 @@ func (p *InnerPlaintext) Unmarshal(data []byte) error {
3943
}
4044
i--
4145
}
42-
if i == 0 {
46+
if i < 0 {
4347
return errBufferTooSmall
4448
}
4549
p.RealType = protocol.ContentType(data[i])
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package recordlayer
5+
6+
import (
7+
"testing"
8+
9+
"github.com/pion/dtls/v3/pkg/protocol"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestInnerPlaintextRoundTrip(t *testing.T) {
14+
inner := &InnerPlaintext{
15+
Content: []byte{0x01, 0x02},
16+
RealType: protocol.ContentTypeApplicationData,
17+
Zeros: 2,
18+
}
19+
20+
raw, err := inner.Marshal()
21+
require.NoError(t, err)
22+
require.Equal(t, []byte{0x01, 0x02, 0x17, 0x00, 0x00}, raw)
23+
24+
var roundTrip InnerPlaintext
25+
require.NoError(t, roundTrip.Unmarshal(raw))
26+
require.Equal(t, inner.Content, roundTrip.Content)
27+
require.Equal(t, inner.RealType, roundTrip.RealType)
28+
require.Equal(t, inner.Zeros, roundTrip.Zeros)
29+
}
30+
31+
func TestInnerPlaintextAllowsEmptyContent(t *testing.T) {
32+
var inner InnerPlaintext
33+
require.NoError(t, inner.Unmarshal([]byte{byte(protocol.ContentTypeAlert)}))
34+
require.Empty(t, inner.Content)
35+
require.Equal(t, protocol.ContentTypeAlert, inner.RealType)
36+
require.Equal(t, uint(0), inner.Zeros)
37+
}
38+
39+
func TestInnerPlaintextRejectsMissingContentType(t *testing.T) {
40+
for _, raw := range [][]byte{
41+
nil,
42+
{},
43+
{0x00},
44+
{0x00, 0x00},
45+
} {
46+
var inner InnerPlaintext
47+
require.ErrorIs(t, inner.Unmarshal(raw), errBufferTooSmall)
48+
}
49+
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package recordlayer
5+
6+
import (
7+
"encoding/binary"
8+
9+
"github.com/pion/dtls/v3/pkg/protocol"
10+
"github.com/pion/dtls/v3/pkg/protocol/alert"
11+
"github.com/pion/dtls/v3/pkg/protocol/handshake"
12+
)
13+
14+
const (
15+
maxDTLSPlaintextRecordLen = 1 << 14
16+
maxDTLSCiphertextRecordLen = maxDTLSPlaintextRecordLen + 256
17+
)
18+
19+
// HeaderLike is implemented by DTLS record header encodings.
20+
type HeaderLike interface {
21+
Marshal() ([]byte, error)
22+
Unmarshal(data []byte) error
23+
Size() int
24+
}
25+
26+
// RecordLayer13 is implemented by DTLS 1.3 plaintext and ciphertext records.
27+
type RecordLayer13 interface {
28+
Marshal() ([]byte, error)
29+
Unmarshal(data []byte) error
30+
RecordHeader() HeaderLike
31+
}
32+
33+
// PlaintextRecord13 implements DTLSPlaintext for epoch 0 records.
34+
type PlaintextRecord13 struct {
35+
Header Header
36+
Content protocol.Content
37+
}
38+
39+
// Marshal encodes a DTLS 1.3 DTLSPlaintext record.
40+
func (r *PlaintextRecord13) Marshal() ([]byte, error) {
41+
if r.Header.Epoch != 0 {
42+
return nil, errInvalidEpoch
43+
}
44+
if r.Header.Version == (protocol.Version{}) {
45+
r.Header.Version = protocol.Version1_2
46+
}
47+
if !r.Header.Version.Equal(protocol.Version1_2) {
48+
return nil, errUnsupportedProtocolVersion
49+
}
50+
51+
contentRaw, err := r.Content.Marshal()
52+
if err != nil {
53+
return nil, err
54+
}
55+
if len(contentRaw) > maxDTLSPlaintextRecordLen {
56+
return nil, ErrInvalidPacketLength
57+
}
58+
59+
r.Header.ContentLen = uint16(len(contentRaw)) //nolint:gosec // G115: checked above.
60+
r.Header.ContentType = r.Content.ContentType()
61+
62+
headerRaw, err := r.Header.Marshal()
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
return append(headerRaw, contentRaw...), nil
68+
}
69+
70+
// Unmarshal populates a DTLS 1.3 DTLSPlaintext record from binary.
71+
func (r *PlaintextRecord13) Unmarshal(data []byte) error {
72+
if err := r.Header.Unmarshal(data); err != nil {
73+
return err
74+
}
75+
if r.Header.Epoch != 0 {
76+
return errInvalidEpoch
77+
}
78+
if !r.Header.Version.Equal(protocol.Version1_2) {
79+
return errUnsupportedProtocolVersion
80+
}
81+
if r.Header.ContentLen > maxDTLSPlaintextRecordLen {
82+
return ErrInvalidPacketLength
83+
}
84+
85+
switch r.Header.ContentType {
86+
case protocol.ContentTypeChangeCipherSpec:
87+
r.Content = &protocol.ChangeCipherSpec{}
88+
case protocol.ContentTypeAlert:
89+
r.Content = &alert.Alert{}
90+
case protocol.ContentTypeHandshake:
91+
r.Content = &handshake.Handshake{}
92+
case protocol.ContentTypeApplicationData:
93+
r.Content = &protocol.ApplicationData{}
94+
default:
95+
return errInvalidContentType
96+
}
97+
98+
contentStart := r.Header.Size()
99+
contentEnd := contentStart + int(r.Header.ContentLen)
100+
if len(data) != contentEnd {
101+
return ErrInvalidPacketLength
102+
}
103+
104+
return r.Content.Unmarshal(data[contentStart:contentEnd])
105+
}
106+
107+
// RecordHeader returns the record header.
108+
func (r *PlaintextRecord13) RecordHeader() HeaderLike {
109+
return &r.Header
110+
}
111+
112+
// CiphertextRecord13 implements DTLSCiphertext for protected records.
113+
type CiphertextRecord13 struct {
114+
Header UnifiedHeader
115+
EncryptedRecord []byte
116+
}
117+
118+
// Marshal encodes a DTLS 1.3 DTLSCiphertext record.
119+
func (r *CiphertextRecord13) Marshal() ([]byte, error) {
120+
if len(r.EncryptedRecord) > maxDTLSCiphertextRecordLen {
121+
return nil, ErrInvalidPacketLength
122+
}
123+
r.Header.SeqBit = true
124+
r.Header.Length = uint16(len(r.EncryptedRecord)) //nolint:gosec // G115: checked above.
125+
r.Header.LengthBit = true
126+
127+
headerRaw, err := r.Header.Marshal()
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
out := make([]byte, 0, len(headerRaw)+len(r.EncryptedRecord))
133+
out = append(out, headerRaw...)
134+
out = append(out, r.EncryptedRecord...)
135+
136+
return out, nil
137+
}
138+
139+
// Unmarshal populates a DTLS 1.3 DTLSCiphertext record from binary.
140+
func (r *CiphertextRecord13) Unmarshal(data []byte) error {
141+
if err := r.Header.Unmarshal(data); err != nil {
142+
return err
143+
}
144+
145+
headerSize := unifiedHeaderWireSize(data[0], len(r.Header.ConnectionID))
146+
if len(data) < headerSize {
147+
return errBufferTooSmall
148+
}
149+
150+
recordLen := len(data) - headerSize
151+
if r.Header.LengthBit {
152+
recordLen = int(r.Header.Length)
153+
if len(data)-headerSize != recordLen {
154+
return ErrInvalidPacketLength
155+
}
156+
}
157+
if recordLen > maxDTLSCiphertextRecordLen {
158+
return ErrInvalidPacketLength
159+
}
160+
161+
r.EncryptedRecord = append([]byte{}, data[headerSize:headerSize+recordLen]...)
162+
163+
return nil
164+
}
165+
166+
// RecordHeader returns the record header.
167+
func (r *CiphertextRecord13) RecordHeader() HeaderLike {
168+
return &r.Header
169+
}
170+
171+
// UnpackDatagram13 extracts DTLS 1.3 records from a single datagram.
172+
func UnpackDatagram13(buf []byte, cidLength int, ciphertextHeadersEnabled bool) ([][]byte, error) {
173+
out := [][]byte{}
174+
175+
for offset := 0; len(buf) != offset; {
176+
if ciphertextHeadersEnabled {
177+
if !protocol.IsDTLS13Ciphertext(protocol.ContentType(buf[offset])) {
178+
return nil, errInvalidContentType
179+
}
180+
181+
header := UnifiedHeader{}
182+
if buf[offset]&UnifiedHeaderCIDBit != 0 {
183+
header.ConnectionID = make([]byte, cidLength)
184+
}
185+
if err := header.Unmarshal(buf[offset:]); err != nil {
186+
return nil, err
187+
}
188+
189+
headerSize := unifiedHeaderWireSize(buf[offset], len(header.ConnectionID))
190+
if !header.LengthBit {
191+
out = append(out, buf[offset:])
192+
193+
return out, nil
194+
}
195+
196+
pktLen := headerSize + int(header.Length)
197+
if offset+pktLen > len(buf) {
198+
return nil, ErrInvalidPacketLength
199+
}
200+
201+
out = append(out, buf[offset:offset+pktLen])
202+
offset += pktLen
203+
204+
continue
205+
}
206+
207+
if len(buf)-offset <= FixedHeaderSize {
208+
return nil, ErrInvalidPacketLength
209+
}
210+
211+
pktLen := FixedHeaderSize + int(binary.BigEndian.Uint16(buf[offset+fixedHeaderLenIdx:]))
212+
if offset+pktLen > len(buf) {
213+
return nil, ErrInvalidPacketLength
214+
}
215+
216+
out = append(out, buf[offset:offset+pktLen])
217+
offset += pktLen
218+
}
219+
220+
return out, nil
221+
}
222+
223+
func unifiedHeaderWireSize(firstByte byte, cidLength int) int {
224+
size := 1 + cidLength
225+
if firstByte&UnifiedHeaderSeqBit != 0 {
226+
size += 2
227+
} else {
228+
size++
229+
}
230+
if firstByte&UnifiedHeaderLengthBit != 0 {
231+
size += 2
232+
}
233+
234+
return size
235+
}

0 commit comments

Comments
 (0)