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