|  | // 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 to 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}, | 
|  | }) | 
|  | } | 
|  | } |