// 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 (
	"strconv"
	"strings"
	"time"

	"boringssl.googlesource.com/boringssl.git/ssl/test/runner/hpke"
)

type echCipher struct {
	name   string
	cipher HPKECipherSuite
}

var echCiphers = []echCipher{
	{
		name:   "HKDF-SHA256-AES-128-GCM",
		cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES128GCM},
	},
	{
		name:   "HKDF-SHA256-AES-256-GCM",
		cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES256GCM},
	},
	{
		name:   "HKDF-SHA256-ChaCha20-Poly1305",
		cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.ChaCha20Poly1305},
	},
}

// generateServerECHConfig constructs a ServerECHConfig with a fresh X25519
// keypair and using |template| as a template for the ECHConfig. If fields are
// omitted, defaults are used.
func generateServerECHConfig(template *ECHConfig) ServerECHConfig {
	publicKey, secretKey, err := hpke.GenerateKeyPairX25519()
	if err != nil {
		panic(err)
	}
	templateCopy := *template
	if templateCopy.KEM == 0 {
		templateCopy.KEM = hpke.X25519WithHKDFSHA256
	}
	if len(templateCopy.PublicKey) == 0 {
		templateCopy.PublicKey = publicKey
	}
	if len(templateCopy.CipherSuites) == 0 {
		templateCopy.CipherSuites = make([]HPKECipherSuite, len(echCiphers))
		for i, cipher := range echCiphers {
			templateCopy.CipherSuites[i] = cipher.cipher
		}
	}
	if len(templateCopy.PublicName) == 0 {
		templateCopy.PublicName = "public.example"
	}
	if templateCopy.MaxNameLen == 0 {
		templateCopy.MaxNameLen = 64
	}
	return ServerECHConfig{ECHConfig: CreateECHConfig(&templateCopy), Key: secretKey}
}

func addEncryptedClientHelloTests() {
	// echConfig's ConfigID should match the one used in ssl/test/fuzzer.h.
	echConfig := generateServerECHConfig(&ECHConfig{ConfigID: 42})
	echConfig1 := generateServerECHConfig(&ECHConfig{ConfigID: 43})
	echConfig2 := generateServerECHConfig(&ECHConfig{ConfigID: 44})
	echConfig3 := generateServerECHConfig(&ECHConfig{ConfigID: 45})
	echConfigRepeatID := generateServerECHConfig(&ECHConfig{ConfigID: 42})

	echSecretCertificate := rootCA.Issue(X509Info{
		PrivateKey: &rsa2048Key,
		DNSNames:   []string{"secret.example"},
	}).ToCredential()
	echPublicCertificate := rootCA.Issue(X509Info{
		PrivateKey: &rsa2048Key,
		DNSNames:   []string{"public.example"},
	}).ToCredential()
	echLongNameCertificate := rootCA.Issue(X509Info{
		PrivateKey: &ecdsaP256Key,
		DNSNames:   []string{"test0123456789.example"},
	}).ToCredential()

	for _, protocol := range []protocol{tls, quic, dtls} {
		prefix := protocol.String() + "-"

		// There are two ClientHellos, so many of our tests have
		// HelloRetryRequest variations.
		for _, hrr := range []bool{false, true} {
			var suffix string
			var defaultCurves []CurveID
			if hrr {
				suffix = "-HelloRetryRequest"
				// Require a HelloRetryRequest for every curve.
				defaultCurves = []CurveID{}
			}

			// Test the server can accept ECH.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server" + suffix,
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
					DefaultCurves:   defaultCurves,
				},
				resumeSession: true,
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})

			// Test the server can accept ECH with a minimal ClientHelloOuter.
			// This confirms that the server does not unexpectedly pick up
			// fields from the wrong ClientHello.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-MinimalClientHelloOuter" + suffix,
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
					DefaultCurves:   defaultCurves,
					Bugs: ProtocolBugs{
						MinimalClientHelloOuter: true,
					},
				},
				resumeSession: true,
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})

			// Test that the server can decline ECH. In particular, it must send
			// retry configs.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-Decline" + suffix,
				config: Config{
					ServerName:    "secret.example",
					DefaultCurves: defaultCurves,
					// The client uses an ECHConfig that the server does not understand
					// so we can observe which retry configs the server sends back.
					ClientECHConfig: echConfig.ECHConfig,
					Bugs: ProtocolBugs{
						OfferSessionInClientHelloOuter: true,
						ExpectECHRetryConfigs:          CreateECHConfigList(echConfig2.ECHConfig.Raw, echConfig3.ECHConfig.Raw),
					},
				},
				resumeSession: true,
				flags: []string{
					// Configure three ECHConfigs on the shim, only two of which
					// should be sent in retry configs.
					"-ech-server-config", base64FlagValue(echConfig1.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig1.Key),
					"-ech-is-retry-config", "0",
					"-ech-server-config", base64FlagValue(echConfig2.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig2.Key),
					"-ech-is-retry-config", "1",
					"-ech-server-config", base64FlagValue(echConfig3.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig3.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "public.example",
				},
			})

			// Test that the server considers a ClientHelloInner indicating TLS
			// 1.2 to be a fatal error.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-TLS12InInner" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					Bugs: ProtocolBugs{
						AllowTLS12InClientHelloInner: true,
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: illegal parameter",
				expectedError:      ":INVALID_CLIENT_HELLO_INNER:",
			})

			// When inner ECH extension is absent from the ClientHelloInner, the
			// server should fail the connection.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-MissingECHInner" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					Bugs: ProtocolBugs{
						OmitECHInner:       !hrr,
						OmitSecondECHInner: hrr,
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: illegal parameter",
				expectedError:      ":INVALID_CLIENT_HELLO_INNER:",
			})

			// Test that the server can decode ech_outer_extensions.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-OuterExtensions" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					ECHOuterExtensions: []uint16{
						extensionKeyShare,
						extensionSupportedCurves,
						// Include a custom extension, to test that unrecognized
						// extensions are also decoded.
						extensionCustom,
					},
					Bugs: ProtocolBugs{
						CustomExtension:                    "test",
						OnlyCompressSecondClientHelloInner: hrr,
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})

			// Test that the server allows referenced ClientHelloOuter
			// extensions to be interleaved with other extensions. Only the
			// relative order must match.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-OuterExtensions-Interleaved" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					ECHOuterExtensions: []uint16{
						extensionKeyShare,
						extensionSupportedCurves,
						extensionCustom,
					},
					Bugs: ProtocolBugs{
						CustomExtension:                    "test",
						OnlyCompressSecondClientHelloInner: hrr,
						ECHOuterExtensionOrder: []uint16{
							extensionServerName,
							extensionKeyShare,
							extensionSupportedVersions,
							extensionPSKKeyExchangeModes,
							extensionSupportedCurves,
							extensionSignatureAlgorithms,
							extensionCustom,
						},
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})

			// Test that the server rejects references to extensions in the
			// wrong order.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-OuterExtensions-WrongOrder" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					ECHOuterExtensions: []uint16{
						extensionKeyShare,
						extensionSupportedCurves,
					},
					Bugs: ProtocolBugs{
						CustomExtension:                    "test",
						OnlyCompressSecondClientHelloInner: hrr,
						ECHOuterExtensionOrder: []uint16{
							extensionSupportedCurves,
							extensionKeyShare,
						},
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: illegal parameter",
				expectedError:      ":INVALID_OUTER_EXTENSION:",
			})

			// Test that the server rejects duplicated values in ech_outer_extensions.
			// Besides causing the server to reconstruct an invalid ClientHelloInner
			// with duplicated extensions, this behavior would be vulnerable to DoS
			// attacks.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-OuterExtensions-Duplicate" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					ECHOuterExtensions: []uint16{
						extensionSupportedCurves,
						extensionSupportedCurves,
					},
					Bugs: ProtocolBugs{
						OnlyCompressSecondClientHelloInner: hrr,
						// Don't duplicate the extension in ClientHelloOuter.
						ECHOuterExtensionOrder: []uint16{
							extensionSupportedCurves,
						},
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: illegal parameter",
				expectedError:      ":INVALID_OUTER_EXTENSION:",
			})

			// Test that the server rejects references to missing extensions in
			// ech_outer_extensions.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-OuterExtensions-Missing" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					ECHOuterExtensions: []uint16{
						extensionCustom,
					},
					Bugs: ProtocolBugs{
						OnlyCompressSecondClientHelloInner: hrr,
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
					"-expect-ech-accept",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: illegal parameter",
				expectedError:      ":INVALID_OUTER_EXTENSION:",
			})

			// Test that the server rejects a references to the ECH extension in
			// ech_outer_extensions. The ECH extension is not authenticated in the
			// AAD and would result in an invalid ClientHelloInner.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-OuterExtensions-SelfReference" + suffix,
				config: Config{
					ServerName:      "secret.example",
					DefaultCurves:   defaultCurves,
					ClientECHConfig: echConfig.ECHConfig,
					ECHOuterExtensions: []uint16{
						extensionEncryptedClientHello,
					},
					Bugs: ProtocolBugs{
						OnlyCompressSecondClientHelloInner: hrr,
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: illegal parameter",
				expectedError:      ":INVALID_OUTER_EXTENSION:",
			})

			// Test the message callback is correctly reported with ECH.
			clientAndServerHello := "read hs 1\nread clienthelloinner\nwrite hs 2\n"
			expectMsgCallback := clientAndServerHello
			if protocol == tls {
				expectMsgCallback += "write ccs\n"
			}
			if hrr {
				expectMsgCallback += clientAndServerHello
			}
			// EncryptedExtensions onwards.
			expectMsgCallback += `write hs 8
write hs 11
write hs 15
write hs 20
read hs 20
write ack
write hs 4
write hs 4
read ack
read ack
`
			if protocol != dtls {
				expectMsgCallback = strings.ReplaceAll(expectMsgCallback, "write ack\n", "")
				expectMsgCallback = strings.ReplaceAll(expectMsgCallback, "read ack\n", "")
			}
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-MessageCallback" + suffix,
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
					DefaultCurves:   defaultCurves,
					Bugs: ProtocolBugs{
						NoCloseNotify: true, // Align QUIC and TCP traces.
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-ech-accept",
					"-expect-msg-callback", expectMsgCallback,
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})
		}

		// Test that ECH, which runs before an async early callback, interacts
		// correctly in the state machine.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-AsyncEarlyCallback",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
			},
			flags: []string{
				"-async",
				"-use-early-callback",
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-server-name", "secret.example",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
		})

		// Test that we successfully rewind the TLS state machine and disable ECH in the
		// case that the select_cert_cb signals that ECH is not possible for the SNI in
		// ClientHelloInner.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-FailCallbackNeedRewind",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
			},
			flags: []string{
				"-async",
				"-fail-early-callback-ech-rewind",
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-server-name", "public.example",
			},
			expectations: connectionExpectations{
				echAccepted: false,
			},
		})

		// Test that we correctly handle falling back to a ClientHelloOuter with
		// no SNI (public name).
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-RewindWithNoPublicName",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
				Bugs: ProtocolBugs{
					OmitPublicName: true,
				},
			},
			flags: []string{
				"-async",
				"-fail-early-callback-ech-rewind",
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-no-server-name",
			},
			expectations: connectionExpectations{
				echAccepted: false,
			},
		})

		// Test ECH-enabled server with two ECHConfigs can decrypt client's ECH when
		// it uses the second ECHConfig.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-SecondECHConfig",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig1.ECHConfig,
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-ech-server-config", base64FlagValue(echConfig1.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig1.Key),
				"-ech-is-retry-config", "1",
				"-expect-server-name", "secret.example",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
		})

		// Test ECH-enabled server with two ECHConfigs that have the same config
		// ID can decrypt client's ECH when it uses the second ECHConfig.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-RepeatedConfigID",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfigRepeatID.ECHConfig,
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-ech-server-config", base64FlagValue(echConfigRepeatID.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfigRepeatID.Key),
				"-ech-is-retry-config", "1",
				"-expect-server-name", "secret.example",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
		})

		// Test all supported ECH cipher suites.
		for i, cipher := range echCiphers {
			otherCipher := echCiphers[(i+1)%len(echCiphers)]

			// Test the ECH server can handle the specified cipher.
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-Cipher-" + cipher.name,
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
					ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "secret.example",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})

			// Test that client can offer the specified cipher and skip over
			// unrecognized ones.
			cipherConfig := generateServerECHConfig(&ECHConfig{
				ConfigID: 42,
				CipherSuites: []HPKECipherSuite{
					{KDF: 0x1111, AEAD: 0x2222},
					{KDF: cipher.cipher.KDF, AEAD: 0x2222},
					{KDF: 0x1111, AEAD: cipher.cipher.AEAD},
					cipher.cipher,
				},
			})
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Cipher-" + cipher.name,
				config: Config{
					ServerECHConfigs: []ServerECHConfig{cipherConfig},
					Credential:       &echSecretCertificate,
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(cipherConfig.ECHConfig.Raw)),
					"-host-name", "secret.example",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})

			// Test that the ECH server rejects the specified cipher if not
			// listed in its ECHConfig.
			otherCipherConfig := generateServerECHConfig(&ECHConfig{
				ConfigID:     42,
				CipherSuites: []HPKECipherSuite{otherCipher.cipher},
			})
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-DisabledCipher-" + cipher.name,
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
					ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
					Bugs: ProtocolBugs{
						ExpectECHRetryConfigs: CreateECHConfigList(otherCipherConfig.ECHConfig.Raw),
					},
				},
				flags: []string{
					"-ech-server-config", base64FlagValue(otherCipherConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(otherCipherConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-server-name", "public.example",
				},
			})
		}

		// Test that the ECH server handles a short enc value by falling back to
		// ClientHelloOuter.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-ShortEnc",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
				Bugs: ProtocolBugs{
					ExpectECHRetryConfigs: CreateECHConfigList(echConfig.ECHConfig.Raw),
					TruncateClientECHEnc:  true,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-server-name", "public.example",
			},
		})

		// Test that the server handles decryption failure by falling back to
		// ClientHelloOuter.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-CorruptEncryptedClientHello",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
				Bugs: ProtocolBugs{
					ExpectECHRetryConfigs:       CreateECHConfigList(echConfig.ECHConfig.Raw),
					CorruptEncryptedClientHello: true,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
			},
		})

		// Test that the server treats decryption failure in the second
		// ClientHello as fatal.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-CorruptSecondEncryptedClientHello",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
				// Force a HelloRetryRequest.
				DefaultCurves: []CurveID{},
				Bugs: ProtocolBugs{
					CorruptSecondEncryptedClientHello: true,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
			},
			shouldFail:         true,
			expectedError:      ":DECRYPTION_FAILED:",
			expectedLocalError: "remote error: error decrypting message",
		})

		// Test that the server treats a missing second ECH extension as fatal.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-OmitSecondEncryptedClientHello",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
				// Force a HelloRetryRequest.
				DefaultCurves: []CurveID{},
				Bugs: ProtocolBugs{
					OmitSecondEncryptedClientHello: true,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
			},
			shouldFail:         true,
			expectedError:      ":MISSING_EXTENSION:",
			expectedLocalError: "remote error: missing extension",
		})

		// Test that the server treats a mismatched config ID in the second ClientHello as fatal.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-DifferentConfigIDSecondClientHello",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
				// Force a HelloRetryRequest.
				DefaultCurves: []CurveID{},
				Bugs: ProtocolBugs{
					CorruptSecondEncryptedClientHelloConfigID: true,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
			},
			shouldFail:         true,
			expectedError:      ":DECODE_ERROR:",
			expectedLocalError: "remote error: illegal parameter",
		})

		// Test early data works with ECH, in both accept and reject cases.
		// TODO(crbug.com/381113363): Enable these tests for DTLS once we
		// support early data in DTLS 1.3.
		if protocol != dtls {
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-EarlyData",
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
				},
				resumeSession: true,
				earlyData:     true,
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-EarlyDataRejected",
				config: Config{
					ServerName:      "secret.example",
					ClientECHConfig: echConfig.ECHConfig,
					Bugs: ProtocolBugs{
						// Cause the server to reject 0-RTT with a bad ticket age.
						SendTicketAge: 1 * time.Hour,
					},
				},
				resumeSession:           true,
				earlyData:               true,
				expectEarlyDataRejected: true,
				flags: []string{
					"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
					"-ech-server-key", base64FlagValue(echConfig.Key),
					"-ech-is-retry-config", "1",
					"-expect-ech-accept",
				},
				expectations: connectionExpectations{
					echAccepted: true,
				},
			})
		}

		// Test servers with ECH disabled correctly ignore the extension and
		// handshake with the ClientHelloOuter.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-Disabled",
			config: Config{
				ServerName:      "secret.example",
				ClientECHConfig: echConfig.ECHConfig,
			},
			flags: []string{
				"-expect-server-name", "public.example",
			},
		})

		// Test that ECH can be used with client certificates. In particular,
		// the name override logic should not interfere with the server.
		// Test the server can accept ECH.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-ClientAuth",
			config: Config{
				Credential:      &rsaCertificate,
				ClientECHConfig: echConfig.ECHConfig,
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-ech-accept",
				"-require-any-client-certificate",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
		})
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-Decline-ClientAuth",
			config: Config{
				Credential:      &rsaCertificate,
				ClientECHConfig: echConfig.ECHConfig,
				Bugs: ProtocolBugs{
					ExpectECHRetryConfigs: CreateECHConfigList(echConfig1.ECHConfig.Raw),
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig1.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig1.Key),
				"-ech-is-retry-config", "1",
				"-require-any-client-certificate",
			},
		})

		// Test that the server accepts padding.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-Padding",
			config: Config{
				ClientECHConfig: echConfig.ECHConfig,
				Bugs: ProtocolBugs{
					ClientECHPadding: 10,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
		})

		// Test that the server rejects bad padding.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-BadPadding",
			config: Config{
				ClientECHConfig: echConfig.ECHConfig,
				Bugs: ProtocolBugs{
					ClientECHPadding:    10,
					BadClientECHPadding: true,
				},
			},
			flags: []string{
				"-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw),
				"-ech-server-key", base64FlagValue(echConfig.Key),
				"-ech-is-retry-config", "1",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
			shouldFail:         true,
			expectedError:      ":DECODE_ERROR",
			expectedLocalError: "remote error: illegal parameter",
		})

		// Test the client's behavior when the server ignores ECH GREASE.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-GREASE-Client-TLS13",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				Bugs: ProtocolBugs{
					ExpectClientECH: true,
				},
			},
			flags: []string{"-enable-ech-grease"},
		})

		// Test the client's ECH GREASE behavior when responding to server's
		// HelloRetryRequest. This test implicitly checks that the first and second
		// ClientHello messages have identical ECH extensions.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-GREASE-Client-TLS13-HelloRetryRequest",
			config: Config{
				MaxVersion: VersionTLS13,
				MinVersion: VersionTLS13,
				// P-384 requires a HelloRetryRequest against BoringSSL's default
				// configuration. Assert this with ExpectMissingKeyShare.
				CurvePreferences: []CurveID{CurveP384},
				Bugs: ProtocolBugs{
					ExpectMissingKeyShare: true,
					ExpectClientECH:       true,
				},
			},
			flags: []string{"-enable-ech-grease", "-expect-hrr"},
		})

		unsupportedVersion := []byte{
			// version
			0xba, 0xdd,
			// length
			0x00, 0x05,
			// contents
			0x05, 0x04, 0x03, 0x02, 0x01,
		}

		// Test that the client accepts a well-formed encrypted_client_hello
		// extension in response to ECH GREASE. The response includes one ECHConfig
		// with a supported version and one with an unsupported version.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-GREASE-Client-TLS13-Retry-Configs",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				Bugs: ProtocolBugs{
					ExpectClientECH: true,
					// Include an additional well-formed ECHConfig with an
					// unsupported version. This ensures the client can skip
					// unsupported configs.
					SendECHRetryConfigs: CreateECHConfigList(echConfig.ECHConfig.Raw, unsupportedVersion),
				},
			},
			flags: []string{"-enable-ech-grease"},
		})

		// TLS 1.2 ServerHellos cannot contain retry configs.
		if protocol != quic {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-GREASE-Client-TLS12-RejectRetryConfigs",
				config: Config{
					MinVersion:       VersionTLS12,
					MaxVersion:       VersionTLS12,
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectClientECH:           true,
						AlwaysSendECHRetryConfigs: true,
					},
				},
				flags:              []string{"-enable-ech-grease"},
				shouldFail:         true,
				expectedLocalError: "remote error: unsupported extension",
				expectedError:      ":UNEXPECTED_EXTENSION:",
			})
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-TLS12-RejectRetryConfigs",
				config: Config{
					MinVersion:       VersionTLS12,
					MaxVersion:       VersionTLS12,
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectClientECH:           true,
						AlwaysSendECHRetryConfigs: true,
					},
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig1.ECHConfig.Raw)),
				},
				shouldFail:         true,
				expectedLocalError: "remote error: unsupported extension",
				expectedError:      ":UNEXPECTED_EXTENSION:",
			})
		}

		// Retry configs must be rejected when ECH is accepted.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Accept-RejectRetryConfigs",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectClientECH:           true,
					AlwaysSendECHRetryConfigs: true,
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
			},
			shouldFail:         true,
			expectedLocalError: "remote error: unsupported extension",
			expectedError:      ":UNEXPECTED_EXTENSION:",
		})

		// Unsolicited ECH HelloRetryRequest extensions should be rejected.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-UnsolictedHRRExtension",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				CurvePreferences: []CurveID{CurveP384},
				Bugs: ProtocolBugs{
					AlwaysSendECHHelloRetryRequest: true,
					ExpectMissingKeyShare:          true, // Check we triggered HRR.
				},
			},
			shouldFail:         true,
			expectedLocalError: "remote error: unsupported extension",
			expectedError:      ":UNEXPECTED_EXTENSION:",
		})

		// GREASE should ignore ECH HelloRetryRequest extensions.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-GREASE-IgnoreHRRExtension",
			config: Config{
				CurvePreferences: []CurveID{CurveP384},
				Bugs: ProtocolBugs{
					AlwaysSendECHHelloRetryRequest: true,
					ExpectMissingKeyShare:          true, // Check we triggered HRR.
				},
			},
			flags: []string{"-enable-ech-grease"},
		})

		// Random ECH HelloRetryRequest extensions also signal ECH reject.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-RandomHRRExtension",
			config: Config{
				CurvePreferences: []CurveID{CurveP384},
				Bugs: ProtocolBugs{
					AlwaysSendECHHelloRetryRequest: true,
					ExpectMissingKeyShare:          true, // Check we triggered HRR.
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
			},
			shouldFail:         true,
			expectedLocalError: "remote error: ECH required",
			expectedError:      ":ECH_REJECTED:",
		})

		// Test that the client aborts with a decode_error alert when it receives a
		// syntactically-invalid encrypted_client_hello extension from the server.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-GREASE-Client-TLS13-Invalid-Retry-Configs",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				Bugs: ProtocolBugs{
					ExpectClientECH:     true,
					SendECHRetryConfigs: []byte{0xba, 0xdd, 0xec, 0xcc},
				},
			},
			flags:              []string{"-enable-ech-grease"},
			shouldFail:         true,
			expectedLocalError: "remote error: error decoding message",
			expectedError:      ":ERROR_PARSING_EXTENSION:",
		})

		// Test that the server responds to an inner ECH extension with the
		// acceptance confirmation.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-ECHInner",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				Bugs: ProtocolBugs{
					AlwaysSendECHInner: true,
				},
			},
			resumeSession: true,
		})
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-ECHInner-HelloRetryRequest",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				// Force a HelloRetryRequest.
				DefaultCurves: []CurveID{},
				Bugs: ProtocolBugs{
					AlwaysSendECHInner: true,
				},
			},
			resumeSession: true,
		})

		// Test that server fails the handshake when it sees a non-empty
		// inner ECH extension.
		testCases = append(testCases, testCase{
			testType: serverTest,
			protocol: protocol,
			name:     prefix + "ECH-Server-ECHInner-NotEmpty",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				Bugs: ProtocolBugs{
					AlwaysSendECHInner:  true,
					SendInvalidECHInner: []byte{42, 42, 42},
				},
			},
			shouldFail:         true,
			expectedLocalError: "remote error: error decoding message",
			expectedError:      ":ERROR_PARSING_EXTENSION:",
		})

		// Test that a TLS 1.3 server that receives an inner ECH extension can
		// negotiate TLS 1.2 without clobbering the downgrade signal.
		if protocol != quic {
			testCases = append(testCases, testCase{
				testType: serverTest,
				protocol: protocol,
				name:     prefix + "ECH-Server-ECHInner-Absent-TLS12",
				config: Config{
					MinVersion: VersionTLS12,
					MaxVersion: VersionTLS13,
					Bugs: ProtocolBugs{
						// Omit supported_versions extension so the server negotiates
						// TLS 1.2.
						OmitSupportedVersions: true,
						AlwaysSendECHInner:    true,
					},
				},
				// Check that the client sees the TLS 1.3 downgrade signal in
				// ServerHello.random.
				shouldFail:         true,
				expectedLocalError: "tls: downgrade from TLS 1.3 detected",
			})
		}

		// Test the client can negotiate ECH, with and without HelloRetryRequest.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client",
			config: Config{
				MinVersion:       VersionTLS13,
				MaxVersion:       VersionTLS13,
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectServerName:      "secret.example",
					ExpectOuterServerName: "public.example",
				},
				Credential: &echSecretCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-host-name", "secret.example",
				"-expect-ech-accept",
			},
			resumeSession: true,
			expectations:  connectionExpectations{echAccepted: true},
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-HelloRetryRequest",
			config: Config{
				MinVersion:       VersionTLS13,
				MaxVersion:       VersionTLS13,
				CurvePreferences: []CurveID{CurveP384},
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectServerName:      "secret.example",
					ExpectOuterServerName: "public.example",
					ExpectMissingKeyShare: true, // Check we triggered HRR.
				},
				Credential: &echSecretCertificate,
			},
			resumeSession: true,
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-host-name", "secret.example",
				"-expect-ech-accept",
				"-expect-hrr", // Check we triggered HRR.
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// Test the client can negotiate ECH with early data.
		// TODO(crbug.com/381113363): Enable these tests for DTLS once we
		// support early data in DTLS 1.3.
		if protocol != dtls {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-EarlyData",
				config: Config{
					MinVersion:       VersionTLS13,
					MaxVersion:       VersionTLS13,
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectServerName: "secret.example",
					},
					Credential: &echSecretCertificate,
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-host-name", "secret.example",
					"-expect-ech-accept",
				},
				resumeSession: true,
				earlyData:     true,
				expectations:  connectionExpectations{echAccepted: true},
			})
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-EarlyDataRejected",
				config: Config{
					MinVersion:       VersionTLS13,
					MaxVersion:       VersionTLS13,
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectServerName:      "secret.example",
						AlwaysRejectEarlyData: true,
					},
					Credential: &echSecretCertificate,
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-host-name", "secret.example",
					"-expect-ech-accept",
				},
				resumeSession:           true,
				earlyData:               true,
				expectEarlyDataRejected: true,
				expectations:            connectionExpectations{echAccepted: true},
			})
		}

		if protocol != quic {
			// Test that an ECH client does not offer a TLS 1.2 session.
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-TLS12SessionID",
				config: Config{
					MaxVersion:             VersionTLS12,
					SessionTicketsDisabled: true,
				},
				resumeConfig: &Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectNoTLS12Session: true,
					},
				},
				flags: []string{
					"-on-resume-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-on-resume-expect-ech-accept",
				},
				resumeSession:        true,
				expectResumeRejected: true,
				resumeExpectations:   &connectionExpectations{echAccepted: true},
			})
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-TLS12SessionTicket",
				config: Config{
					MaxVersion: VersionTLS12,
				},
				resumeConfig: &Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectNoTLS12Session: true,
					},
				},
				flags: []string{
					"-on-resume-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-on-resume-expect-ech-accept",
				},
				resumeSession:        true,
				expectResumeRejected: true,
				resumeExpectations:   &connectionExpectations{echAccepted: true},
			})
		}

		// ClientHelloInner should not include NPN, which is a TLS 1.2-only
		// extensions. The Go server will enforce this, so this test only needs
		// to configure the feature on the shim. Other application extensions
		// are sent implicitly.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-NoNPN",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				// Enable NPN.
				"-select-next-proto", "foo",
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// Test that the client iterates over configurations in the
		// ECHConfigList and selects the first with supported parameters.
		unsupportedKEM := generateServerECHConfig(&ECHConfig{
			KEM:       0x6666,
			PublicKey: []byte{1, 2, 3, 4},
		}).ECHConfig
		unsupportedCipherSuites := generateServerECHConfig(&ECHConfig{
			CipherSuites: []HPKECipherSuite{{0x1111, 0x2222}},
		}).ECHConfig
		unsupportedMandatoryExtension := generateServerECHConfig(&ECHConfig{
			UnsupportedMandatoryExtension: true,
		}).ECHConfig
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-SelectECHConfig",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(
					unsupportedVersion,
					unsupportedKEM.Raw,
					unsupportedCipherSuites.Raw,
					unsupportedMandatoryExtension.Raw,
					echConfig.ECHConfig.Raw,
					// |echConfig1| is also supported, but the client should
					// select the first one.
					echConfig1.ECHConfig.Raw,
				)),
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{
				echAccepted: true,
			},
		})

		// Test that the client skips sending ECH if all ECHConfigs are
		// unsupported.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-NoSupportedConfigs",
			config: Config{
				Bugs: ProtocolBugs{
					ExpectNoClientECH: true,
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(
					unsupportedVersion,
					unsupportedKEM.Raw,
					unsupportedCipherSuites.Raw,
					unsupportedMandatoryExtension.Raw,
				)),
			},
		})

		// If ECH GREASE is enabled, the client should send ECH GREASE when no
		// configured ECHConfig is suitable.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-NoSupportedConfigs-GREASE",
			config: Config{
				Bugs: ProtocolBugs{
					ExpectClientECH: true,
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(
					unsupportedVersion,
					unsupportedKEM.Raw,
					unsupportedCipherSuites.Raw,
					unsupportedMandatoryExtension.Raw,
				)),
				"-enable-ech-grease",
			},
		})

		// If both ECH GREASE and suitable ECHConfigs are available, the
		// client should send normal ECH.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-GREASE",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
			},
			resumeSession: true,
			expectations:  connectionExpectations{echAccepted: true},
		})

		// Test that GREASE extensions correctly interact with ECH. Both the
		// inner and outer ClientHellos should include GREASE extensions.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-GREASEExtensions",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectGREASE: true,
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				"-enable-grease",
			},
			resumeSession: true,
			expectations:  connectionExpectations{echAccepted: true},
		})

		// Test that the client tolerates unsupported extensions if the
		// mandatory bit is not set.
		unsupportedExtension := generateServerECHConfig(&ECHConfig{UnsupportedExtension: true})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-UnsupportedExtension",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{unsupportedExtension},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(unsupportedExtension.ECHConfig.Raw)),
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// Syntax errors in the ECHConfigList should be rejected.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-InvalidECHConfigList",
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw[1:])),
			},
			shouldFail:    true,
			expectedError: ":INVALID_ECH_CONFIG_LIST:",
		})

		// If the ClientHelloInner has no server_name extension, while the
		// ClientHelloOuter has one, the client must check for unsolicited
		// extensions based on the selected ClientHello.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-UnsolicitedInnerServerNameAck",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					// ClientHelloOuter should have a server name.
					ExpectOuterServerName: "public.example",
					// The server will acknowledge the server_name extension.
					// This option runs whether or not the client requested the
					// extension.
					SendServerNameAck: true,
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				// No -host-name flag.
				"-expect-ech-accept",
			},
			shouldFail:         true,
			expectedError:      ":UNEXPECTED_EXTENSION:",
			expectedLocalError: "remote error: unsupported extension",
			expectations:       connectionExpectations{echAccepted: true},
		})

		// Most extensions are the same between ClientHelloInner and
		// ClientHelloOuter and can be compressed.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-ExpectECHOuterExtensions",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				NextProtos:       []string{"proto"},
				Bugs: ProtocolBugs{
					ExpectECHOuterExtensions: []uint16{
						extensionALPN,
						extensionKeyShare,
						extensionPSKKeyExchangeModes,
						extensionSignatureAlgorithms,
						extensionSupportedCurves,
					},
				},
				Credential: &echSecretCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				"-advertise-alpn", "\x05proto",
				"-expect-alpn", "proto",
				"-host-name", "secret.example",
			},
			expectations: connectionExpectations{
				echAccepted: true,
				nextProto:   "proto",
			},
			skipQUICALPNConfig: true,
		})

		// If the server name happens to match the public name, it still should
		// not be compressed. It is not publicly known that they match.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-NeverCompressServerName",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				NextProtos:       []string{"proto"},
				Bugs: ProtocolBugs{
					ExpectECHUncompressedExtensions: []uint16{extensionServerName},
					ExpectServerName:                "public.example",
					ExpectOuterServerName:           "public.example",
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				"-host-name", "public.example",
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// If the ClientHelloOuter disables TLS 1.3, e.g. in QUIC, the client
		// should also compress supported_versions.
		tls13Vers := VersionTLS13
		if protocol == dtls {
			tls13Vers = VersionDTLS13
		}
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-CompressSupportedVersions",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectECHOuterExtensions: []uint16{
						extensionSupportedVersions,
					},
				},
				Credential: &echSecretCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-host-name", "secret.example",
				"-expect-ech-accept",
				"-min-version", strconv.Itoa(int(tls13Vers)),
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// Test that the client can still offer server names that exceed the
		// maximum name length. It is only a padding hint.
		maxNameLen10 := generateServerECHConfig(&ECHConfig{MaxNameLen: 10})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-NameTooLong",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{maxNameLen10},
				Bugs: ProtocolBugs{
					ExpectServerName: "test0123456789.example",
				},
				Credential: &echLongNameCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(maxNameLen10.ECHConfig.Raw)),
				"-host-name", "test0123456789.example",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// Test the client can recognize when ECH is rejected.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig2, echConfig3},
				Bugs: ProtocolBugs{
					ExpectServerName: "public.example",
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-retry-configs", base64FlagValue(CreateECHConfigList(echConfig2.ECHConfig.Raw, echConfig3.ECHConfig.Raw)),
			},
			shouldFail:         true,
			expectedLocalError: "remote error: ECH required",
			expectedError:      ":ECH_REJECTED:",
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-HelloRetryRequest",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig2, echConfig3},
				CurvePreferences: []CurveID{CurveP384},
				Bugs: ProtocolBugs{
					ExpectServerName:      "public.example",
					ExpectMissingKeyShare: true, // Check we triggered HRR.
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-retry-configs", base64FlagValue(CreateECHConfigList(echConfig2.ECHConfig.Raw, echConfig3.ECHConfig.Raw)),
				"-expect-hrr", // Check we triggered HRR.
			},
			shouldFail:         true,
			expectedLocalError: "remote error: ECH required",
			expectedError:      ":ECH_REJECTED:",
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-NoRetryConfigs",
			config: Config{
				Bugs: ProtocolBugs{
					ExpectServerName: "public.example",
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-no-ech-retry-configs",
			},
			shouldFail:         true,
			expectedLocalError: "remote error: ECH required",
			expectedError:      ":ECH_REJECTED:",
		})
		if protocol != quic {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-TLS12",
				config: Config{
					MaxVersion: VersionTLS12,
					Bugs: ProtocolBugs{
						ExpectServerName: "public.example",
					},
					Credential: &echPublicCertificate,
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					// TLS 1.2 cannot provide retry configs.
					"-expect-no-ech-retry-configs",
				},
				shouldFail:         true,
				expectedLocalError: "remote error: ECH required",
				expectedError:      ":ECH_REJECTED:",
			})

			// Test that the client disables False Start when ECH is rejected.
			testCases = append(testCases, testCase{
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-TLS12-NoFalseStart",
				config: Config{
					MaxVersion:   VersionTLS12,
					CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
					NextProtos:   []string{"foo"},
					Bugs: ProtocolBugs{
						// The options below cause the server to, immediately
						// after client Finished, send an alert and try to read
						// application data without sending server Finished.
						ExpectFalseStart:          true,
						AlertBeforeFalseStartTest: alertAccessDenied,
					},
					Credential: &echPublicCertificate,
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-false-start",
					"-advertise-alpn", "\x03foo",
					"-expect-alpn", "foo",
				},
				shimWritesFirst: true,
				shouldFail:      true,
				// Ensure the client does not send application data at the False
				// Start point. EOF comes from the client closing the connection
				// in response ot the alert.
				expectedLocalError: "tls: peer did not false start: EOF",
				// Ensures the client picks up the alert before reporting an
				// authenticated |SSL_R_ECH_REJECTED|.
				expectedError: ":TLSV1_ALERT_ACCESS_DENIED:",
			})
		}

		// Test that unsupported retry configs in a valid ECHConfigList are
		// allowed. They will be skipped when configured in the retry.
		retryConfigs := CreateECHConfigList(
			unsupportedVersion,
			unsupportedKEM.Raw,
			unsupportedCipherSuites.Raw,
			unsupportedMandatoryExtension.Raw,
			echConfig2.ECHConfig.Raw)
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-UnsupportedRetryConfigs",
			config: Config{
				Bugs: ProtocolBugs{
					SendECHRetryConfigs: retryConfigs,
					ExpectServerName:    "public.example",
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-retry-configs", base64FlagValue(retryConfigs),
			},
			shouldFail:         true,
			expectedLocalError: "remote error: ECH required",
			expectedError:      ":ECH_REJECTED:",
		})

		// Test that the client rejects ClientHelloOuter handshakes that attempt
		// to resume the ClientHelloInner's ticket, at TLS 1.2 and TLS 1.3.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-ResumeInnerSession-TLS13",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectServerName: "secret.example",
				},
				Credential: &echSecretCertificate,
			},
			resumeConfig: &Config{
				MaxVersion:       VersionTLS13,
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectServerName:                    "public.example",
					UseInnerSessionWithClientHelloOuter: true,
				},
				Credential: &echPublicCertificate,
			},
			resumeSession: true,
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-host-name", "secret.example",
				"-on-initial-expect-ech-accept",
			},
			shouldFail:         true,
			expectedError:      ":UNEXPECTED_EXTENSION:",
			expectations:       connectionExpectations{echAccepted: true},
			resumeExpectations: &connectionExpectations{echAccepted: false},
		})
		if protocol == tls {
			// This is only syntactically possible with TLS. In DTLS, we don't
			// have middlebox compatibility mode, so the session ID will only
			// filled in if we are offering a DTLS 1.2 session. But a DTLS 1.2
			// would never be offered in ClientHelloInner. Without a session ID,
			// the server syntactically cannot express a resumption at DTLS 1.2.
			// In QUIC, the above is true, and 1.2 does not exist anyway.
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-ResumeInnerSession-TLS12",
				config: Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectServerName: "secret.example",
					},
					Credential: &echSecretCertificate,
				},
				resumeConfig: &Config{
					MinVersion:       VersionTLS12,
					MaxVersion:       VersionTLS12,
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectServerName:                    "public.example",
						UseInnerSessionWithClientHelloOuter: true,
						// The client only ever offers TLS 1.3 sessions in
						// ClientHelloInner. AcceptAnySession allows them to be
						// resumed at TLS 1.2.
						AcceptAnySession: true,
					},
					Credential: &echPublicCertificate,
				},
				resumeSession: true,
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-host-name", "secret.example",
					"-on-initial-expect-ech-accept",
				},
				// From the client's perspective, the server echoed a session ID to
				// signal resumption, but the selected ClientHello had nothing to
				// resume.
				shouldFail:         true,
				expectedError:      ":SERVER_ECHOED_INVALID_SESSION_ID:",
				expectedLocalError: "remote error: illegal parameter",
				expectations:       connectionExpectations{echAccepted: true},
				resumeExpectations: &connectionExpectations{echAccepted: false},
			})
		}

		// Test that the client can process ECH rejects after an early data reject.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-EarlyDataRejected",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectServerName: "secret.example",
				},
				Credential: &echSecretCertificate,
			},
			resumeConfig: &Config{
				ServerECHConfigs: []ServerECHConfig{echConfig2},
				Bugs: ProtocolBugs{
					ExpectServerName: "public.example",
				},
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-host-name", "secret.example",
				// Although the resumption connection does not accept ECH, the
				// API will report ECH was accepted at the 0-RTT point.
				"-expect-ech-accept",
				// -on-retry refers to the retried handshake after 0-RTT reject,
				// while ech-retry-configs refers to the ECHConfigs to use in
				// the next connection attempt.
				"-on-retry-expect-ech-retry-configs", base64FlagValue(CreateECHConfigList(echConfig2.ECHConfig.Raw)),
			},
			resumeSession:           true,
			expectResumeRejected:    true,
			earlyData:               true,
			expectEarlyDataRejected: true,
			expectations:            connectionExpectations{echAccepted: true},
			resumeExpectations:      &connectionExpectations{echAccepted: false},
			shouldFail:              true,
			expectedLocalError:      "remote error: ECH required",
			expectedError:           ":ECH_REJECTED:",
		})
		// TODO(crbug.com/381113363): Enable this test for DTLS once we
		// support early data in DTLS 1.3.
		if protocol != quic && protocol != dtls {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-EarlyDataRejected-TLS12",
				config: Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Bugs: ProtocolBugs{
						ExpectServerName: "secret.example",
					},
					Credential: &echSecretCertificate,
				},
				resumeConfig: &Config{
					MaxVersion: VersionTLS12,
					Bugs: ProtocolBugs{
						ExpectServerName: "public.example",
					},
					Credential: &echPublicCertificate,
				},
				flags: []string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-host-name", "secret.example",
					// Although the resumption connection does not accept ECH, the
					// API will report ECH was accepted at the 0-RTT point.
					"-expect-ech-accept",
				},
				resumeSession:           true,
				expectResumeRejected:    true,
				earlyData:               true,
				expectEarlyDataRejected: true,
				expectations:            connectionExpectations{echAccepted: true},
				resumeExpectations:      &connectionExpectations{echAccepted: false},
				// ClientHellos with early data cannot negotiate TLS 1.2, with
				// or without ECH. The shim should first report
				// |SSL_R_WRONG_VERSION_ON_EARLY_DATA|. The caller will then
				// repair the first error by retrying without early data. That
				// will look like ECH-Client-Reject-TLS12 and select TLS 1.2
				// and ClientHelloOuter. The caller will then trigger a third
				// attempt, which will succeed.
				shouldFail:    true,
				expectedError: ":WRONG_VERSION_ON_EARLY_DATA:",
			})
		}

		// Test that the client ignores ECHConfigs with invalid public names.
		invalidPublicName := generateServerECHConfig(&ECHConfig{PublicName: "dns_names_have_no_underscores.example"})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-SkipInvalidPublicName",
			config: Config{
				Bugs: ProtocolBugs{
					// No ECHConfigs are supported, so the client should fall
					// back to cleartext.
					ExpectNoClientECH: true,
					ExpectServerName:  "secret.example",
				},
				Credential: &echSecretCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(invalidPublicName.ECHConfig.Raw)),
				"-host-name", "secret.example",
			},
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-SkipInvalidPublicName-2",
			config: Config{
				// The client should skip |invalidPublicName| and use |echConfig|.
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectOuterServerName: "public.example",
					ExpectServerName:      "secret.example",
				},
				Credential: &echSecretCertificate,
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(invalidPublicName.ECHConfig.Raw, echConfig.ECHConfig.Raw)),
				"-host-name", "secret.example",
				"-expect-ech-accept",
			},
			expectations: connectionExpectations{echAccepted: true},
		})

		// Test both sync and async mode, to test both with and without the
		// client certificate callback.
		for _, async := range []bool{false, true} {
			var flags []string
			var suffix string
			if async {
				flags = []string{"-async"}
				suffix = "-Async"
			}

			// Test that ECH and client certificates can be used together.
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-ClientCertificate" + suffix,
				config: Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					ClientAuth:       RequireAnyClientCert,
				},
				shimCertificate: &rsaCertificate,
				flags: append([]string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-expect-ech-accept",
				}, flags...),
				expectations: connectionExpectations{echAccepted: true},
			})

			// Test that, when ECH is rejected, the client does not send a client
			// certificate.
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-NoClientCertificate-TLS13" + suffix,
				config: Config{
					MinVersion: VersionTLS13,
					MaxVersion: VersionTLS13,
					ClientAuth: RequireAnyClientCert,
					Credential: &echPublicCertificate,
				},
				shimCertificate: &rsaCertificate,
				flags: append([]string{
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				}, flags...),
				shouldFail:         true,
				expectedLocalError: "tls: client didn't provide a certificate",
			})
			if protocol != quic {
				testCases = append(testCases, testCase{
					testType: clientTest,
					protocol: protocol,
					name:     prefix + "ECH-Client-Reject-NoClientCertificate-TLS12" + suffix,
					config: Config{
						MinVersion: VersionTLS12,
						MaxVersion: VersionTLS12,
						ClientAuth: RequireAnyClientCert,
						Credential: &echPublicCertificate,
					},
					shimCertificate: &rsaCertificate,
					flags: append([]string{
						"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					}, flags...),
					shouldFail:         true,
					expectedLocalError: "tls: client didn't provide a certificate",
				})
			}
		}

		// Test that ECH and Channel ID can be used together. Channel ID does
		// not exist in DTLS.
		if protocol != dtls {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-ChannelID",
				config: Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					RequestChannelID: true,
				},
				flags: []string{
					"-send-channel-id", channelIDKeyPath,
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					"-expect-ech-accept",
				},
				resumeSession: true,
				expectations: connectionExpectations{
					channelID:   true,
					echAccepted: true,
				},
			})

			// Handshakes where ECH is rejected do not offer or accept Channel ID.
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-NoChannelID-TLS13",
				config: Config{
					MinVersion: VersionTLS13,
					MaxVersion: VersionTLS13,
					Bugs: ProtocolBugs{
						AlwaysNegotiateChannelID: true,
					},
					Credential: &echPublicCertificate,
				},
				flags: []string{
					"-send-channel-id", channelIDKeyPath,
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				},
				shouldFail:         true,
				expectedLocalError: "remote error: unsupported extension",
				expectedError:      ":UNEXPECTED_EXTENSION:",
			})
			if protocol != quic {
				testCases = append(testCases, testCase{
					testType: clientTest,
					protocol: protocol,
					name:     prefix + "ECH-Client-Reject-NoChannelID-TLS12",
					config: Config{
						MinVersion: VersionTLS12,
						MaxVersion: VersionTLS12,
						Bugs: ProtocolBugs{
							AlwaysNegotiateChannelID: true,
						},
						Credential: &echPublicCertificate,
					},
					flags: []string{
						"-send-channel-id", channelIDKeyPath,
						"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					},
					shouldFail:         true,
					expectedLocalError: "remote error: unsupported extension",
					expectedError:      ":UNEXPECTED_EXTENSION:",
				})
			}
		}

		// Test that ECH correctly overrides the host name for certificate
		// verification.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-NotOffered-NoOverrideName",
			flags: []string{
				"-verify-peer",
				"-use-custom-verify-callback",
				// When not offering ECH, verify the usual name in both full
				// and resumption handshakes.
				"-reverify-on-resume",
				"-expect-no-ech-name-override",
			},
			resumeSession: true,
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-GREASE-NoOverrideName",
			flags: []string{
				"-verify-peer",
				"-use-custom-verify-callback",
				"-enable-ech-grease",
				// When offering ECH GREASE, verify the usual name in both full
				// and resumption handshakes.
				"-reverify-on-resume",
				"-expect-no-ech-name-override",
			},
			resumeSession: true,
		})
		if protocol != quic {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Rejected-OverrideName-TLS12",
				config: Config{
					MinVersion: VersionTLS12,
					MaxVersion: VersionTLS12,
				},
				flags: []string{
					"-verify-peer",
					"-use-custom-verify-callback",
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					// When ECH is rejected, verify the public name. This can
					// only happen in full handshakes.
					"-expect-ech-name-override", "public.example",
				},
				shouldFail:         true,
				expectedError:      ":ECH_REJECTED:",
				expectedLocalError: "remote error: ECH required",
			})
		}
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Reject-OverrideName-TLS13",
			config: Config{
				MinVersion: VersionTLS13,
				MaxVersion: VersionTLS13,
				Credential: &echPublicCertificate,
			},
			flags: []string{
				"-verify-peer",
				"-use-custom-verify-callback",
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				// When ECH is rejected, verify the public name. This can
				// only happen in full handshakes.
				"-expect-ech-name-override", "public.example",
			},
			shouldFail:         true,
			expectedError:      ":ECH_REJECTED:",
			expectedLocalError: "remote error: ECH required",
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-Accept-NoOverrideName",
			config: Config{
				ServerECHConfigs: []ServerECHConfig{echConfig},
			},
			flags: []string{
				"-verify-peer",
				"-use-custom-verify-callback",
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				// When ECH is accepted, verify the usual name in both full and
				// resumption handshakes.
				"-reverify-on-resume",
				"-expect-no-ech-name-override",
			},
			resumeSession: true,
			expectations:  connectionExpectations{echAccepted: true},
		})
		// TODO(crbug.com/381113363): Enable this test for DTLS once we
		// support early data in DTLS 1.3.
		if protocol != dtls {
			testCases = append(testCases, testCase{
				testType: clientTest,
				protocol: protocol,
				name:     prefix + "ECH-Client-Reject-EarlyDataRejected-OverrideNameOnRetry",
				config: Config{
					ServerECHConfigs: []ServerECHConfig{echConfig},
					Credential:       &echPublicCertificate,
				},
				resumeConfig: &Config{
					Credential: &echPublicCertificate,
				},
				flags: []string{
					"-verify-peer",
					"-use-custom-verify-callback",
					"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
					// Although the resumption connection does not accept ECH, the
					// API will report ECH was accepted at the 0-RTT point.
					"-expect-ech-accept",
					// The resumption connection verifies certificates twice. First,
					// if reverification is enabled, we verify the 0-RTT certificate
					// as if ECH as accepted. There should be no name override.
					// Next, on the post-0-RTT-rejection retry, we verify the new
					// server certificate. This picks up the ECH reject, so it
					// should use public.example.
					"-reverify-on-resume",
					"-on-resume-expect-no-ech-name-override",
					"-on-retry-expect-ech-name-override", "public.example",
				},
				resumeSession:           true,
				expectResumeRejected:    true,
				earlyData:               true,
				expectEarlyDataRejected: true,
				expectations:            connectionExpectations{echAccepted: true},
				resumeExpectations:      &connectionExpectations{echAccepted: false},
				shouldFail:              true,
				expectedError:           ":ECH_REJECTED:",
				expectedLocalError:      "remote error: ECH required",
			})
		}

		// Test that the client checks both HelloRetryRequest and ServerHello
		// for a confirmation signal.
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-HelloRetryRequest-MissingServerHelloConfirmation",
			config: Config{
				MinVersion:       VersionTLS13,
				MaxVersion:       VersionTLS13,
				CurvePreferences: []CurveID{CurveP384},
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectMissingKeyShare:          true, // Check we triggered HRR.
					OmitServerHelloECHConfirmation: true,
				},
			},
			resumeSession: true,
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-hrr", // Check we triggered HRR.
			},
			shouldFail:    true,
			expectedError: ":INCONSISTENT_ECH_NEGOTIATION:",
		})

		// Test the message callback is correctly reported, with and without
		// HelloRetryRequest.
		clientAndServerHello := "write clienthelloinner\nwrite hs 1\nread hs 2\n"
		clientAndServerHelloInitial := clientAndServerHello
		if protocol == tls {
			clientAndServerHelloInitial += "write ccs\n"
		}
		// EncryptedExtensions onwards.
		finishHandshake := `read hs 8
read hs 11
read hs 15
read hs 20
write hs 20
read ack
read hs 4
read hs 4
`
		if protocol != dtls {
			finishHandshake = strings.ReplaceAll(finishHandshake, "read ack\n", "")
		}
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-MessageCallback",
			config: Config{
				MinVersion:       VersionTLS13,
				MaxVersion:       VersionTLS13,
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					NoCloseNotify: true, // Align QUIC and TCP traces.
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				"-expect-msg-callback", clientAndServerHelloInitial + finishHandshake,
			},
			expectations: connectionExpectations{echAccepted: true},
		})
		testCases = append(testCases, testCase{
			testType: clientTest,
			protocol: protocol,
			name:     prefix + "ECH-Client-MessageCallback-HelloRetryRequest",
			config: Config{
				MinVersion:       VersionTLS13,
				MaxVersion:       VersionTLS13,
				CurvePreferences: []CurveID{CurveP384},
				ServerECHConfigs: []ServerECHConfig{echConfig},
				Bugs: ProtocolBugs{
					ExpectMissingKeyShare: true, // Check we triggered HRR.
					NoCloseNotify:         true, // Align QUIC and TCP traces.
				},
			},
			flags: []string{
				"-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)),
				"-expect-ech-accept",
				"-expect-hrr", // Check we triggered HRR.
				"-expect-msg-callback", clientAndServerHelloInitial + clientAndServerHello + finishHandshake,
			},
			expectations: connectionExpectations{echAccepted: true},
		})
	}
}
