1// Copyright 2025 The BoringSSL Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package runner
16
17import "slices"
18
19func addChangeCipherSpecTests() {
20	// Test missing ChangeCipherSpecs.
21	testCases = append(testCases, testCase{
22		name: "SkipChangeCipherSpec-Client",
23		config: Config{
24			MaxVersion: VersionTLS12,
25			Bugs: ProtocolBugs{
26				SkipChangeCipherSpec: true,
27			},
28		},
29		shouldFail:    true,
30		expectedError: ":UNEXPECTED_RECORD:",
31	})
32	testCases = append(testCases, testCase{
33		testType: serverTest,
34		name:     "SkipChangeCipherSpec-Server",
35		config: Config{
36			MaxVersion: VersionTLS12,
37			Bugs: ProtocolBugs{
38				SkipChangeCipherSpec: true,
39			},
40		},
41		shouldFail:    true,
42		expectedError: ":UNEXPECTED_RECORD:",
43	})
44	testCases = append(testCases, testCase{
45		testType: serverTest,
46		name:     "SkipChangeCipherSpec-Server-NPN",
47		config: Config{
48			MaxVersion: VersionTLS12,
49			NextProtos: []string{"bar"},
50			Bugs: ProtocolBugs{
51				SkipChangeCipherSpec: true,
52			},
53		},
54		flags: []string{
55			"-advertise-npn", "\x03foo\x03bar\x03baz",
56		},
57		shouldFail:    true,
58		expectedError: ":UNEXPECTED_RECORD:",
59	})
60
61	// Test synchronization between the handshake and ChangeCipherSpec.
62	// Partial post-CCS handshake messages before ChangeCipherSpec should be
63	// rejected. Test both with and without handshake packing to handle both
64	// when the partial post-CCS message is in its own record and when it is
65	// attached to the pre-CCS message.
66	for _, packed := range []bool{false, true} {
67		var suffix string
68		if packed {
69			suffix = "-Packed"
70		}
71
72		testCases = append(testCases, testCase{
73			name: "FragmentAcrossChangeCipherSpec-Client" + suffix,
74			config: Config{
75				MaxVersion: VersionTLS12,
76				Bugs: ProtocolBugs{
77					FragmentAcrossChangeCipherSpec: true,
78					PackHandshakeFlight:            packed,
79				},
80			},
81			shouldFail:    true,
82			expectedError: ":UNEXPECTED_RECORD:",
83		})
84		testCases = append(testCases, testCase{
85			name: "FragmentAcrossChangeCipherSpec-Client-Resume" + suffix,
86			config: Config{
87				MaxVersion: VersionTLS12,
88			},
89			resumeSession: true,
90			resumeConfig: &Config{
91				MaxVersion: VersionTLS12,
92				Bugs: ProtocolBugs{
93					FragmentAcrossChangeCipherSpec: true,
94					PackHandshakeFlight:            packed,
95				},
96			},
97			shouldFail:    true,
98			expectedError: ":UNEXPECTED_RECORD:",
99		})
100		testCases = append(testCases, testCase{
101			testType: serverTest,
102			name:     "FragmentAcrossChangeCipherSpec-Server" + suffix,
103			config: Config{
104				MaxVersion: VersionTLS12,
105				Bugs: ProtocolBugs{
106					FragmentAcrossChangeCipherSpec: true,
107					PackHandshakeFlight:            packed,
108				},
109			},
110			shouldFail:    true,
111			expectedError: ":UNEXPECTED_RECORD:",
112		})
113		testCases = append(testCases, testCase{
114			testType: serverTest,
115			name:     "FragmentAcrossChangeCipherSpec-Server-Resume" + suffix,
116			config: Config{
117				MaxVersion: VersionTLS12,
118			},
119			resumeSession: true,
120			resumeConfig: &Config{
121				MaxVersion: VersionTLS12,
122				Bugs: ProtocolBugs{
123					FragmentAcrossChangeCipherSpec: true,
124					PackHandshakeFlight:            packed,
125				},
126			},
127			shouldFail:    true,
128			expectedError: ":UNEXPECTED_RECORD:",
129		})
130		testCases = append(testCases, testCase{
131			testType: serverTest,
132			name:     "FragmentAcrossChangeCipherSpec-Server-NPN" + suffix,
133			config: Config{
134				MaxVersion: VersionTLS12,
135				NextProtos: []string{"bar"},
136				Bugs: ProtocolBugs{
137					FragmentAcrossChangeCipherSpec: true,
138					PackHandshakeFlight:            packed,
139				},
140			},
141			flags: []string{
142				"-advertise-npn", "\x03foo\x03bar\x03baz",
143			},
144			shouldFail:    true,
145			expectedError: ":UNEXPECTED_RECORD:",
146		})
147	}
148
149	// In TLS 1.2 resumptions, the client sends ClientHello in the first flight
150	// and ChangeCipherSpec + Finished in the second flight. Test the server's
151	// behavior when the Finished message is fragmented across not only
152	// ChangeCipherSpec but also the flight boundary.
153	testCases = append(testCases, testCase{
154		testType: serverTest,
155		name:     "PartialClientFinishedWithClientHello-TLS12-Resume",
156		config: Config{
157			MaxVersion: VersionTLS12,
158		},
159		resumeConfig: &Config{
160			MaxVersion: VersionTLS12,
161			Bugs: ProtocolBugs{
162				PartialClientFinishedWithClientHello: true,
163			},
164		},
165		resumeSession:      true,
166		shouldFail:         true,
167		expectedError:      ":EXCESS_HANDSHAKE_DATA:",
168		expectedLocalError: "remote error: unexpected message",
169	})
170
171	// In TLS 1.2 full handshakes without tickets, the server's first flight ends
172	// with ServerHelloDone and the second flight is ChangeCipherSpec + Finished.
173	// Test the client's behavior when the Finished message is fragmented across
174	// not only ChangeCipherSpec but also the flight boundary.
175	testCases = append(testCases, testCase{
176		testType: clientTest,
177		name:     "PartialFinishedWithServerHelloDone",
178		config: Config{
179			MaxVersion:             VersionTLS12,
180			SessionTicketsDisabled: true,
181			Bugs: ProtocolBugs{
182				PartialFinishedWithServerHelloDone: true,
183			},
184		},
185		shouldFail:         true,
186		expectedError:      ":EXCESS_HANDSHAKE_DATA:",
187		expectedLocalError: "remote error: unexpected message",
188	})
189
190	// Test that, in DTLS 1.2, key changes are not allowed when there are
191	// buffered messages. Do this sending all messages in reverse, so that later
192	// ones are buffered, and leaving Finished unencrypted.
193	testCases = append(testCases, testCase{
194		protocol: dtls,
195		testType: serverTest,
196		name:     "KeyChangeWithBufferedMessages-DTLS",
197		config: Config{
198			MaxVersion: VersionTLS12,
199			Bugs: ProtocolBugs{
200				WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) {
201					next = slices.Clone(next)
202					slices.Reverse(next)
203					for i := range next {
204						next[i].Epoch = 0
205					}
206					c.WriteFlight(next)
207				},
208			},
209		},
210		shouldFail:    true,
211		expectedError: ":EXCESS_HANDSHAKE_DATA:",
212	})
213
214	// Test synchronization between encryption changes and the handshake in
215	// TLS 1.3, where ChangeCipherSpec is implicit.
216	testCases = append(testCases, testCase{
217		name: "PartialEncryptedExtensionsWithServerHello",
218		config: Config{
219			MaxVersion: VersionTLS13,
220			Bugs: ProtocolBugs{
221				PartialEncryptedExtensionsWithServerHello: true,
222			},
223		},
224		shouldFail:    true,
225		expectedError: ":EXCESS_HANDSHAKE_DATA:",
226	})
227	testCases = append(testCases, testCase{
228		testType: serverTest,
229		name:     "PartialClientFinishedWithClientHello",
230		config: Config{
231			MaxVersion: VersionTLS13,
232			Bugs: ProtocolBugs{
233				PartialClientFinishedWithClientHello: true,
234			},
235		},
236		shouldFail:    true,
237		expectedError: ":EXCESS_HANDSHAKE_DATA:",
238	})
239	testCases = append(testCases, testCase{
240		testType: serverTest,
241		name:     "PartialClientFinishedWithSecondClientHello",
242		config: Config{
243			MaxVersion: VersionTLS13,
244			// Trigger a curve-based HelloRetryRequest.
245			DefaultCurves: []CurveID{},
246			Bugs: ProtocolBugs{
247				PartialClientFinishedWithSecondClientHello: true,
248			},
249		},
250		shouldFail:    true,
251		expectedError: ":EXCESS_HANDSHAKE_DATA:",
252	})
253	testCases = append(testCases, testCase{
254		testType: serverTest,
255		name:     "PartialEndOfEarlyDataWithClientHello",
256		config: Config{
257			MaxVersion: VersionTLS13,
258		},
259		resumeConfig: &Config{
260			MaxVersion: VersionTLS13,
261			Bugs: ProtocolBugs{
262				PartialEndOfEarlyDataWithClientHello: true,
263			},
264		},
265		resumeSession: true,
266		earlyData:     true,
267		shouldFail:    true,
268		expectedError: ":EXCESS_HANDSHAKE_DATA:",
269	})
270
271	// Test that early ChangeCipherSpecs are handled correctly.
272	testCases = append(testCases, testCase{
273		testType: serverTest,
274		name:     "EarlyChangeCipherSpec-server-1",
275		config: Config{
276			MaxVersion: VersionTLS12,
277			Bugs: ProtocolBugs{
278				EarlyChangeCipherSpec: 1,
279			},
280		},
281		shouldFail:    true,
282		expectedError: ":UNEXPECTED_RECORD:",
283	})
284	testCases = append(testCases, testCase{
285		testType: serverTest,
286		name:     "EarlyChangeCipherSpec-server-2",
287		config: Config{
288			MaxVersion: VersionTLS12,
289			Bugs: ProtocolBugs{
290				EarlyChangeCipherSpec: 2,
291			},
292		},
293		shouldFail:    true,
294		expectedError: ":UNEXPECTED_RECORD:",
295	})
296	testCases = append(testCases, testCase{
297		protocol: dtls,
298		name:     "StrayChangeCipherSpec",
299		config: Config{
300			// TODO(davidben): Once DTLS 1.3 exists, test
301			// that stray ChangeCipherSpec messages are
302			// rejected.
303			MaxVersion: VersionTLS12,
304			Bugs: ProtocolBugs{
305				WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) {
306					c.WriteFragments([]DTLSFragment{{IsChangeCipherSpec: true, Data: []byte{1}}})
307					c.WriteFlight(next)
308				},
309			},
310		},
311	})
312
313	// Test that the contents of ChangeCipherSpec are checked.
314	testCases = append(testCases, testCase{
315		name: "BadChangeCipherSpec-1",
316		config: Config{
317			MaxVersion: VersionTLS12,
318			Bugs: ProtocolBugs{
319				BadChangeCipherSpec: []byte{2},
320			},
321		},
322		shouldFail:    true,
323		expectedError: ":BAD_CHANGE_CIPHER_SPEC:",
324	})
325	testCases = append(testCases, testCase{
326		name: "BadChangeCipherSpec-2",
327		config: Config{
328			MaxVersion: VersionTLS12,
329			Bugs: ProtocolBugs{
330				BadChangeCipherSpec: []byte{1, 1},
331			},
332		},
333		shouldFail:    true,
334		expectedError: ":BAD_CHANGE_CIPHER_SPEC:",
335	})
336	testCases = append(testCases, testCase{
337		protocol: dtls,
338		name:     "BadChangeCipherSpec-DTLS-1",
339		config: Config{
340			MaxVersion: VersionTLS12,
341			Bugs: ProtocolBugs{
342				BadChangeCipherSpec: []byte{2},
343			},
344		},
345		shouldFail:    true,
346		expectedError: ":BAD_CHANGE_CIPHER_SPEC:",
347	})
348	testCases = append(testCases, testCase{
349		protocol: dtls,
350		name:     "BadChangeCipherSpec-DTLS-2",
351		config: Config{
352			MaxVersion: VersionTLS12,
353			Bugs: ProtocolBugs{
354				BadChangeCipherSpec: []byte{1, 1},
355			},
356		},
357		shouldFail:    true,
358		expectedError: ":BAD_CHANGE_CIPHER_SPEC:",
359	})
360}
361