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