// Copyright 2025 The BoringSSL Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package runner

import (
	"fmt"
)

func addExtensionTests() {
	exampleCertificate := rootCA.Issue(X509Info{
		PrivateKey: &ecdsaP256Key,
		DNSNames:   []string{"example.com"},
	}).ToCredential()

	// Repeat extensions tests at all versions.
	for _, protocol := range []protocol{tls, dtls, quic} {
		for _, ver := range allVersions(protocol) {
			suffix := fmt.Sprintf("%s-%s", protocol.String(), ver.name)

			// Test that duplicate extensions are rejected.
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "DuplicateExtensionClient-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						DuplicateExtension: true,
					},
				},
				shouldFail:         true,
				expectedLocalError: "remote error: error decoding message",
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: serverTest,
				name:     "DuplicateExtensionServer-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						DuplicateExtension: true,
					},
				},
				shouldFail:         true,
				expectedLocalError: "remote error: error decoding message",
			})

			// Test SNI.
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "ServerNameExtensionClient-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						ExpectServerName: "example.com",
					},
					Credential: &exampleCertificate,
				},
				flags: []string{"-host-name", "example.com"},
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "ServerNameExtensionClientMismatch-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						ExpectServerName: "mismatch.com",
					},
				},
				flags:              []string{"-host-name", "example.com"},
				shouldFail:         true,
				expectedLocalError: "tls: unexpected server name",
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "ServerNameExtensionClientMissing-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						ExpectServerName: "missing.com",
					},
				},
				shouldFail:         true,
				expectedLocalError: "tls: unexpected server name",
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "TolerateServerNameAck-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						SendServerNameAck: true,
					},
					Credential: &exampleCertificate,
				},
				flags:         []string{"-host-name", "example.com"},
				resumeSession: true,
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "UnsolicitedServerNameAck-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						SendServerNameAck: true,
					},
				},
				shouldFail:         true,
				expectedError:      ":UNEXPECTED_EXTENSION:",
				expectedLocalError: "remote error: unsupported extension",
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: serverTest,
				name:     "ServerNameExtensionServer-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					ServerName: "example.com",
				},
				flags:         []string{"-expect-server-name", "example.com"},
				resumeSession: true,
				expectations: connectionExpectations{
					serverNameAck: ptrTo(true),
				},
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: serverTest,
				name:     "ServerNameExtensionServer-NoACK-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					ServerName: "example.com",
				},
				flags: []string{
					"-expect-server-name", "example.com",
					"-no-server-name-ack",
				},
				resumeSession: true,
				expectations: connectionExpectations{
					serverNameAck: ptrTo(false),
				},
			})

			// Test ALPN.
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           clientTest,
				skipQUICALPNConfig: true,
				name:               "ALPNClient-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{"foo"},
				},
				flags: []string{
					"-advertise-alpn", "\x03foo\x03bar\x03baz",
					"-expect-alpn", "foo",
				},
				expectations: connectionExpectations{
					nextProto:     "foo",
					nextProtoType: alpn,
				},
				resumeSession: true,
			})
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           clientTest,
				skipQUICALPNConfig: true,
				name:               "ALPNClient-RejectUnknown-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						SendALPN: "baz",
					},
				},
				flags: []string{
					"-advertise-alpn", "\x03foo\x03bar",
				},
				shouldFail:         true,
				expectedError:      ":INVALID_ALPN_PROTOCOL:",
				expectedLocalError: "remote error: illegal parameter",
			})
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           clientTest,
				skipQUICALPNConfig: true,
				name:               "ALPNClient-AllowUnknown-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						SendALPN: "baz",
					},
				},
				flags: []string{
					"-advertise-alpn", "\x03foo\x03bar",
					"-allow-unknown-alpn-protos",
					"-expect-alpn", "baz",
				},
			})
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           serverTest,
				skipQUICALPNConfig: true,
				name:               "ALPNServer-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{"foo", "bar", "baz"},
				},
				flags: []string{
					"-expect-advertised-alpn", "\x03foo\x03bar\x03baz",
					"-select-alpn", "foo",
				},
				expectations: connectionExpectations{
					nextProto:     "foo",
					nextProtoType: alpn,
				},
				resumeSession: true,
			})

			var shouldDeclineALPNFail bool
			var declineALPNError, declineALPNLocalError string
			if protocol == quic {
				// ALPN is mandatory in QUIC.
				shouldDeclineALPNFail = true
				declineALPNError = ":NO_APPLICATION_PROTOCOL:"
				declineALPNLocalError = "remote error: no application protocol"
			}
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           serverTest,
				skipQUICALPNConfig: true,
				name:               "ALPNServer-Decline-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{"foo", "bar", "baz"},
				},
				flags: []string{"-decline-alpn"},
				expectations: connectionExpectations{
					noNextProto: true,
				},
				resumeSession:      true,
				shouldFail:         shouldDeclineALPNFail,
				expectedError:      declineALPNError,
				expectedLocalError: declineALPNLocalError,
			})

			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           serverTest,
				skipQUICALPNConfig: true,
				name:               "ALPNServer-Reject-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{"foo", "bar", "baz"},
				},
				flags:              []string{"-reject-alpn"},
				shouldFail:         true,
				expectedError:      ":NO_APPLICATION_PROTOCOL:",
				expectedLocalError: "remote error: no application protocol",
			})

			// Test that the server implementation catches itself if the
			// callback tries to return an invalid empty ALPN protocol.
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           serverTest,
				skipQUICALPNConfig: true,
				name:               "ALPNServer-SelectEmpty-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{"foo", "bar", "baz"},
				},
				flags: []string{
					"-expect-advertised-alpn", "\x03foo\x03bar\x03baz",
					"-select-empty-alpn",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: internal error",
				expectedError:      ":INVALID_ALPN_PROTOCOL:",
			})

			// Test ALPN in async mode as well to ensure that extensions callbacks are only
			// called once.
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           serverTest,
				skipQUICALPNConfig: true,
				name:               "ALPNServer-Async-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{"foo", "bar", "baz"},
					// Prior to TLS 1.3, exercise the asynchronous session callback.
					SessionTicketsDisabled: ver.version < VersionTLS13,
				},
				flags: []string{
					"-expect-advertised-alpn", "\x03foo\x03bar\x03baz",
					"-select-alpn", "foo",
					"-async",
				},
				expectations: connectionExpectations{
					nextProto:     "foo",
					nextProtoType: alpn,
				},
				resumeSession: true,
			})

			var emptyString string
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           clientTest,
				skipQUICALPNConfig: true,
				name:               "ALPNClient-EmptyProtocolName-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					NextProtos: []string{""},
					Bugs: ProtocolBugs{
						// A server returning an empty ALPN protocol
						// should be rejected.
						ALPNProtocol: &emptyString,
					},
				},
				flags: []string{
					"-advertise-alpn", "\x03foo",
				},
				shouldFail:    true,
				expectedError: ":PARSE_TLSEXT:",
			})
			testCases = append(testCases, testCase{
				protocol:           protocol,
				testType:           serverTest,
				skipQUICALPNConfig: true,
				name:               "ALPNServer-EmptyProtocolName-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					// A ClientHello containing an empty ALPN protocol
					// should be rejected.
					NextProtos: []string{"foo", "", "baz"},
				},
				flags: []string{
					"-select-alpn", "foo",
				},
				shouldFail:    true,
				expectedError: ":PARSE_TLSEXT:",
			})

			// Test NPN and the interaction with ALPN.
			if ver.version < VersionTLS13 && protocol == tls {
				// Test that the server prefers ALPN over NPN.
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "ALPNServer-Preferred-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						NextProtos: []string{"foo", "bar", "baz"},
					},
					flags: []string{
						"-expect-advertised-alpn", "\x03foo\x03bar\x03baz",
						"-select-alpn", "foo",
						"-advertise-npn", "\x03foo\x03bar\x03baz",
					},
					expectations: connectionExpectations{
						nextProto:     "foo",
						nextProtoType: alpn,
					},
					resumeSession: true,
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "ALPNServer-Preferred-Swapped-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						NextProtos: []string{"foo", "bar", "baz"},
						Bugs: ProtocolBugs{
							SwapNPNAndALPN: true,
						},
					},
					flags: []string{
						"-expect-advertised-alpn", "\x03foo\x03bar\x03baz",
						"-select-alpn", "foo",
						"-advertise-npn", "\x03foo\x03bar\x03baz",
					},
					expectations: connectionExpectations{
						nextProto:     "foo",
						nextProtoType: alpn,
					},
					resumeSession: true,
				})

				// Test that negotiating both NPN and ALPN is forbidden.
				testCases = append(testCases, testCase{
					protocol: protocol,
					name:     "NegotiateALPNAndNPN-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						NextProtos: []string{"foo", "bar", "baz"},
						Bugs: ProtocolBugs{
							NegotiateALPNAndNPN: true,
						},
					},
					flags: []string{
						"-advertise-alpn", "\x03foo",
						"-select-next-proto", "foo",
					},
					shouldFail:    true,
					expectedError: ":NEGOTIATED_BOTH_NPN_AND_ALPN:",
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					name:     "NegotiateALPNAndNPN-Swapped-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						NextProtos: []string{"foo", "bar", "baz"},
						Bugs: ProtocolBugs{
							NegotiateALPNAndNPN: true,
							SwapNPNAndALPN:      true,
						},
					},
					flags: []string{
						"-advertise-alpn", "\x03foo",
						"-select-next-proto", "foo",
					},
					shouldFail:    true,
					expectedError: ":NEGOTIATED_BOTH_NPN_AND_ALPN:",
				})
			}

			// Test missing ALPN in QUIC
			if protocol == quic {
				testCases = append(testCases, testCase{
					testType: clientTest,
					protocol: protocol,
					name:     "Client-ALPNMissingFromConfig-" + suffix,
					config: Config{
						MinVersion: ver.version,
						MaxVersion: ver.version,
					},
					skipQUICALPNConfig: true,
					shouldFail:         true,
					expectedError:      ":NO_APPLICATION_PROTOCOL:",
				})
				testCases = append(testCases, testCase{
					testType: clientTest,
					protocol: protocol,
					name:     "Client-ALPNMissing-" + suffix,
					config: Config{
						MinVersion: ver.version,
						MaxVersion: ver.version,
					},
					flags: []string{
						"-advertise-alpn", "\x03foo",
					},
					skipQUICALPNConfig: true,
					shouldFail:         true,
					expectedError:      ":NO_APPLICATION_PROTOCOL:",
					expectedLocalError: "remote error: no application protocol",
				})
				testCases = append(testCases, testCase{
					testType: serverTest,
					protocol: protocol,
					name:     "Server-ALPNMissing-" + suffix,
					config: Config{
						MinVersion: ver.version,
						MaxVersion: ver.version,
					},
					skipQUICALPNConfig: true,
					shouldFail:         true,
					expectedError:      ":NO_APPLICATION_PROTOCOL:",
					expectedLocalError: "remote error: no application protocol",
				})
				testCases = append(testCases, testCase{
					testType: serverTest,
					protocol: protocol,
					name:     "Server-ALPNMismatch-" + suffix,
					config: Config{
						MinVersion: ver.version,
						MaxVersion: ver.version,
						NextProtos: []string{"foo"},
					},
					flags: []string{
						"-decline-alpn",
					},
					skipQUICALPNConfig: true,
					shouldFail:         true,
					expectedError:      ":NO_APPLICATION_PROTOCOL:",
					expectedLocalError: "remote error: no application protocol",
				})
			}

			// Test ALPS.
			if ver.version >= VersionTLS13 {
				// Test basic client with different ALPS codepoint.
				for _, alpsCodePoint := range []ALPSUseCodepoint{ALPSUseCodepointNew, ALPSUseCodepointOld} {
					useAlpsCodepointFlag := "0"
					if alpsCodePoint == ALPSUseCodepointNew {
						useAlpsCodepointFlag = "1"
					}

					flags := []string{"-alps-use-new-codepoint", useAlpsCodepointFlag}
					expectations := connectionExpectations{
						peerApplicationSettingsOld: []byte("shim1"),
					}
					resumeExpectations := &connectionExpectations{
						peerApplicationSettingsOld: []byte("shim2"),
					}

					if alpsCodePoint == ALPSUseCodepointNew {
						expectations = connectionExpectations{
							peerApplicationSettings: []byte("shim1"),
						}
						resumeExpectations = &connectionExpectations{
							peerApplicationSettings: []byte("shim2"),
						}
					}

					flags = append(flags,
						"-advertise-alpn", "\x05proto",
						"-expect-alpn", "proto",
						"-on-initial-application-settings", "proto,shim1",
						"-on-initial-expect-peer-application-settings", "runner1",
						"-on-resume-application-settings", "proto,shim2",
						"-on-resume-expect-peer-application-settings", "runner2")

					// Test that server can negotiate ALPS, including different values
					// on resumption.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           clientTest,
						name:               fmt.Sprintf("ALPS-Basic-Client-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeConfig: &Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner2")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeSession:      true,
						expectations:       expectations,
						resumeExpectations: resumeExpectations,
						flags:              flags,
					})

					// Test basic server with different ALPS codepoint.
					flags = []string{"-alps-use-new-codepoint", useAlpsCodepointFlag}
					expectations = connectionExpectations{
						peerApplicationSettingsOld: []byte("shim1"),
					}
					resumeExpectations = &connectionExpectations{
						peerApplicationSettingsOld: []byte("shim2"),
					}

					if alpsCodePoint == ALPSUseCodepointNew {
						expectations = connectionExpectations{
							peerApplicationSettings: []byte("shim1"),
						}
						resumeExpectations = &connectionExpectations{
							peerApplicationSettings: []byte("shim2"),
						}
					}

					flags = append(flags,
						"-select-alpn", "proto",
						"-on-initial-application-settings", "proto,shim1",
						"-on-initial-expect-peer-application-settings", "runner1",
						"-on-resume-application-settings", "proto,shim2",
						"-on-resume-expect-peer-application-settings", "runner2")

					// Test that server can negotiate ALPS, including different values
					// on resumption.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-Basic-Server-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeConfig: &Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner2")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeSession:      true,
						expectations:       expectations,
						resumeExpectations: resumeExpectations,
						flags:              flags,
					})

					// Try different ALPS codepoint for all the existing tests.
					alpsFlags := []string{"-alps-use-new-codepoint", useAlpsCodepointFlag}
					expectations = connectionExpectations{
						peerApplicationSettingsOld: []byte("shim1"),
					}
					resumeExpectations = &connectionExpectations{
						peerApplicationSettingsOld: []byte("shim2"),
					}
					if alpsCodePoint == ALPSUseCodepointNew {
						expectations = connectionExpectations{
							peerApplicationSettings: []byte("shim1"),
						}
						resumeExpectations = &connectionExpectations{
							peerApplicationSettings: []byte("shim2"),
						}
					}

					// Test that the server can defer its ALPS configuration to the ALPN
					// selection callback.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-Basic-Server-Defer-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeConfig: &Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner2")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeSession:      true,
						expectations:       expectations,
						resumeExpectations: resumeExpectations,
						flags: append([]string{
							"-select-alpn", "proto",
							"-defer-alps",
							"-on-initial-application-settings", "proto,shim1",
							"-on-initial-expect-peer-application-settings", "runner1",
							"-on-resume-application-settings", "proto,shim2",
							"-on-resume-expect-peer-application-settings", "runner2",
						}, alpsFlags...),
					})

					expectations = connectionExpectations{
						peerApplicationSettingsOld: []byte{},
					}
					if alpsCodePoint == ALPSUseCodepointNew {
						expectations = connectionExpectations{
							peerApplicationSettings: []byte{},
						}
					}
					// Test the client and server correctly handle empty settings.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           clientTest,
						name:               fmt.Sprintf("ALPS-Empty-Client-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": {}},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeSession: true,
						expectations:  expectations,
						flags: append([]string{
							"-advertise-alpn", "\x05proto",
							"-expect-alpn", "proto",
							"-application-settings", "proto,",
							"-expect-peer-application-settings", "",
						}, alpsFlags...),
					})
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-Empty-Server-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": {}},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeSession: true,
						expectations:  expectations,
						flags: append([]string{
							"-select-alpn", "proto",
							"-application-settings", "proto,",
							"-expect-peer-application-settings", "",
						}, alpsFlags...),
					})

					bugs := ProtocolBugs{
						AlwaysNegotiateApplicationSettingsOld: true,
					}
					if alpsCodePoint == ALPSUseCodepointNew {
						bugs = ProtocolBugs{
							AlwaysNegotiateApplicationSettingsNew: true,
						}
					}
					// Test the client rejects application settings from the server on
					// protocols it doesn't have them.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           clientTest,
						name:               fmt.Sprintf("ALPS-UnsupportedProtocol-Client-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto1"},
							ApplicationSettings: map[string][]byte{"proto1": []byte("runner")},
							Bugs:                bugs,
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						// The client supports ALPS with "proto2", but not "proto1".
						flags: append([]string{
							"-advertise-alpn", "\x06proto1\x06proto2",
							"-application-settings", "proto2,shim",
							"-expect-alpn", "proto1",
						}, alpsFlags...),
						// The server sends ALPS with "proto1", which is invalid.
						shouldFail:         true,
						expectedError:      ":INVALID_ALPN_PROTOCOL:",
						expectedLocalError: "remote error: illegal parameter",
					})

					// Test client rejects application settings from the server when
					// server sends the wrong ALPS codepoint.
					bugs = ProtocolBugs{
						AlwaysNegotiateApplicationSettingsOld: true,
					}
					if alpsCodePoint == ALPSUseCodepointOld {
						bugs = ProtocolBugs{
							AlwaysNegotiateApplicationSettingsNew: true,
						}
					}

					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           clientTest,
						name:               fmt.Sprintf("ALPS-WrongServerCodepoint-Client-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": {}},
							Bugs:                bugs,
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						flags: append([]string{
							"-advertise-alpn", "\x05proto",
							"-expect-alpn", "proto",
							"-application-settings", "proto,",
							"-expect-peer-application-settings", "",
						}, alpsFlags...),
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_EXTENSION:",
						expectedLocalError: "remote error: unsupported extension",
					})

					// Test server ignore wrong codepoint from client.
					clientSends := ALPSUseCodepointNew
					if alpsCodePoint == ALPSUseCodepointNew {
						clientSends = ALPSUseCodepointOld
					}

					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-IgnoreClientWrongCodepoint-Server-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
							ALPSUseNewCodepoint: clientSends,
						},
						resumeConfig: &Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner2")},
							ALPSUseNewCodepoint: clientSends,
						},
						resumeSession: true,
						flags: append([]string{
							"-select-alpn", "proto",
							"-on-initial-application-settings", "proto,shim1",
							"-on-resume-application-settings", "proto,shim2",
						}, alpsFlags...),
					})

					// Test the server declines ALPS if it doesn't support it for the
					// specified protocol.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-UnsupportedProtocol-Server-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto1"},
							ApplicationSettings: map[string][]byte{"proto1": []byte("runner")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						// The server supports ALPS with "proto2", but not "proto1".
						flags: append([]string{
							"-select-alpn", "proto1",
							"-application-settings", "proto2,shim",
						}, alpsFlags...),
					})

					// Test the client rejects application settings from the server when
					// it always negotiate both codepoint.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           clientTest,
						name:               fmt.Sprintf("ALPS-UnsupportedProtocol-Client-ServerBoth-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto1"},
							ApplicationSettings: map[string][]byte{"proto1": []byte("runner")},
							Bugs: ProtocolBugs{
								AlwaysNegotiateApplicationSettingsBoth: true,
							},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						flags: append([]string{
							"-advertise-alpn", "\x06proto1\x06proto2",
							"-application-settings", "proto1,shim",
							"-expect-alpn", "proto1",
						}, alpsFlags...),
						// The server sends ALPS with both application settings, which is invalid.
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_EXTENSION:",
						expectedLocalError: "remote error: unsupported extension",
					})

					expectations = connectionExpectations{
						peerApplicationSettingsOld: []byte("shim"),
					}
					if alpsCodePoint == ALPSUseCodepointNew {
						expectations = connectionExpectations{
							peerApplicationSettings: []byte("shim"),
						}
					}

					// Test that the server rejects a missing application_settings extension.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-OmitClientApplicationSettings-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
							Bugs: ProtocolBugs{
								OmitClientApplicationSettings: true,
							},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						flags: append([]string{
							"-select-alpn", "proto",
							"-application-settings", "proto,shim",
						}, alpsFlags...),
						// The runner is a client, so it only processes the shim's alert
						// after checking connection state.
						expectations:       expectations,
						shouldFail:         true,
						expectedError:      ":MISSING_EXTENSION:",
						expectedLocalError: "remote error: missing extension",
					})

					// Test that the server rejects a missing EncryptedExtensions message.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ALPS-OmitClientEncryptedExtensions-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
							Bugs: ProtocolBugs{
								OmitClientEncryptedExtensions: true,
							},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						flags: append([]string{
							"-select-alpn", "proto",
							"-application-settings", "proto,shim",
						}, alpsFlags...),
						// The runner is a client, so it only processes the shim's alert
						// after checking connection state.
						expectations:       expectations,
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_MESSAGE:",
						expectedLocalError: "remote error: unexpected message",
					})

					// Test that the server rejects an unexpected EncryptedExtensions message.
					testCases = append(testCases, testCase{
						protocol: protocol,
						testType: serverTest,
						name:     fmt.Sprintf("UnexpectedClientEncryptedExtensions-%s-%s", alpsCodePoint, suffix),
						config: Config{
							MaxVersion: ver.version,
							Bugs: ProtocolBugs{
								AlwaysSendClientEncryptedExtensions: true,
							},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_MESSAGE:",
						expectedLocalError: "remote error: unexpected message",
					})

					// Test that the server rejects an unexpected extension in an
					// expected EncryptedExtensions message.
					testCases = append(testCases, testCase{
						protocol:           protocol,
						testType:           serverTest,
						name:               fmt.Sprintf("ExtraClientEncryptedExtension-%s-%s", alpsCodePoint, suffix),
						skipQUICALPNConfig: true,
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"proto"},
							ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
							Bugs: ProtocolBugs{
								SendExtraClientEncryptedExtension: true,
							},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						flags: append([]string{
							"-select-alpn", "proto",
							"-application-settings", "proto,shim",
						}, alpsFlags...),
						// The runner is a client, so it only processes the shim's alert
						// after checking connection state.
						expectations:       expectations,
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_EXTENSION:",
						expectedLocalError: "remote error: unsupported extension",
					})

					// Test that ALPS is carried over on 0-RTT.
					// TODO(crbug.com/381113363): Support 0-RTT in DTLS 1.3.
					if protocol != dtls {
						for _, empty := range []bool{false, true} {
							maybeEmpty := ""
							runnerSettings := "runner"
							shimSettings := "shim"
							if empty {
								maybeEmpty = "Empty-"
								runnerSettings = ""
								shimSettings = ""
							}

							expectations = connectionExpectations{
								peerApplicationSettingsOld: []byte(shimSettings),
							}
							if alpsCodePoint == ALPSUseCodepointNew {
								expectations = connectionExpectations{
									peerApplicationSettings: []byte(shimSettings),
								}
							}
							testCases = append(testCases, testCase{
								protocol:           protocol,
								testType:           clientTest,
								name:               fmt.Sprintf("ALPS-EarlyData-Client-%s-%s-%s", alpsCodePoint, maybeEmpty, suffix),
								skipQUICALPNConfig: true,
								config: Config{
									MaxVersion:          ver.version,
									NextProtos:          []string{"proto"},
									ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
									ALPSUseNewCodepoint: alpsCodePoint,
								},
								resumeSession: true,
								earlyData:     true,
								flags: append([]string{
									"-advertise-alpn", "\x05proto",
									"-expect-alpn", "proto",
									"-application-settings", "proto," + shimSettings,
									"-expect-peer-application-settings", runnerSettings,
								}, alpsFlags...),
								expectations: expectations,
							})
							testCases = append(testCases, testCase{
								protocol:           protocol,
								testType:           serverTest,
								name:               fmt.Sprintf("ALPS-EarlyData-Server-%s-%s-%s", alpsCodePoint, maybeEmpty, suffix),
								skipQUICALPNConfig: true,
								config: Config{
									MaxVersion:          ver.version,
									NextProtos:          []string{"proto"},
									ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
									ALPSUseNewCodepoint: alpsCodePoint,
								},
								resumeSession: true,
								earlyData:     true,
								flags: append([]string{
									"-select-alpn", "proto",
									"-application-settings", "proto," + shimSettings,
									"-expect-peer-application-settings", runnerSettings,
								}, alpsFlags...),
								expectations: expectations,
							})

							// Sending application settings in 0-RTT handshakes is forbidden.
							testCases = append(testCases, testCase{
								protocol:           protocol,
								testType:           clientTest,
								name:               fmt.Sprintf("ALPS-EarlyData-SendApplicationSettingsWithEarlyData-Client-%s-%s-%s", alpsCodePoint, maybeEmpty, suffix),
								skipQUICALPNConfig: true,
								config: Config{
									MaxVersion:          ver.version,
									NextProtos:          []string{"proto"},
									ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
									Bugs: ProtocolBugs{
										SendApplicationSettingsWithEarlyData: true,
									},
									ALPSUseNewCodepoint: alpsCodePoint,
								},
								resumeSession: true,
								earlyData:     true,
								flags: append([]string{
									"-advertise-alpn", "\x05proto",
									"-expect-alpn", "proto",
									"-application-settings", "proto," + shimSettings,
									"-expect-peer-application-settings", runnerSettings,
								}, alpsFlags...),
								expectations:       expectations,
								shouldFail:         true,
								expectedError:      ":UNEXPECTED_EXTENSION_ON_EARLY_DATA:",
								expectedLocalError: "remote error: illegal parameter",
							})
							testCases = append(testCases, testCase{
								protocol:           protocol,
								testType:           serverTest,
								name:               fmt.Sprintf("ALPS-EarlyData-SendApplicationSettingsWithEarlyData-Server-%s-%s-%s", alpsCodePoint, maybeEmpty, suffix),
								skipQUICALPNConfig: true,
								config: Config{
									MaxVersion:          ver.version,
									NextProtos:          []string{"proto"},
									ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
									Bugs: ProtocolBugs{
										SendApplicationSettingsWithEarlyData: true,
									},
									ALPSUseNewCodepoint: alpsCodePoint,
								},
								resumeSession: true,
								earlyData:     true,
								flags: append([]string{
									"-select-alpn", "proto",
									"-application-settings", "proto," + shimSettings,
									"-expect-peer-application-settings", runnerSettings,
								}, alpsFlags...),
								expectations:       expectations,
								shouldFail:         true,
								expectedError:      ":UNEXPECTED_MESSAGE:",
								expectedLocalError: "remote error: unexpected message",
							})
						}

						// Test that the client and server each decline early data if local
						// ALPS preferences has changed for the current connection.
						alpsMismatchTests := []struct {
							name                            string
							initialSettings, resumeSettings []byte
						}{
							{"DifferentValues", []byte("settings1"), []byte("settings2")},
							{"OnOff", []byte("settings"), nil},
							{"OffOn", nil, []byte("settings")},
							// The empty settings value should not be mistaken for ALPS not
							// being negotiated.
							{"OnEmpty", []byte("settings"), []byte{}},
							{"EmptyOn", []byte{}, []byte("settings")},
							{"EmptyOff", []byte{}, nil},
							{"OffEmpty", nil, []byte{}},
						}
						for _, test := range alpsMismatchTests {
							flags := []string{"-on-resume-expect-early-data-reason", "alps_mismatch"}
							flags = append(flags, alpsFlags...)
							if test.initialSettings != nil {
								flags = append(flags, "-on-initial-application-settings", "proto,"+string(test.initialSettings))
								flags = append(flags, "-on-initial-expect-peer-application-settings", "runner")
							}
							if test.resumeSettings != nil {
								flags = append(flags, "-on-resume-application-settings", "proto,"+string(test.resumeSettings))
								flags = append(flags, "-on-resume-expect-peer-application-settings", "runner")
							}

							expectations = connectionExpectations{
								peerApplicationSettingsOld: test.initialSettings,
							}
							resumeExpectations = &connectionExpectations{
								peerApplicationSettingsOld: test.resumeSettings,
							}
							if alpsCodePoint == ALPSUseCodepointNew {
								expectations = connectionExpectations{
									peerApplicationSettings: test.initialSettings,
								}
								resumeExpectations = &connectionExpectations{
									peerApplicationSettings: test.resumeSettings,
								}
							}
							// The client should not offer early data if the session is
							// inconsistent with the new configuration. Note that if
							// the session did not negotiate ALPS (test.initialSettings
							// is nil), the client always offers early data.
							if test.initialSettings != nil {
								testCases = append(testCases, testCase{
									protocol:           protocol,
									testType:           clientTest,
									name:               fmt.Sprintf("ALPS-EarlyData-Mismatch-%s-Client-%s-%s", test.name, alpsCodePoint, suffix),
									skipQUICALPNConfig: true,
									config: Config{
										MaxVersion:          ver.version,
										MaxEarlyDataSize:    16384,
										NextProtos:          []string{"proto"},
										ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
										ALPSUseNewCodepoint: alpsCodePoint,
									},
									resumeSession: true,
									flags: append([]string{
										"-enable-early-data",
										"-expect-ticket-supports-early-data",
										"-expect-no-offer-early-data",
										"-advertise-alpn", "\x05proto",
										"-expect-alpn", "proto",
									}, flags...),
									expectations:       expectations,
									resumeExpectations: resumeExpectations,
								})
							}

							// The server should reject early data if the session is
							// inconsistent with the new selection.
							testCases = append(testCases, testCase{
								protocol:           protocol,
								testType:           serverTest,
								name:               fmt.Sprintf("ALPS-EarlyData-Mismatch-%s-Server-%s-%s", test.name, alpsCodePoint, suffix),
								skipQUICALPNConfig: true,
								config: Config{
									MaxVersion:          ver.version,
									NextProtos:          []string{"proto"},
									ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
									ALPSUseNewCodepoint: alpsCodePoint,
								},
								resumeSession:           true,
								earlyData:               true,
								expectEarlyDataRejected: true,
								flags: append([]string{
									"-select-alpn", "proto",
								}, flags...),
								expectations:       expectations,
								resumeExpectations: resumeExpectations,
							})
						}

						// Test that 0-RTT continues working when the shim configures
						// ALPS but the peer does not.
						testCases = append(testCases, testCase{
							protocol:           protocol,
							testType:           clientTest,
							name:               fmt.Sprintf("ALPS-EarlyData-Client-ServerDecline-%s-%s", alpsCodePoint, suffix),
							skipQUICALPNConfig: true,
							config: Config{
								MaxVersion:          ver.version,
								NextProtos:          []string{"proto"},
								ALPSUseNewCodepoint: alpsCodePoint,
							},
							resumeSession: true,
							earlyData:     true,
							flags: append([]string{
								"-advertise-alpn", "\x05proto",
								"-expect-alpn", "proto",
								"-application-settings", "proto,shim",
							}, alpsFlags...),
						})
						testCases = append(testCases, testCase{
							protocol:           protocol,
							testType:           serverTest,
							name:               fmt.Sprintf("ALPS-EarlyData-Server-ClientNoOffer-%s-%s", alpsCodePoint, suffix),
							skipQUICALPNConfig: true,
							config: Config{
								MaxVersion:          ver.version,
								NextProtos:          []string{"proto"},
								ALPSUseNewCodepoint: alpsCodePoint,
							},
							resumeSession: true,
							earlyData:     true,
							flags: append([]string{
								"-select-alpn", "proto",
								"-application-settings", "proto,shim",
							}, alpsFlags...),
						})
					}
				}
			} else {
				// Test the client rejects the ALPS extension if the server
				// negotiated TLS 1.2 or below.
				for _, alpsCodePoint := range []ALPSUseCodepoint{ALPSUseCodepointNew, ALPSUseCodepointOld} {
					useAlpsCodepointFlag := "0"
					if alpsCodePoint == ALPSUseCodepointNew {
						useAlpsCodepointFlag = "1"
					}

					flags := []string{
						"-advertise-alpn", "\x03foo",
						"-expect-alpn", "foo",
						"-application-settings", "foo,shim",
						"-alps-use-new-codepoint", useAlpsCodepointFlag,
					}
					bugs := ProtocolBugs{
						AlwaysNegotiateApplicationSettingsOld: true,
					}
					if alpsCodePoint == ALPSUseCodepointNew {
						bugs = ProtocolBugs{
							AlwaysNegotiateApplicationSettingsNew: true,
						}
					}
					testCases = append(testCases, testCase{
						protocol: protocol,
						testType: clientTest,
						name:     fmt.Sprintf("ALPS-Reject-Client-%s-%s", alpsCodePoint, suffix),
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"foo"},
							ApplicationSettings: map[string][]byte{"foo": []byte("runner")},
							Bugs:                bugs,
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						flags:              flags,
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_EXTENSION:",
						expectedLocalError: "remote error: unsupported extension",
					})

					flags = []string{
						"-on-resume-advertise-alpn", "\x03foo",
						"-on-resume-expect-alpn", "foo",
						"-on-resume-application-settings", "foo,shim",
						"-alps-use-new-codepoint", useAlpsCodepointFlag,
					}
					bugs = ProtocolBugs{
						AlwaysNegotiateApplicationSettingsOld: true,
					}
					if alpsCodePoint == ALPSUseCodepointNew {
						bugs = ProtocolBugs{
							AlwaysNegotiateApplicationSettingsNew: true,
						}
					}
					testCases = append(testCases, testCase{
						protocol: protocol,
						testType: clientTest,
						name:     fmt.Sprintf("ALPS-Reject-Client-Resume-%s-%s", alpsCodePoint, suffix),
						config: Config{
							MaxVersion: ver.version,
						},
						resumeConfig: &Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"foo"},
							ApplicationSettings: map[string][]byte{"foo": []byte("runner")},
							Bugs:                bugs,
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						resumeSession:      true,
						flags:              flags,
						shouldFail:         true,
						expectedError:      ":UNEXPECTED_EXTENSION:",
						expectedLocalError: "remote error: unsupported extension",
					})

					// Test the server declines ALPS if it negotiates TLS 1.2 or below.
					flags = []string{
						"-select-alpn", "foo",
						"-application-settings", "foo,shim",
						"-alps-use-new-codepoint", useAlpsCodepointFlag,
					}

					testCases = append(testCases, testCase{
						protocol: protocol,
						testType: serverTest,
						name:     fmt.Sprintf("ALPS-Decline-Server-%s-%s", alpsCodePoint, suffix),
						config: Config{
							MaxVersion:          ver.version,
							NextProtos:          []string{"foo"},
							ApplicationSettings: map[string][]byte{"foo": []byte("runner")},
							ALPSUseNewCodepoint: alpsCodePoint,
						},
						// Test both TLS 1.2 full and resumption handshakes.
						resumeSession: true,
						flags:         flags,
						// If not specified, runner and shim both implicitly expect ALPS
						// is not negotiated.
					})
				}
			}

			// Test QUIC transport params
			if protocol == quic {
				// Client sends params
				for _, clientConfig := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy} {
					for _, serverSends := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy, QUICUseCodepointBoth, QUICUseCodepointNeither} {
						useCodepointFlag := "0"
						if clientConfig == QUICUseCodepointLegacy {
							useCodepointFlag = "1"
						}
						flags := []string{
							"-quic-transport-params",
							base64FlagValue([]byte{1, 2}),
							"-quic-use-legacy-codepoint", useCodepointFlag,
						}
						expectations := connectionExpectations{
							quicTransportParams: []byte{1, 2},
						}
						shouldFail := false
						expectedError := ""
						expectedLocalError := ""
						if clientConfig == QUICUseCodepointLegacy {
							expectations = connectionExpectations{
								quicTransportParamsLegacy: []byte{1, 2},
							}
						}
						if serverSends != clientConfig {
							expectations = connectionExpectations{}
							shouldFail = true
							if serverSends == QUICUseCodepointNeither {
								expectedError = ":MISSING_EXTENSION:"
							} else {
								expectedLocalError = "remote error: unsupported extension"
							}
						} else {
							flags = append(flags,
								"-expect-quic-transport-params",
								base64FlagValue([]byte{3, 4}))
						}
						testCases = append(testCases, testCase{
							testType: clientTest,
							protocol: protocol,
							name:     fmt.Sprintf("QUICTransportParams-Client-Client%s-Server%s-%s", clientConfig, serverSends, suffix),
							config: Config{
								MinVersion:                            ver.version,
								MaxVersion:                            ver.version,
								QUICTransportParams:                   []byte{3, 4},
								QUICTransportParamsUseLegacyCodepoint: serverSends,
							},
							flags:                     flags,
							expectations:              expectations,
							shouldFail:                shouldFail,
							expectedError:             expectedError,
							expectedLocalError:        expectedLocalError,
							skipTransportParamsConfig: true,
						})
					}
				}
				// Server sends params
				for _, clientSends := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy, QUICUseCodepointBoth, QUICUseCodepointNeither} {
					for _, serverConfig := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy} {
						expectations := connectionExpectations{
							quicTransportParams: []byte{3, 4},
						}
						shouldFail := false
						expectedError := ""
						useCodepointFlag := "0"
						if serverConfig == QUICUseCodepointLegacy {
							useCodepointFlag = "1"
							expectations = connectionExpectations{
								quicTransportParamsLegacy: []byte{3, 4},
							}
						}
						flags := []string{
							"-quic-transport-params",
							base64FlagValue([]byte{3, 4}),
							"-quic-use-legacy-codepoint", useCodepointFlag,
						}
						if clientSends != QUICUseCodepointBoth && clientSends != serverConfig {
							expectations = connectionExpectations{}
							shouldFail = true
							expectedError = ":MISSING_EXTENSION:"
						} else {
							flags = append(flags,
								"-expect-quic-transport-params",
								base64FlagValue([]byte{1, 2}),
							)
						}
						testCases = append(testCases, testCase{
							testType: serverTest,
							protocol: protocol,
							name:     fmt.Sprintf("QUICTransportParams-Server-Client%s-Server%s-%s", clientSends, serverConfig, suffix),
							config: Config{
								MinVersion:                            ver.version,
								MaxVersion:                            ver.version,
								QUICTransportParams:                   []byte{1, 2},
								QUICTransportParamsUseLegacyCodepoint: clientSends,
							},
							flags:                     flags,
							expectations:              expectations,
							shouldFail:                shouldFail,
							expectedError:             expectedError,
							skipTransportParamsConfig: true,
						})
					}
				}
			} else {
				// Ensure non-QUIC client doesn't send QUIC transport parameters.
				for _, clientConfig := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy} {
					useCodepointFlag := "0"
					if clientConfig == QUICUseCodepointLegacy {
						useCodepointFlag = "1"
					}
					testCases = append(testCases, testCase{
						protocol: protocol,
						testType: clientTest,
						name:     fmt.Sprintf("QUICTransportParams-Client-NotSentInNonQUIC-%s-%s", clientConfig, suffix),
						config: Config{
							MinVersion:                            ver.version,
							MaxVersion:                            ver.version,
							QUICTransportParamsUseLegacyCodepoint: clientConfig,
						},
						flags: []string{
							"-max-version",
							ver.shimFlag(protocol),
							"-quic-transport-params",
							base64FlagValue([]byte{3, 4}),
							"-quic-use-legacy-codepoint", useCodepointFlag,
						},
						shouldFail:                true,
						expectedError:             ":QUIC_TRANSPORT_PARAMETERS_MISCONFIGURED:",
						skipTransportParamsConfig: true,
					})
				}
				// Ensure non-QUIC server rejects codepoint 57 but ignores legacy 0xffa5.
				for _, clientSends := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy, QUICUseCodepointBoth, QUICUseCodepointNeither} {
					for _, serverConfig := range []QUICUseCodepoint{QUICUseCodepointStandard, QUICUseCodepointLegacy} {
						shouldFail := false
						expectedLocalError := ""
						useCodepointFlag := "0"
						if serverConfig == QUICUseCodepointLegacy {
							useCodepointFlag = "1"
						}
						if clientSends == QUICUseCodepointStandard || clientSends == QUICUseCodepointBoth {
							shouldFail = true
							expectedLocalError = "remote error: unsupported extension"
						}
						testCases = append(testCases, testCase{
							protocol: protocol,
							testType: serverTest,
							name:     fmt.Sprintf("QUICTransportParams-NonQUICServer-Client%s-Server%s-%s", clientSends, serverConfig, suffix),
							config: Config{
								MinVersion:                            ver.version,
								MaxVersion:                            ver.version,
								QUICTransportParams:                   []byte{1, 2},
								QUICTransportParamsUseLegacyCodepoint: clientSends,
							},
							flags: []string{
								"-quic-use-legacy-codepoint", useCodepointFlag,
							},
							shouldFail:                shouldFail,
							expectedLocalError:        expectedLocalError,
							skipTransportParamsConfig: true,
						})
					}
				}

			}

			// Test ticket behavior.

			// Resume with a corrupt ticket.
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: serverTest,
				name:     "CorruptTicket-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						FilterTicket: func(in []byte) ([]byte, error) {
							in[len(in)-1] ^= 1
							return in, nil
						},
					},
				},
				resumeSession:        true,
				expectResumeRejected: true,
			})
			// Test the ticket callbacks.
			for _, aeadCallback := range []bool{false, true} {
				flag := "-use-ticket-callback"
				callbackSuffix := suffix
				if aeadCallback {
					flag = "-use-ticket-aead-callback"
					callbackSuffix += "-AEAD"
				}
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketCallback-" + callbackSuffix,
					config: Config{
						MaxVersion: ver.version,
					},
					resumeSession: true,
					flags:         []string{flag},
				})
				// Only the old callback supports renewal.
				if !aeadCallback {
					testCases = append(testCases, testCase{
						protocol: protocol,
						testType: serverTest,
						name:     "TicketCallback-Renew-" + callbackSuffix,
						config: Config{
							MaxVersion: ver.version,
							Bugs: ProtocolBugs{
								ExpectNewTicket: true,
							},
						},
						flags:         []string{flag, "-renew-ticket"},
						resumeSession: true,
					})
				}
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketCallback-Skip-" + callbackSuffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							ExpectNoNonEmptyNewSessionTicket: true,
						},
					},
					flags: []string{flag, "-skip-ticket"},
				})

				// Test that the ticket callback is only called once when everything before
				// it in the ClientHello is asynchronous. This corrupts the ticket so
				// certificate selection callbacks run.
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketCallback-SingleCall-" + callbackSuffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							FilterTicket: func(in []byte) ([]byte, error) {
								in[len(in)-1] ^= 1
								return in, nil
							},
						},
					},
					resumeSession:        true,
					expectResumeRejected: true,
					flags: []string{
						flag,
						"-async",
					},
				})
			}

			// Resume with various lengths of ticket session id.
			if ver.version < VersionTLS13 {
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketSessionIDLength-0-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							EmptyTicketSessionID: true,
						},
					},
					resumeSession: true,
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketSessionIDLength-16-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							TicketSessionIDLength: 16,
						},
					},
					resumeSession: true,
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketSessionIDLength-32-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							TicketSessionIDLength: 32,
						},
					},
					resumeSession: true,
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "TicketSessionIDLength-33-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							TicketSessionIDLength: 33,
						},
					},
					resumeSession: true,
					shouldFail:    true,
					// The maximum session ID length is 32.
					expectedError: ":CLIENTHELLO_PARSE_FAILED:",
				})
			}

			// Basic DTLS-SRTP tests. Include fake profiles to ensure they
			// are ignored.
			if protocol == dtls {
				testCases = append(testCases, testCase{
					protocol: protocol,
					name:     "SRTP-Client-" + suffix,
					config: Config{
						MaxVersion:             ver.version,
						SRTPProtectionProfiles: []uint16{40, SRTP_AES128_CM_HMAC_SHA1_80, 42},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
					},
					expectations: connectionExpectations{
						srtpProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
					},
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "SRTP-Server-" + suffix,
					config: Config{
						MaxVersion:             ver.version,
						SRTPProtectionProfiles: []uint16{40, SRTP_AES128_CM_HMAC_SHA1_80, 42},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
					},
					expectations: connectionExpectations{
						srtpProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
					},
				})
				// Test that the MKI is ignored.
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "SRTP-Server-IgnoreMKI-" + suffix,
					config: Config{
						MaxVersion:             ver.version,
						SRTPProtectionProfiles: []uint16{SRTP_AES128_CM_HMAC_SHA1_80},
						Bugs: ProtocolBugs{
							SRTPMasterKeyIdentifier: "bogus",
						},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
					},
					expectations: connectionExpectations{
						srtpProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_80,
					},
				})
				// Test that SRTP isn't negotiated on the server if there were
				// no matching profiles.
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "SRTP-Server-NoMatch-" + suffix,
					config: Config{
						MaxVersion:             ver.version,
						SRTPProtectionProfiles: []uint16{100, 101, 102},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
					},
					expectations: connectionExpectations{
						srtpProtectionProfile: 0,
					},
				})
				// Test that the server returning an invalid SRTP profile is
				// flagged as an error by the client.
				testCases = append(testCases, testCase{
					protocol: protocol,
					name:     "SRTP-Client-NoMatch-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							SendSRTPProtectionProfile: SRTP_AES128_CM_HMAC_SHA1_32,
						},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80",
					},
					shouldFail:    true,
					expectedError: ":BAD_SRTP_PROTECTION_PROFILE_LIST:",
				})
			} else {
				// DTLS-SRTP is not defined for other protocols. Configuring it
				// on the client and server should ignore the extension.
				testCases = append(testCases, testCase{
					protocol: protocol,
					name:     "SRTP-Client-Ignore-" + suffix,
					config: Config{
						MaxVersion:             ver.version,
						SRTPProtectionProfiles: []uint16{40, SRTP_AES128_CM_HMAC_SHA1_80, 42},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
					},
					expectations: connectionExpectations{
						srtpProtectionProfile: 0,
					},
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "SRTP-Server-Ignore-" + suffix,
					config: Config{
						MaxVersion:             ver.version,
						SRTPProtectionProfiles: []uint16{40, SRTP_AES128_CM_HMAC_SHA1_80, 42},
					},
					flags: []string{
						"-srtp-profiles",
						"SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32",
					},
					expectations: connectionExpectations{
						srtpProtectionProfile: 0,
					},
				})
			}

			// Test SCT list.
			testCases = append(testCases, testCase{
				protocol: protocol,
				name:     "SignedCertificateTimestampList-Client-" + suffix,
				testType: clientTest,
				config: Config{
					MaxVersion: ver.version,
					Credential: rsaCertificate.WithSCTList(testSCTList),
				},
				flags: []string{
					"-enable-signed-cert-timestamps",
					"-expect-signed-cert-timestamps",
					base64FlagValue(testSCTList),
				},
				resumeSession: true,
			})

			var differentSCTList []byte
			differentSCTList = append(differentSCTList, testSCTList...)
			differentSCTList[len(differentSCTList)-1] ^= 1

			// The SCT extension did not specify that it must only be sent on the initial handshake as it
			// should have, so test that we tolerate but ignore it. This is only an issue pre-1.3, since
			// SCTs are sent in the CertificateEntry message in 1.3, whereas they were previously sent
			// in an extension in the ServerHello pre-1.3.
			testCases = append(testCases, testCase{
				protocol: protocol,
				name:     "SendSCTListOnResume-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Credential: rsaCertificate.WithSCTList(testSCTList),
					Bugs: ProtocolBugs{
						SendSCTListOnResume: differentSCTList,
					},
				},
				flags: []string{
					"-enable-signed-cert-timestamps",
					"-expect-signed-cert-timestamps",
					base64FlagValue(testSCTList),
				},
				resumeSession: true,
			})

			testCases = append(testCases, testCase{
				protocol: protocol,
				name:     "SignedCertificateTimestampList-Server-" + suffix,
				testType: serverTest,
				config: Config{
					MaxVersion: ver.version,
				},
				shimCertificate: rsaCertificate.WithSCTList(testSCTList),
				expectations: connectionExpectations{
					peerCertificate: rsaCertificate.WithSCTList(testSCTList),
				},
				resumeSession: true,
			})

			// Test empty SCT list.
			testCases = append(testCases, testCase{
				protocol: protocol,
				name:     "SignedCertificateTimestampListEmpty-Client-" + suffix,
				testType: clientTest,
				config: Config{
					MaxVersion: ver.version,
					Credential: rsaCertificate.WithSCTList([]byte{0, 0}),
				},
				flags: []string{
					"-enable-signed-cert-timestamps",
				},
				shouldFail:    true,
				expectedError: ":ERROR_PARSING_EXTENSION:",
			})

			// Test empty SCT in non-empty list.
			testCases = append(testCases, testCase{
				protocol: protocol,
				name:     "SignedCertificateTimestampListEmptySCT-Client-" + suffix,
				testType: clientTest,
				config: Config{
					MaxVersion: ver.version,
					Credential: rsaCertificate.WithSCTList([]byte{0, 6, 0, 2, 1, 2, 0, 0}),
				},
				flags: []string{
					"-enable-signed-cert-timestamps",
				},
				shouldFail:    true,
				expectedError: ":ERROR_PARSING_EXTENSION:",
			})

			// Test that certificate-related extensions are not sent unsolicited.
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: serverTest,
				name:     "UnsolicitedCertificateExtensions-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						NoOCSPStapling:                true,
						NoSignedCertificateTimestamps: true,
					},
				},
				shimCertificate: rsaCertificate.WithOCSP(testOCSPResponse).WithSCTList(testSCTList),
			})

			// The client should reject empty OCSP responses from the server. A server
			// with no OCSP response should not send the status_request extension.
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "RejectEmptyOCSPResponse-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Credential: rsaCertificate.WithOCSP([]byte{}),
				},
				flags:              []string{"-enable-ocsp-stapling"},
				shouldFail:         true,
				expectedError:      ":DECODE_ERROR:",
				expectedLocalError: "remote error: error decoding message",
			})

			// The certificate authorities extension cannot be empty. The sender should
			// have omitted if empty.
			if ver.version >= VersionTLS13 {
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: clientTest,
					name:     "RejectEmptyCertificateAuthorities-Client-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						ClientAuth: RequireAnyClientCert,
						Bugs: ProtocolBugs{
							SendEmptyCertificateAuthorities: true,
						},
					},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "RejectEmptyCertificateAuthorities-Server-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							SendEmptyCertificateAuthorities: true,
						},
					},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})
			}

			// Extension permutation should interact correctly with other extensions,
			// HelloVerifyRequest, HelloRetryRequest, and ECH. SSLTest.PermuteExtensions
			// in ssl_test.cc tests that the extensions are actually permuted. This
			// tests the handshake still works.
			//
			// This test also tests that all our extensions interact with each other.
			for _, ech := range []bool{false, true} {
				if ech && ver.version < VersionTLS13 {
					continue
				}

				test := testCase{
					protocol:           protocol,
					name:               "AllExtensions-Client-Permute",
					skipQUICALPNConfig: true,
					config: Config{
						MinVersion:          ver.version,
						MaxVersion:          ver.version,
						Credential:          rsaCertificate.WithOCSP(testOCSPResponse).WithSCTList(testSCTList),
						NextProtos:          []string{"proto"},
						ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
						Bugs: ProtocolBugs{
							SendServerNameAck: true,
							ExpectServerName:  "example.com",
							ExpectGREASE:      true,
						},
					},
					resumeSession: true,
					flags: []string{
						"-permute-extensions",
						"-enable-grease",
						"-enable-ocsp-stapling",
						"-enable-signed-cert-timestamps",
						"-advertise-alpn", "\x05proto",
						"-expect-alpn", "proto",
						"-host-name", "example.com",
					},
				}

				if ech {
					test.name += "-ECH"
					echConfig := generateServerECHConfig(&ECHConfig{ConfigID: 42})
					test.config.ServerECHConfigs = []ServerECHConfig{echConfig}
					test.flags = append(test.flags,
						"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
						"-expect-ech-accept",
					)
					test.expectations.echAccepted = true
				}

				if ver.version >= VersionTLS13 {
					// Trigger a HelloRetryRequest to test both ClientHellos. Note
					// our DTLS tests always enable HelloVerifyRequest.
					test.name += "-HelloRetryRequest"

					// ALPS is only available on TLS 1.3.
					test.config.ApplicationSettings = map[string][]byte{"proto": []byte("runner")}
					test.flags = append(test.flags,
						"-application-settings", "proto,shim",
						"-alps-use-new-codepoint", "1",
						"-expect-peer-application-settings", "runner")
					test.expectations.peerApplicationSettings = []byte("shim")
				}

				if protocol == dtls {
					test.config.SRTPProtectionProfiles = []uint16{SRTP_AES128_CM_HMAC_SHA1_80}
					test.flags = append(test.flags, "-srtp-profiles", "SRTP_AES128_CM_SHA1_80")
					test.expectations.srtpProtectionProfile = SRTP_AES128_CM_HMAC_SHA1_80
				}

				test.name += "-" + suffix
				testCases = append(testCases, test)
			}
		}
	}

	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "ClientHelloPadding",
		config: Config{
			Bugs: ProtocolBugs{
				RequireClientHelloSize: 512,
			},
		},
		// This hostname just needs to be long enough to push the
		// ClientHello into F5's danger zone between 256 and 511 bytes
		// long.
		flags: []string{"-host-name", "01234567890123456789012345678901234567890123456789012345678901234567890123456789.com"},
	})

	// Test that illegal extensions in TLS 1.3 are rejected by the client if
	// in ServerHello.
	testCases = append(testCases, testCase{
		name: "NPN-Forbidden-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			NextProtos: []string{"foo"},
			Bugs: ProtocolBugs{
				NegotiateNPNAtAllVersions: true,
			},
		},
		flags:         []string{"-select-next-proto", "foo"},
		shouldFail:    true,
		expectedError: ":ERROR_PARSING_EXTENSION:",
	})
	testCases = append(testCases, testCase{
		name: "EMS-Forbidden-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				NegotiateEMSAtAllVersions: true,
			},
		},
		shouldFail:    true,
		expectedError: ":ERROR_PARSING_EXTENSION:",
	})
	testCases = append(testCases, testCase{
		name: "RenegotiationInfo-Forbidden-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				NegotiateRenegotiationInfoAtAllVersions: true,
			},
		},
		shouldFail:    true,
		expectedError: ":ERROR_PARSING_EXTENSION:",
	})
	testCases = append(testCases, testCase{
		name: "Ticket-Forbidden-TLS13",
		config: Config{
			MaxVersion: VersionTLS12,
		},
		resumeConfig: &Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				AdvertiseTicketExtension: true,
			},
		},
		resumeSession: true,
		shouldFail:    true,
		expectedError: ":ERROR_PARSING_EXTENSION:",
	})

	// Test that illegal extensions in TLS 1.3 are declined by the server if
	// offered in ClientHello. The runner's server will fail if this occurs,
	// so we exercise the offering path. (EMS and Renegotiation Info are
	// implicit in every test.)
	testCases = append(testCases, testCase{
		testType: serverTest,
		name:     "NPN-Declined-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			NextProtos: []string{"bar"},
		},
		flags: []string{"-advertise-npn", "\x03foo\x03bar\x03baz"},
	})

	// OpenSSL sends the status_request extension on resumption in TLS 1.2. Test that this is
	// tolerated.
	testCases = append(testCases, testCase{
		name: "SendOCSPResponseOnResume-TLS12",
		config: Config{
			MaxVersion: VersionTLS12,
			Credential: rsaCertificate.WithOCSP(testOCSPResponse),
			Bugs: ProtocolBugs{
				SendOCSPResponseOnResume: []byte("bogus"),
			},
		},
		flags: []string{
			"-enable-ocsp-stapling",
			"-expect-ocsp-response",
			base64FlagValue(testOCSPResponse),
		},
		resumeSession: true,
	})

	testCases = append(testCases, testCase{
		name: "SendUnsolicitedOCSPOnCertificate-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				SendExtensionOnCertificate: testOCSPExtension,
			},
		},
		shouldFail:    true,
		expectedError: ":UNEXPECTED_EXTENSION:",
	})

	testCases = append(testCases, testCase{
		name: "SendUnsolicitedSCTOnCertificate-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				SendExtensionOnCertificate: testSCTExtension,
			},
		},
		shouldFail:    true,
		expectedError: ":UNEXPECTED_EXTENSION:",
	})

	// Test that extensions on client certificates are never accepted.
	testCases = append(testCases, testCase{
		name:     "SendExtensionOnClientCertificate-TLS13",
		testType: serverTest,
		config: Config{
			MaxVersion: VersionTLS13,
			Credential: &rsaCertificate,
			Bugs: ProtocolBugs{
				SendExtensionOnCertificate: testOCSPExtension,
			},
		},
		flags: []string{
			"-enable-ocsp-stapling",
			"-require-any-client-certificate",
		},
		shouldFail:    true,
		expectedError: ":UNEXPECTED_EXTENSION:",
	})

	testCases = append(testCases, testCase{
		name: "SendUnknownExtensionOnCertificate-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				SendExtensionOnCertificate: []byte{0x00, 0x7f, 0, 0},
			},
		},
		shouldFail:    true,
		expectedError: ":UNEXPECTED_EXTENSION:",
	})

	// Test that extensions on intermediates are allowed but ignored.
	testCases = append(testCases, testCase{
		name: "IgnoreExtensionsOnIntermediates-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Credential: rsaChainCertificate.WithOCSP(testOCSPResponse).WithSCTList(testSCTList),
			Bugs: ProtocolBugs{
				// Send different values on the intermediate. This tests
				// the intermediate's extensions do not override the
				// leaf's.
				SendOCSPOnIntermediates: testOCSPResponse2,
				SendSCTOnIntermediates:  testSCTList2,
			},
		},
		flags: []string{
			"-enable-ocsp-stapling",
			"-expect-ocsp-response",
			base64FlagValue(testOCSPResponse),
			"-enable-signed-cert-timestamps",
			"-expect-signed-cert-timestamps",
			base64FlagValue(testSCTList),
		},
		resumeSession: true,
	})

	// Test that extensions are not sent on intermediates when configured
	// only for a leaf.
	testCases = append(testCases, testCase{
		testType: serverTest,
		name:     "SendNoExtensionsOnIntermediate-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				ExpectNoExtensionsOnIntermediate: true,
			},
		},
		shimCertificate: rsaChainCertificate.WithOCSP(testOCSPResponse).WithSCTList(testSCTList),
	})

	// Test that extensions are not sent on client certificates.
	testCases = append(testCases, testCase{
		name: "SendNoClientCertificateExtensions-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			ClientAuth: RequireAnyClientCert,
		},
		shimCertificate: rsaChainCertificate.WithOCSP(testOCSPResponse).WithSCTList(testSCTList),
	})

	testCases = append(testCases, testCase{
		name: "SendDuplicateExtensionsOnCerts-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Credential: rsaCertificate.WithOCSP(testOCSPResponse).WithSCTList(testSCTList),
			Bugs: ProtocolBugs{
				SendDuplicateCertExtensions: true,
			},
		},
		flags: []string{
			"-enable-ocsp-stapling",
			"-enable-signed-cert-timestamps",
		},
		resumeSession: true,
		shouldFail:    true,
		expectedError: ":DUPLICATE_EXTENSION:",
	})

	testCases = append(testCases, testCase{
		name:            "SignedCertificateTimestampListInvalid-Server",
		testType:        serverTest,
		shimCertificate: rsaCertificate.WithSCTList([]byte{0, 0}),
		shouldFail:      true,
		expectedError:   ":INVALID_SCT_LIST:",
	})
}

func addUnknownExtensionTests() {
	// Test an unknown extension from the server.
	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "UnknownExtension-Client",
		config: Config{
			MaxVersion: VersionTLS12,
			Bugs: ProtocolBugs{
				CustomExtension: "custom extension",
			},
		},
		shouldFail:         true,
		expectedError:      ":UNEXPECTED_EXTENSION:",
		expectedLocalError: "remote error: unsupported extension",
	})
	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "UnknownExtension-Client-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				CustomExtension: "custom extension",
			},
		},
		shouldFail:         true,
		expectedError:      ":UNEXPECTED_EXTENSION:",
		expectedLocalError: "remote error: unsupported extension",
	})
	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "UnknownUnencryptedExtension-Client-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				CustomUnencryptedExtension: "custom extension",
			},
		},
		shouldFail:    true,
		expectedError: ":UNEXPECTED_EXTENSION:",
		// The shim must send an alert, but alerts at this point do not
		// get successfully decrypted by the runner.
		expectedLocalError: "local error: bad record MAC",
	})
	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "UnexpectedUnencryptedExtension-Client-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				SendUnencryptedALPN: "foo",
			},
		},
		flags: []string{
			"-advertise-alpn", "\x03foo\x03bar",
		},
		shouldFail:    true,
		expectedError: ":UNEXPECTED_EXTENSION:",
		// The shim must send an alert, but alerts at this point do not
		// get successfully decrypted by the runner.
		expectedLocalError: "local error: bad record MAC",
	})

	// Test a known but unoffered extension from the server.
	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "UnofferedExtension-Client",
		config: Config{
			MaxVersion: VersionTLS12,
			Bugs: ProtocolBugs{
				SendALPN: "alpn",
			},
		},
		shouldFail:         true,
		expectedError:      ":UNEXPECTED_EXTENSION:",
		expectedLocalError: "remote error: unsupported extension",
	})
	testCases = append(testCases, testCase{
		testType: clientTest,
		name:     "UnofferedExtension-Client-TLS13",
		config: Config{
			MaxVersion: VersionTLS13,
			Bugs: ProtocolBugs{
				SendALPN: "alpn",
			},
		},
		shouldFail:         true,
		expectedError:      ":UNEXPECTED_EXTENSION:",
		expectedLocalError: "remote error: unsupported extension",
	})
}

// Test that omitted and empty extensions blocks are tolerated.
func addOmitExtensionsTests() {
	// Check the ExpectOmitExtensions setting works.
	testCases = append(testCases, testCase{
		testType: serverTest,
		name:     "ExpectOmitExtensions",
		config: Config{
			MinVersion: VersionTLS12,
			MaxVersion: VersionTLS12,
			Bugs: ProtocolBugs{
				ExpectOmitExtensions: true,
			},
		},
		shouldFail:         true,
		expectedLocalError: "tls: ServerHello did not omit extensions",
	})

	for _, ver := range tlsVersions {
		if ver.version > VersionTLS12 {
			continue
		}

		testCases = append(testCases, testCase{
			testType: serverTest,
			name:     "OmitExtensions-ClientHello-" + ver.name,
			config: Config{
				MinVersion:             ver.version,
				MaxVersion:             ver.version,
				SessionTicketsDisabled: true,
				Bugs: ProtocolBugs{
					OmitExtensions: true,
					// With no client extensions, the ServerHello must not have
					// extensions. It should then omit the extensions field.
					ExpectOmitExtensions: true,
				},
			},
		})

		testCases = append(testCases, testCase{
			testType: serverTest,
			name:     "EmptyExtensions-ClientHello-" + ver.name,
			config: Config{
				MinVersion:             ver.version,
				MaxVersion:             ver.version,
				SessionTicketsDisabled: true,
				Bugs: ProtocolBugs{
					EmptyExtensions: true,
					// With no client extensions, the ServerHello must not have
					// extensions. It should then omit the extensions field.
					ExpectOmitExtensions: true,
				},
			},
		})

		testCases = append(testCases, testCase{
			testType: clientTest,
			name:     "OmitExtensions-ServerHello-" + ver.name,
			config: Config{
				MinVersion:             ver.version,
				MaxVersion:             ver.version,
				SessionTicketsDisabled: true,
				Bugs: ProtocolBugs{
					OmitExtensions: true,
					// Disable all ServerHello extensions so
					// OmitExtensions works.
					NoExtendedMasterSecret:        true,
					NoRenegotiationInfo:           true,
					NoOCSPStapling:                true,
					NoSignedCertificateTimestamps: true,
				},
			},
		})

		testCases = append(testCases, testCase{
			testType: clientTest,
			name:     "EmptyExtensions-ServerHello-" + ver.name,
			config: Config{
				MinVersion:             ver.version,
				MaxVersion:             ver.version,
				SessionTicketsDisabled: true,
				Bugs: ProtocolBugs{
					EmptyExtensions: true,
					// Disable all ServerHello extensions so
					// EmptyExtensions works.
					NoExtendedMasterSecret:        true,
					NoRenegotiationInfo:           true,
					NoOCSPStapling:                true,
					NoSignedCertificateTimestamps: true,
				},
			},
		})
	}
}

// Test that our extension parsers consistently reject trailing data.
// TODO(crbug.com/505803427): Test other extensions.
func addExtensionTrailingDataTests() {
	for _, protocol := range []protocol{tls, dtls, quic} {
		for _, ver := range allVersions(protocol) {
			suffix := fmt.Sprintf("%s-%s", protocol.String(), ver.name)

			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "ExtensionTrailingData-ServerName-Client-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Bugs: ProtocolBugs{
						SendServerNameAck:          true,
						ExtensionsWithTrailingData: []uint16{extensionServerName},
					},
				},
				flags:              []string{"-host-name", "example.com"},
				shouldFail:         true,
				expectedError:      ":ERROR_PARSING_EXTENSION:",
				expectedLocalError: "remote error: error decoding message",
			})
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: serverTest,
				name:     "ExtensionTrailingData-ServerName-Server-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					ServerName: "example.com",
					Bugs: ProtocolBugs{
						ExtensionsWithTrailingData: []uint16{extensionServerName},
					},
				},
				shouldFail:         true,
				expectedError:      ":ERROR_PARSING_EXTENSION:",
				expectedLocalError: "remote error: error decoding message",
			})

			ocspError := ":ERROR_PARSING_EXTENSION:"
			if ver.version >= VersionTLS13 {
				ocspError = ":DECODE_ERROR:"
			}
			testCases = append(testCases, testCase{
				protocol: protocol,
				testType: clientTest,
				name:     "ExtensionTrailingData-StatusRequest-Client-" + suffix,
				config: Config{
					MaxVersion: ver.version,
					Credential: rsaCertificate.WithOCSP(testOCSPResponse),
					Bugs: ProtocolBugs{
						ExtensionsWithTrailingData: []uint16{extensionStatusRequest},
					},
				},
				flags:              []string{"-enable-ocsp-stapling"},
				shouldFail:         true,
				expectedError:      ocspError,
				expectedLocalError: "remote error: error decoding message",
			})
			// Skip the server test. BoringSSL does not currently parse most of the
			// ClientHello status_request extension.

			if ver.version >= VersionTLS13 {
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: clientTest,
					name:     "ExtensionTrailingData-CertificateAuthorities-Client-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						ClientAuth: RequireAnyClientCert,
						ClientCAs:  makeCertPoolFromRoots(&rsaChainCertificate, &ecdsaP384Certificate),
						Bugs: ProtocolBugs{
							ExtensionsWithTrailingData: []uint16{extensionCertificateAuthorities},
						},
					},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "ExtensionTrailingData-CertificateAuthorities-Server-" + suffix,
					config: Config{
						MaxVersion:  ver.version,
						RootCAs:     makeCertPoolFromRoots(&rsaChainCertificate, &ecdsaP384Certificate),
						SendRootCAs: true,
						Bugs: ProtocolBugs{
							ExtensionsWithTrailingData: []uint16{extensionCertificateAuthorities},
						},
					},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})

				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: clientTest,
					name:     "ExtensionTrailingData-TrustAnchors-EncryptedExtensions-Client-" + suffix,
					config: Config{
						MaxVersion:            ver.version,
						AvailableTrustAnchors: [][]byte{{1}},
						Bugs: ProtocolBugs{
							ExtensionsWithTrailingData: []uint16{extensionTrustAnchors},
						},
					},
					flags:              []string{"-requested-trust-anchors", trustAnchorListFlagValue([]byte{2})},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: clientTest,
					name:     "ExtensionTrailingData-TrustAnchors-Certificate-Client-" + suffix,
					config: Config{
						MaxVersion: ver.version,
						Bugs: ProtocolBugs{
							AlwaysMatchTrustAnchorID:   true,
							ExtensionsWithTrailingData: []uint16{extensionTrustAnchors},
						},
					},
					flags:              []string{"-requested-trust-anchors", trustAnchorListFlagValue([]byte{2})},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})
				testCases = append(testCases, testCase{
					protocol: protocol,
					testType: serverTest,
					name:     "ExtensionTrailingData-TrustAnchors-ClientHello-Server-" + suffix,
					config: Config{
						MaxVersion:          ver.version,
						RequestTrustAnchors: [][]byte{{1}},
						Bugs: ProtocolBugs{
							ExtensionsWithTrailingData: []uint16{extensionTrustAnchors},
						},
					},
					shouldFail:         true,
					expectedError:      ":ERROR_PARSING_EXTENSION:",
					expectedLocalError: "remote error: error decoding message",
				})
			}
		}
	}
}
