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 (
18	"bytes"
19	"crypto/rand"
20	"fmt"
21	"strconv"
22)
23
24const (
25	shrinkingCompressionAlgID = 0xff01
26	expandingCompressionAlgID = 0xff02
27	randomCompressionAlgID    = 0xff03
28)
29
30var (
31	// shrinkingPrefix is the first two bytes of a Certificate message.
32	shrinkingPrefix = []byte{0, 0}
33	// expandingPrefix is just some arbitrary byte string. This has to match the
34	// value in the shim.
35	expandingPrefix = []byte{1, 2, 3, 4}
36)
37
38var shrinkingCompression = CertCompressionAlg{
39	Compress: func(uncompressed []byte) []byte {
40		if !bytes.HasPrefix(uncompressed, shrinkingPrefix) {
41			panic(fmt.Sprintf("cannot compress certificate message %x", uncompressed))
42		}
43		return uncompressed[len(shrinkingPrefix):]
44	},
45	Decompress: func(out []byte, compressed []byte) bool {
46		if len(out) != len(shrinkingPrefix)+len(compressed) {
47			return false
48		}
49
50		copy(out, shrinkingPrefix)
51		copy(out[len(shrinkingPrefix):], compressed)
52		return true
53	},
54}
55
56var expandingCompression = CertCompressionAlg{
57	Compress: func(uncompressed []byte) []byte {
58		ret := make([]byte, 0, len(expandingPrefix)+len(uncompressed))
59		ret = append(ret, expandingPrefix...)
60		return append(ret, uncompressed...)
61	},
62	Decompress: func(out []byte, compressed []byte) bool {
63		if !bytes.HasPrefix(compressed, expandingPrefix) {
64			return false
65		}
66		copy(out, compressed[len(expandingPrefix):])
67		return true
68	},
69}
70
71var randomCompression = CertCompressionAlg{
72	Compress: func(uncompressed []byte) []byte {
73		ret := make([]byte, 1+len(uncompressed))
74		if _, err := rand.Read(ret[:1]); err != nil {
75			panic(err)
76		}
77		copy(ret[1:], uncompressed)
78		return ret
79	},
80	Decompress: func(out []byte, compressed []byte) bool {
81		if len(compressed) != 1+len(out) {
82			return false
83		}
84		copy(out, compressed[1:])
85		return true
86	},
87}
88
89func addCertCompressionTests() {
90	for _, ver := range tlsVersions {
91		if ver.version < VersionTLS12 {
92			continue
93		}
94
95		// Duplicate compression algorithms is an error, even if nothing is
96		// configured.
97		testCases = append(testCases, testCase{
98			testType: serverTest,
99			name:     "DuplicateCertCompressionExt-" + ver.name,
100			config: Config{
101				MinVersion: ver.version,
102				MaxVersion: ver.version,
103				Bugs: ProtocolBugs{
104					DuplicateCompressedCertAlgs: true,
105				},
106			},
107			shouldFail:    true,
108			expectedError: ":ERROR_PARSING_EXTENSION:",
109		})
110
111		// With compression algorithms configured, an duplicate values should still
112		// be an error.
113		testCases = append(testCases, testCase{
114			testType: serverTest,
115			name:     "DuplicateCertCompressionExt2-" + ver.name,
116			flags:    []string{"-install-cert-compression-algs"},
117			config: Config{
118				MinVersion: ver.version,
119				MaxVersion: ver.version,
120				Bugs: ProtocolBugs{
121					DuplicateCompressedCertAlgs: true,
122				},
123			},
124			shouldFail:    true,
125			expectedError: ":ERROR_PARSING_EXTENSION:",
126		})
127
128		if ver.version < VersionTLS13 {
129			testCases = append(testCases, testCase{
130				testType: serverTest,
131				name:     "CertCompressionIgnoredBefore13-" + ver.name,
132				flags:    []string{"-install-cert-compression-algs"},
133				config: Config{
134					MinVersion: ver.version,
135					MaxVersion: ver.version,
136					CertCompressionAlgs: map[uint16]CertCompressionAlg{
137						expandingCompressionAlgID: expandingCompression,
138					},
139				},
140			})
141
142			continue
143		}
144
145		testCases = append(testCases, testCase{
146			testType: serverTest,
147			name:     "CertCompressionExpands-" + ver.name,
148			flags:    []string{"-install-cert-compression-algs"},
149			config: Config{
150				MinVersion: ver.version,
151				MaxVersion: ver.version,
152				CertCompressionAlgs: map[uint16]CertCompressionAlg{
153					expandingCompressionAlgID: expandingCompression,
154				},
155				Bugs: ProtocolBugs{
156					ExpectedCompressedCert: expandingCompressionAlgID,
157				},
158			},
159		})
160
161		testCases = append(testCases, testCase{
162			testType: serverTest,
163			name:     "CertCompressionShrinks-" + ver.name,
164			flags:    []string{"-install-cert-compression-algs"},
165			config: Config{
166				MinVersion: ver.version,
167				MaxVersion: ver.version,
168				CertCompressionAlgs: map[uint16]CertCompressionAlg{
169					shrinkingCompressionAlgID: shrinkingCompression,
170				},
171				Bugs: ProtocolBugs{
172					ExpectedCompressedCert: shrinkingCompressionAlgID,
173				},
174			},
175		})
176
177		// Test that the shim behaves consistently if the compression function
178		// is non-deterministic. This is intended to model version differences
179		// between the shim and handshaker with handshake hints, but it is also
180		// useful in confirming we only call the callbacks once.
181		testCases = append(testCases, testCase{
182			testType: serverTest,
183			name:     "CertCompressionRandom-" + ver.name,
184			flags:    []string{"-install-cert-compression-algs"},
185			config: Config{
186				MinVersion: ver.version,
187				MaxVersion: ver.version,
188				CertCompressionAlgs: map[uint16]CertCompressionAlg{
189					randomCompressionAlgID: randomCompression,
190				},
191				Bugs: ProtocolBugs{
192					ExpectedCompressedCert: randomCompressionAlgID,
193				},
194			},
195		})
196
197		// With both algorithms configured, the server should pick its most
198		// preferable. (Which is expandingCompressionAlgID.)
199		testCases = append(testCases, testCase{
200			testType: serverTest,
201			name:     "CertCompressionPriority-" + ver.name,
202			flags:    []string{"-install-cert-compression-algs"},
203			config: Config{
204				MinVersion: ver.version,
205				MaxVersion: ver.version,
206				CertCompressionAlgs: map[uint16]CertCompressionAlg{
207					shrinkingCompressionAlgID: shrinkingCompression,
208					expandingCompressionAlgID: expandingCompression,
209				},
210				Bugs: ProtocolBugs{
211					ExpectedCompressedCert: expandingCompressionAlgID,
212				},
213			},
214		})
215
216		// With no common algorithms configured, the server should decline
217		// compression.
218		testCases = append(testCases, testCase{
219			testType: serverTest,
220			name:     "CertCompressionNoCommonAlgs-" + ver.name,
221			flags:    []string{"-install-one-cert-compression-alg", strconv.Itoa(shrinkingCompressionAlgID)},
222			config: Config{
223				MinVersion: ver.version,
224				MaxVersion: ver.version,
225				CertCompressionAlgs: map[uint16]CertCompressionAlg{
226					expandingCompressionAlgID: expandingCompression,
227				},
228				Bugs: ProtocolBugs{
229					ExpectUncompressedCert: true,
230				},
231			},
232		})
233
234		testCases = append(testCases, testCase{
235			testType: clientTest,
236			name:     "CertCompressionExpandsClient-" + ver.name,
237			flags:    []string{"-install-cert-compression-algs"},
238			config: Config{
239				MinVersion: ver.version,
240				MaxVersion: ver.version,
241				CertCompressionAlgs: map[uint16]CertCompressionAlg{
242					expandingCompressionAlgID: expandingCompression,
243				},
244				Bugs: ProtocolBugs{
245					ExpectedCompressedCert: expandingCompressionAlgID,
246				},
247			},
248		})
249
250		testCases = append(testCases, testCase{
251			testType: clientTest,
252			name:     "CertCompressionShrinksClient-" + ver.name,
253			flags:    []string{"-install-cert-compression-algs"},
254			config: Config{
255				MinVersion: ver.version,
256				MaxVersion: ver.version,
257				CertCompressionAlgs: map[uint16]CertCompressionAlg{
258					shrinkingCompressionAlgID: shrinkingCompression,
259				},
260				Bugs: ProtocolBugs{
261					ExpectedCompressedCert: shrinkingCompressionAlgID,
262				},
263			},
264		})
265
266		testCases = append(testCases, testCase{
267			testType: clientTest,
268			name:     "CertCompressionBadAlgIDClient-" + ver.name,
269			flags:    []string{"-install-cert-compression-algs"},
270			config: Config{
271				MinVersion: ver.version,
272				MaxVersion: ver.version,
273				CertCompressionAlgs: map[uint16]CertCompressionAlg{
274					shrinkingCompressionAlgID: shrinkingCompression,
275				},
276				Bugs: ProtocolBugs{
277					ExpectedCompressedCert:   shrinkingCompressionAlgID,
278					SendCertCompressionAlgID: 1234,
279				},
280			},
281			shouldFail:    true,
282			expectedError: ":UNKNOWN_CERT_COMPRESSION_ALG:",
283		})
284
285		testCases = append(testCases, testCase{
286			testType: clientTest,
287			name:     "CertCompressionTooSmallClient-" + ver.name,
288			flags:    []string{"-install-cert-compression-algs"},
289			config: Config{
290				MinVersion: ver.version,
291				MaxVersion: ver.version,
292				CertCompressionAlgs: map[uint16]CertCompressionAlg{
293					shrinkingCompressionAlgID: shrinkingCompression,
294				},
295				Bugs: ProtocolBugs{
296					ExpectedCompressedCert:     shrinkingCompressionAlgID,
297					SendCertUncompressedLength: 12,
298				},
299			},
300			shouldFail:    true,
301			expectedError: ":CERT_DECOMPRESSION_FAILED:",
302		})
303
304		testCases = append(testCases, testCase{
305			testType: clientTest,
306			name:     "CertCompressionTooLargeClient-" + ver.name,
307			flags:    []string{"-install-cert-compression-algs"},
308			config: Config{
309				MinVersion: ver.version,
310				MaxVersion: ver.version,
311				CertCompressionAlgs: map[uint16]CertCompressionAlg{
312					shrinkingCompressionAlgID: shrinkingCompression,
313				},
314				Bugs: ProtocolBugs{
315					ExpectedCompressedCert:     shrinkingCompressionAlgID,
316					SendCertUncompressedLength: 1 << 20,
317				},
318			},
319			shouldFail:    true,
320			expectedError: ":UNCOMPRESSED_CERT_TOO_LARGE:",
321		})
322	}
323}
324