Skip to content

Commit 62219a2

Browse files
committed
DTLS 1.3 CertificateRequest + Certificate Messages
Implements TLS 1.3 Certificate Request and Certificate messages per RFC 8446 Sections 4.3.2 and 4.4.2 respectively. Adds comprehensive unit tests for both.
1 parent 9121462 commit 62219a2

7 files changed

+1152
-0
lines changed

pkg/protocol/handshake/errors.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,19 @@ var (
5353
errNotImplemented = &protocol.InternalError{
5454
Err: errors.New("feature has not been implemented yet"), //nolint:err113
5555
}
56+
errInvalidCertificateRequestContext = &protocol.FatalError{
57+
Err: errors.New("invalid certificate request context"), //nolint:err113
58+
}
59+
errInvalidCertificateEntry = &protocol.FatalError{
60+
Err: errors.New("invalid certificate entry"), //nolint:err113
61+
}
62+
errCertificateRequestContextTooLong = &protocol.FatalError{
63+
Err: errors.New("certificate request context must not be longer than 255 bytes"), //nolint:err113
64+
}
65+
errCertificateListTooLong = &protocol.FatalError{
66+
Err: errors.New("certificate list must not be longer than 2^24-1 bytes"), //nolint:err113
67+
}
68+
errMissingSignatureAlgorithmsExtension = &protocol.FatalError{
69+
Err: errors.New("signature_algorithms extension is required in CertificateRequest"), //nolint:err113
70+
}
5671
)
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package handshake
5+
6+
import (
7+
"github.com/pion/dtls/v3/internal/util"
8+
"github.com/pion/dtls/v3/pkg/protocol/extension"
9+
"golang.org/x/crypto/cryptobyte"
10+
)
11+
12+
// CertificateEntry13 represents a single certificate entry in the DTLS 1.3 Certificate message.
13+
// Each entry contains certificate data and optional per-certificate extensions.
14+
//
15+
// https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2
16+
type CertificateEntry13 struct {
17+
// CertificateData contains the DER-encoded X.509 certificate.
18+
// Can be empty for certain contexts (e.g., RawPublicKey mode).
19+
CertificateData []byte
20+
21+
// Extensions contains per-certificate extensions.
22+
// Examples: OCSP status, SignedCertificateTimestamp, etc.
23+
Extensions []extension.Extension
24+
}
25+
26+
// MessageCertificate13 represents the Certificate handshake message for DTLS 1.3.
27+
// This message is used to transport the certificate chain and associated extensions.
28+
//
29+
// https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2
30+
type MessageCertificate13 struct {
31+
// CertificateRequestContext is an opaque value that binds this certificate
32+
// to a specific CertificateRequest (for client certificates) or is empty
33+
// for server certificates.
34+
CertificateRequestContext []byte
35+
36+
// CertificateList contains the certificate chain with each entry having
37+
// optional per-certificate extensions.
38+
CertificateList []CertificateEntry13
39+
}
40+
41+
// Type returns the handshake message type.
42+
func (m MessageCertificate13) Type() Type {
43+
return TypeCertificate
44+
}
45+
46+
const (
47+
cert13ContextLengthFieldSize = 1
48+
cert13ContextMaxLength = 255
49+
cert13CertLengthFieldSize = 3
50+
)
51+
52+
// Marshal encodes the MessageCertificate13 into its wire format.
53+
//
54+
// Wire format:
55+
//
56+
// [1 byte] certificate_request_context length
57+
// [0-255] certificate_request_context data
58+
// [3 bytes] certificate_list length
59+
// For each certificate:
60+
// [3 bytes] cert_data length
61+
// [variable] cert_data (DER certificate)
62+
// [2 bytes] extensions length (from extension.Marshal)
63+
// [variable] extensions data
64+
func (m *MessageCertificate13) Marshal() ([]byte, error) {
65+
// Validate certificate_request_context length
66+
if len(m.CertificateRequestContext) > cert13ContextMaxLength {
67+
return nil, errCertificateRequestContextTooLong
68+
}
69+
70+
// Start with certificate_request_context (1-byte length prefix)
71+
out := []byte{byte(len(m.CertificateRequestContext))}
72+
out = append(out, m.CertificateRequestContext...)
73+
74+
// Build certificate_list
75+
certificateList := []byte{}
76+
for _, entry := range m.CertificateList {
77+
// Add cert_data as a 3-byte length prefix
78+
certDataLen := len(entry.CertificateData)
79+
if certDataLen == 0 || certDataLen > 0xffffff {
80+
return nil, errInvalidCertificateEntry
81+
}
82+
certDataLenBytes := make([]byte, cert13CertLengthFieldSize)
83+
util.PutBigEndianUint24(certDataLenBytes, uint32(certDataLen)) //nolint:gosec // G115
84+
certificateList = append(certificateList, certDataLenBytes...)
85+
certificateList = append(certificateList, entry.CertificateData...)
86+
87+
// Marshal extensions (includes a 2-byte length prefix)
88+
extensionsData, err := extension.Marshal(entry.Extensions)
89+
if err != nil {
90+
return nil, err
91+
}
92+
certificateList = append(certificateList, extensionsData...)
93+
}
94+
95+
// Add certificate_list with 3-byte length prefix
96+
if len(certificateList) > 0xffffff {
97+
return nil, errCertificateListTooLong
98+
}
99+
certificateListLenBytes := make([]byte, cert13CertLengthFieldSize)
100+
util.PutBigEndianUint24(certificateListLenBytes, uint32(len(certificateList))) //nolint:gosec // G115
101+
out = append(out, certificateListLenBytes...)
102+
out = append(out, certificateList...)
103+
104+
return out, nil
105+
}
106+
107+
// parseCertificate13Entry parses a single certificate entry from the cryptobyte string.
108+
func parseCertificate13Entry(str *cryptobyte.String) (*CertificateEntry13, error) {
109+
// Read cert_data with 3-byte length prefix
110+
var certData cryptobyte.String
111+
if !str.ReadUint24LengthPrefixed(&certData) {
112+
return nil, errInvalidCertificateEntry
113+
}
114+
115+
// Validate cert_data length is in valid range <1..2^24-1>
116+
if len(certData) == 0 {
117+
return nil, errInvalidCertificateEntry
118+
}
119+
120+
// Copy cert_data to avoid aliasing issues
121+
certDataBytes := make([]byte, len(certData))
122+
copy(certDataBytes, certData)
123+
124+
// Read extensions length (2 bytes)
125+
var extensionsLen uint16
126+
if !str.ReadUint16(&extensionsLen) {
127+
return nil, errInvalidCertificateEntry
128+
}
129+
130+
// Read extensions data
131+
var extensionsDataAlias []byte
132+
if !str.ReadBytes(&extensionsDataAlias, int(extensionsLen)) {
133+
return nil, errInvalidCertificateEntry
134+
}
135+
136+
// Prepend the length for extension.Unmarshal (which expects it)
137+
// Take defensive copy to avoid aliasing issues
138+
extensionsWithLen := make([]byte, 2+len(extensionsDataAlias))
139+
extensionsWithLen[0] = byte(extensionsLen >> 8)
140+
extensionsWithLen[1] = byte(extensionsLen)
141+
copy(extensionsWithLen[2:], extensionsDataAlias)
142+
143+
// Unmarshal extensions
144+
extensions, err := extension.Unmarshal(extensionsWithLen)
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
return &CertificateEntry13{
150+
CertificateData: certDataBytes,
151+
Extensions: extensions,
152+
}, nil
153+
}
154+
155+
// Unmarshal decodes the MessageCertificate13 from its wire format.
156+
func (m *MessageCertificate13) Unmarshal(data []byte) error {
157+
// Validate minimum data length
158+
if len(data) < cert13ContextLengthFieldSize+cert13CertLengthFieldSize {
159+
return errBufferTooSmall
160+
}
161+
162+
str := cryptobyte.String(data)
163+
164+
// Read certificate_request_context with 1-byte length prefix
165+
var contextData cryptobyte.String
166+
if !str.ReadUint8LengthPrefixed(&contextData) {
167+
return errInvalidCertificateRequestContext
168+
}
169+
m.CertificateRequestContext = make([]byte, len(contextData))
170+
copy(m.CertificateRequestContext, contextData)
171+
172+
// Read certificate_list with 3-byte length prefix
173+
var certificateListData cryptobyte.String
174+
if !str.ReadUint24LengthPrefixed(&certificateListData) {
175+
return errInvalidCertificateEntry
176+
}
177+
178+
// Ensure no trailing data
179+
if len(str) != 0 {
180+
return errLengthMismatch
181+
}
182+
183+
// Parse certificate_list
184+
m.CertificateList = []CertificateEntry13{}
185+
for len(certificateListData) > 0 {
186+
entry, err := parseCertificate13Entry(&certificateListData)
187+
if err != nil {
188+
return err
189+
}
190+
m.CertificateList = append(m.CertificateList, *entry)
191+
}
192+
193+
return nil
194+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package handshake
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func FuzzMessageCertificate13(f *testing.F) {
11+
// Seed with valid minimal message (empty context, empty cert list)
12+
f.Add([]byte{
13+
0x00, // context length = 0
14+
0x00, 0x00, 0x00, // certificate_list length = 0
15+
})
16+
17+
// Seed with valid message with context
18+
f.Add([]byte{
19+
0x04, // context length = 4
20+
0x01, 0x02, 0x03, 0x04, // context data
21+
0x00, 0x00, 0x00, // certificate_list length = 0
22+
})
23+
24+
// Seed with valid message with single cert (no extensions)
25+
f.Add([]byte{
26+
0x00, // context length = 0
27+
0x00, 0x00, 0x07, // certificate_list length = 7
28+
0x00, 0x00, 0x03, // cert_data length = 3
29+
0xDE, 0xAD, 0xBE, // cert_data
30+
0x00, 0x00, // extensions length = 0
31+
})
32+
33+
// Seed with invalid data for edge case testing
34+
f.Add([]byte{0x00})
35+
f.Add([]byte{0xFF, 0xFF, 0xFF, 0xFF})
36+
37+
f.Fuzz(func(_ *testing.T, data []byte) {
38+
msg := &MessageCertificate13{}
39+
_ = msg.Unmarshal(data)
40+
})
41+
}

0 commit comments

Comments
 (0)