// Copyright 2025 The BoringSSL Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package runner import ( "strconv" "strings" "time" "boringssl.googlesource.com/boringssl.git/ssl/test/runner/hpke" ) type echCipher struct { name string cipher HPKECipherSuite } var echCiphers = []echCipher{ { name: "HKDF-SHA256-AES-128-GCM", cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES128GCM}, }, { name: "HKDF-SHA256-AES-256-GCM", cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.AES256GCM}, }, { name: "HKDF-SHA256-ChaCha20-Poly1305", cipher: HPKECipherSuite{KDF: hpke.HKDFSHA256, AEAD: hpke.ChaCha20Poly1305}, }, } // generateServerECHConfig constructs a ServerECHConfig with a fresh X25519 // keypair and using |template| as a template for the ECHConfig. If fields are // omitted, defaults are used. func generateServerECHConfig(template *ECHConfig) ServerECHConfig { publicKey, secretKey, err := hpke.GenerateKeyPairX25519() if err != nil { panic(err) } templateCopy := *template if templateCopy.KEM == 0 { templateCopy.KEM = hpke.X25519WithHKDFSHA256 } if len(templateCopy.PublicKey) == 0 { templateCopy.PublicKey = publicKey } if len(templateCopy.CipherSuites) == 0 { templateCopy.CipherSuites = make([]HPKECipherSuite, len(echCiphers)) for i, cipher := range echCiphers { templateCopy.CipherSuites[i] = cipher.cipher } } if len(templateCopy.PublicName) == 0 { templateCopy.PublicName = "public.example" } if templateCopy.MaxNameLen == 0 { templateCopy.MaxNameLen = 64 } return ServerECHConfig{ECHConfig: CreateECHConfig(&templateCopy), Key: secretKey} } func addEncryptedClientHelloTests() { // echConfig's ConfigID should match the one used in ssl/test/fuzzer.h. echConfig := generateServerECHConfig(&ECHConfig{ConfigID: 42}) echConfig1 := generateServerECHConfig(&ECHConfig{ConfigID: 43}) echConfig2 := generateServerECHConfig(&ECHConfig{ConfigID: 44}) echConfig3 := generateServerECHConfig(&ECHConfig{ConfigID: 45}) echConfigRepeatID := generateServerECHConfig(&ECHConfig{ConfigID: 42}) echSecretCertificate := rootCA.Issue(X509Info{ PrivateKey: &rsa2048Key, DNSNames: []string{"secret.example"}, }).ToCredential() echPublicCertificate := rootCA.Issue(X509Info{ PrivateKey: &rsa2048Key, DNSNames: []string{"public.example"}, }).ToCredential() echLongNameCertificate := rootCA.Issue(X509Info{ PrivateKey: &ecdsaP256Key, DNSNames: []string{"test0123456789.example"}, }).ToCredential() for _, protocol := range []protocol{tls, quic, dtls} { prefix := protocol.String() + "-" // There are two ClientHellos, so many of our tests have // HelloRetryRequest variations. for _, hrr := range []bool{false, true} { var suffix string var defaultCurves []CurveID if hrr { suffix = "-HelloRetryRequest" // Require a HelloRetryRequest for every curve. defaultCurves = []CurveID{} } // Test the server can accept ECH. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server" + suffix, config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, DefaultCurves: defaultCurves, }, resumeSession: true, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test the server can accept ECH with a minimal ClientHelloOuter. // This confirms that the server does not unexpectedly pick up // fields from the wrong ClientHello. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-MinimalClientHelloOuter" + suffix, config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, DefaultCurves: defaultCurves, Bugs: ProtocolBugs{ MinimalClientHelloOuter: true, }, }, resumeSession: true, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that the server can decline ECH. In particular, it must send // retry configs. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-Decline" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, // The client uses an ECHConfig that the server does not understand // so we can observe which retry configs the server sends back. ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ OfferSessionInClientHelloOuter: true, ExpectECHRetryConfigs: CreateECHConfigList(echConfig2.ECHConfig.Raw, echConfig3.ECHConfig.Raw), }, }, resumeSession: true, flags: []string{ // Configure three ECHConfigs on the shim, only two of which // should be sent in retry configs. "-ech-server-config", base64FlagValue(echConfig1.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig1.Key), "-ech-is-retry-config", "0", "-ech-server-config", base64FlagValue(echConfig2.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig2.Key), "-ech-is-retry-config", "1", "-ech-server-config", base64FlagValue(echConfig3.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig3.Key), "-ech-is-retry-config", "1", "-expect-server-name", "public.example", }, }) // Test that the server considers a ClientHelloInner indicating TLS // 1.2 to be a fatal error. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-TLS12InInner" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ AllowTLS12InClientHelloInner: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedLocalError: "remote error: illegal parameter", expectedError: ":INVALID_CLIENT_HELLO_INNER:", }) // When inner ECH extension is absent from the ClientHelloInner, the // server should fail the connection. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-MissingECHInner" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ OmitECHInner: !hrr, OmitSecondECHInner: hrr, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedLocalError: "remote error: illegal parameter", expectedError: ":INVALID_CLIENT_HELLO_INNER:", }) // Test that the server can decode ech_outer_extensions. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OuterExtensions" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, ECHOuterExtensions: []uint16{ extensionKeyShare, extensionSupportedCurves, // Include a custom extension, to test that unrecognized // extensions are also decoded. extensionCustom, }, Bugs: ProtocolBugs{ CustomExtension: "test", OnlyCompressSecondClientHelloInner: hrr, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that the server allows referenced ClientHelloOuter // extensions to be interleaved with other extensions. Only the // relative order must match. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OuterExtensions-Interleaved" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, ECHOuterExtensions: []uint16{ extensionKeyShare, extensionSupportedCurves, extensionCustom, }, Bugs: ProtocolBugs{ CustomExtension: "test", OnlyCompressSecondClientHelloInner: hrr, ECHOuterExtensionOrder: []uint16{ extensionServerName, extensionKeyShare, extensionSupportedVersions, extensionPSKKeyExchangeModes, extensionSupportedCurves, extensionSignatureAlgorithms, extensionCustom, }, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that the server rejects references to extensions in the // wrong order. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OuterExtensions-WrongOrder" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, ECHOuterExtensions: []uint16{ extensionKeyShare, extensionSupportedCurves, }, Bugs: ProtocolBugs{ CustomExtension: "test", OnlyCompressSecondClientHelloInner: hrr, ECHOuterExtensionOrder: []uint16{ extensionSupportedCurves, extensionKeyShare, }, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", }, shouldFail: true, expectedLocalError: "remote error: illegal parameter", expectedError: ":INVALID_OUTER_EXTENSION:", }) // Test that the server rejects duplicated values in ech_outer_extensions. // Besides causing the server to reconstruct an invalid ClientHelloInner // with duplicated extensions, this behavior would be vulnerable to DoS // attacks. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OuterExtensions-Duplicate" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, ECHOuterExtensions: []uint16{ extensionSupportedCurves, extensionSupportedCurves, }, Bugs: ProtocolBugs{ OnlyCompressSecondClientHelloInner: hrr, // Don't duplicate the extension in ClientHelloOuter. ECHOuterExtensionOrder: []uint16{ extensionSupportedCurves, }, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedLocalError: "remote error: illegal parameter", expectedError: ":INVALID_OUTER_EXTENSION:", }) // Test that the server rejects references to missing extensions in // ech_outer_extensions. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OuterExtensions-Missing" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, ECHOuterExtensions: []uint16{ extensionCustom, }, Bugs: ProtocolBugs{ OnlyCompressSecondClientHelloInner: hrr, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, shouldFail: true, expectedLocalError: "remote error: illegal parameter", expectedError: ":INVALID_OUTER_EXTENSION:", }) // Test that the server rejects a references to the ECH extension in // ech_outer_extensions. The ECH extension is not authenticated in the // AAD and would result in an invalid ClientHelloInner. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OuterExtensions-SelfReference" + suffix, config: Config{ ServerName: "secret.example", DefaultCurves: defaultCurves, ClientECHConfig: echConfig.ECHConfig, ECHOuterExtensions: []uint16{ extensionEncryptedClientHello, }, Bugs: ProtocolBugs{ OnlyCompressSecondClientHelloInner: hrr, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedLocalError: "remote error: illegal parameter", expectedError: ":INVALID_OUTER_EXTENSION:", }) // Test the message callback is correctly reported with ECH. clientAndServerHello := "read hs 1\nread clienthelloinner\nwrite hs 2\n" expectMsgCallback := clientAndServerHello if protocol == tls { expectMsgCallback += "write ccs\n" } if hrr { expectMsgCallback += clientAndServerHello } // EncryptedExtensions onwards. expectMsgCallback += `write hs 8 write hs 11 write hs 15 write hs 20 read hs 20 write ack write hs 4 write hs 4 read ack read ack ` if protocol != dtls { expectMsgCallback = strings.ReplaceAll(expectMsgCallback, "write ack\n", "") expectMsgCallback = strings.ReplaceAll(expectMsgCallback, "read ack\n", "") } testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-MessageCallback" + suffix, config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, DefaultCurves: defaultCurves, Bugs: ProtocolBugs{ NoCloseNotify: true, // Align QUIC and TCP traces. }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-ech-accept", "-expect-msg-callback", expectMsgCallback, }, expectations: connectionExpectations{ echAccepted: true, }, }) } // Test that ECH, which runs before an async early callback, interacts // correctly in the state machine. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-AsyncEarlyCallback", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, }, flags: []string{ "-async", "-use-early-callback", "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that we successfully rewind the TLS state machine and disable ECH in the // case that the select_cert_cb signals that ECH is not possible for the SNI in // ClientHelloInner. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-FailCallbackNeedRewind", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, }, flags: []string{ "-async", "-fail-early-callback-ech-rewind", "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "public.example", }, expectations: connectionExpectations{ echAccepted: false, }, }) // Test that we correctly handle falling back to a ClientHelloOuter with // no SNI (public name). testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-RewindWithNoPublicName", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ OmitPublicName: true, }, }, flags: []string{ "-async", "-fail-early-callback-ech-rewind", "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-no-server-name", }, expectations: connectionExpectations{ echAccepted: false, }, }) // Test ECH-enabled server with two ECHConfigs can decrypt client's ECH when // it uses the second ECHConfig. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-SecondECHConfig", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig1.ECHConfig, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-ech-server-config", base64FlagValue(echConfig1.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig1.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test ECH-enabled server with two ECHConfigs that have the same config // ID can decrypt client's ECH when it uses the second ECHConfig. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-RepeatedConfigID", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfigRepeatID.ECHConfig, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-ech-server-config", base64FlagValue(echConfigRepeatID.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfigRepeatID.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test all supported ECH cipher suites. for i, cipher := range echCiphers { otherCipher := echCiphers[(i+1)%len(echCiphers)] // Test the ECH server can handle the specified cipher. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-Cipher-" + cipher.name, config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, ECHCipherSuites: []HPKECipherSuite{cipher.cipher}, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that client can offer the specified cipher and skip over // unrecognized ones. cipherConfig := generateServerECHConfig(&ECHConfig{ ConfigID: 42, CipherSuites: []HPKECipherSuite{ {KDF: 0x1111, AEAD: 0x2222}, {KDF: cipher.cipher.KDF, AEAD: 0x2222}, {KDF: 0x1111, AEAD: cipher.cipher.AEAD}, cipher.cipher, }, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Cipher-" + cipher.name, config: Config{ ServerECHConfigs: []ServerECHConfig{cipherConfig}, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(cipherConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that the ECH server rejects the specified cipher if not // listed in its ECHConfig. otherCipherConfig := generateServerECHConfig(&ECHConfig{ ConfigID: 42, CipherSuites: []HPKECipherSuite{otherCipher.cipher}, }) testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-DisabledCipher-" + cipher.name, config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, ECHCipherSuites: []HPKECipherSuite{cipher.cipher}, Bugs: ProtocolBugs{ ExpectECHRetryConfigs: CreateECHConfigList(otherCipherConfig.ECHConfig.Raw), }, }, flags: []string{ "-ech-server-config", base64FlagValue(otherCipherConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(otherCipherConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "public.example", }, }) } // Test that the ECH server handles a short enc value by falling back to // ClientHelloOuter. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-ShortEnc", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ ExpectECHRetryConfigs: CreateECHConfigList(echConfig.ECHConfig.Raw), TruncateClientECHEnc: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-server-name", "public.example", }, }) // Test that the server handles decryption failure by falling back to // ClientHelloOuter. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-CorruptEncryptedClientHello", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ ExpectECHRetryConfigs: CreateECHConfigList(echConfig.ECHConfig.Raw), CorruptEncryptedClientHello: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, }) // Test that the server treats decryption failure in the second // ClientHello as fatal. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-CorruptSecondEncryptedClientHello", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, // Force a HelloRetryRequest. DefaultCurves: []CurveID{}, Bugs: ProtocolBugs{ CorruptSecondEncryptedClientHello: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedError: ":DECRYPTION_FAILED:", expectedLocalError: "remote error: error decrypting message", }) // Test that the server treats a missing second ECH extension as fatal. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-OmitSecondEncryptedClientHello", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, // Force a HelloRetryRequest. DefaultCurves: []CurveID{}, Bugs: ProtocolBugs{ OmitSecondEncryptedClientHello: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedError: ":MISSING_EXTENSION:", expectedLocalError: "remote error: missing extension", }) // Test that the server treats a mismatched config ID in the second ClientHello as fatal. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-DifferentConfigIDSecondClientHello", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, // Force a HelloRetryRequest. DefaultCurves: []CurveID{}, Bugs: ProtocolBugs{ CorruptSecondEncryptedClientHelloConfigID: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", }, shouldFail: true, expectedError: ":DECODE_ERROR:", expectedLocalError: "remote error: illegal parameter", }) // Test early data works with ECH, in both accept and reject cases. // TODO(crbug.com/381113363): Enable these tests for DTLS once we // support early data in DTLS 1.3. if protocol != dtls { testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-EarlyData", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, }, resumeSession: true, earlyData: true, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-EarlyDataRejected", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ // Cause the server to reject 0-RTT with a bad ticket age. SendTicketAge: 1 * time.Hour, }, }, resumeSession: true, earlyData: true, expectEarlyDataRejected: true, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) } // Test servers with ECH disabled correctly ignore the extension and // handshake with the ClientHelloOuter. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-Disabled", config: Config{ ServerName: "secret.example", ClientECHConfig: echConfig.ECHConfig, }, flags: []string{ "-expect-server-name", "public.example", }, }) // Test that ECH can be used with client certificates. In particular, // the name override logic should not interfere with the server. // Test the server can accept ECH. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-ClientAuth", config: Config{ Credential: &rsaCertificate, ClientECHConfig: echConfig.ECHConfig, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-ech-accept", "-require-any-client-certificate", }, expectations: connectionExpectations{ echAccepted: true, }, }) testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-Decline-ClientAuth", config: Config{ Credential: &rsaCertificate, ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ ExpectECHRetryConfigs: CreateECHConfigList(echConfig1.ECHConfig.Raw), }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig1.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig1.Key), "-ech-is-retry-config", "1", "-require-any-client-certificate", }, }) // Test that the server accepts padding. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-Padding", config: Config{ ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ ClientECHPadding: 10, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that the server rejects bad padding. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-BadPadding", config: Config{ ClientECHConfig: echConfig.ECHConfig, Bugs: ProtocolBugs{ ClientECHPadding: 10, BadClientECHPadding: true, }, }, flags: []string{ "-ech-server-config", base64FlagValue(echConfig.ECHConfig.Raw), "-ech-server-key", base64FlagValue(echConfig.Key), "-ech-is-retry-config", "1", "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, shouldFail: true, expectedError: ":DECODE_ERROR", expectedLocalError: "remote error: illegal parameter", }) // Test the client's behavior when the server ignores ECH GREASE. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-GREASE-Client-TLS13", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectClientECH: true, }, }, flags: []string{"-enable-ech-grease"}, }) // Test the client's ECH GREASE behavior when responding to server's // HelloRetryRequest. This test implicitly checks that the first and second // ClientHello messages have identical ECH extensions. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-GREASE-Client-TLS13-HelloRetryRequest", config: Config{ MaxVersion: VersionTLS13, MinVersion: VersionTLS13, // P-384 requires a HelloRetryRequest against BoringSSL's default // configuration. Assert this with ExpectMissingKeyShare. CurvePreferences: []CurveID{CurveP384}, Bugs: ProtocolBugs{ ExpectMissingKeyShare: true, ExpectClientECH: true, }, }, flags: []string{"-enable-ech-grease", "-expect-hrr"}, }) unsupportedVersion := []byte{ // version 0xba, 0xdd, // length 0x00, 0x05, // contents 0x05, 0x04, 0x03, 0x02, 0x01, } // Test that the client accepts a well-formed encrypted_client_hello // extension in response to ECH GREASE. The response includes one ECHConfig // with a supported version and one with an unsupported version. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-GREASE-Client-TLS13-Retry-Configs", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectClientECH: true, // Include an additional well-formed ECHConfig with an // unsupported version. This ensures the client can skip // unsupported configs. SendECHRetryConfigs: CreateECHConfigList(echConfig.ECHConfig.Raw, unsupportedVersion), }, }, flags: []string{"-enable-ech-grease"}, }) // TLS 1.2 ServerHellos cannot contain retry configs. if protocol != quic { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-GREASE-Client-TLS12-RejectRetryConfigs", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectClientECH: true, AlwaysSendECHRetryConfigs: true, }, }, flags: []string{"-enable-ech-grease"}, shouldFail: true, expectedLocalError: "remote error: unsupported extension", expectedError: ":UNEXPECTED_EXTENSION:", }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-TLS12-RejectRetryConfigs", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectClientECH: true, AlwaysSendECHRetryConfigs: true, }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig1.ECHConfig.Raw)), }, shouldFail: true, expectedLocalError: "remote error: unsupported extension", expectedError: ":UNEXPECTED_EXTENSION:", }) } // Retry configs must be rejected when ECH is accepted. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Accept-RejectRetryConfigs", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectClientECH: true, AlwaysSendECHRetryConfigs: true, }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), }, shouldFail: true, expectedLocalError: "remote error: unsupported extension", expectedError: ":UNEXPECTED_EXTENSION:", }) // Unsolicited ECH HelloRetryRequest extensions should be rejected. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-UnsolictedHRRExtension", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, CurvePreferences: []CurveID{CurveP384}, Bugs: ProtocolBugs{ AlwaysSendECHHelloRetryRequest: true, ExpectMissingKeyShare: true, // Check we triggered HRR. }, }, shouldFail: true, expectedLocalError: "remote error: unsupported extension", expectedError: ":UNEXPECTED_EXTENSION:", }) // GREASE should ignore ECH HelloRetryRequest extensions. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-GREASE-IgnoreHRRExtension", config: Config{ CurvePreferences: []CurveID{CurveP384}, Bugs: ProtocolBugs{ AlwaysSendECHHelloRetryRequest: true, ExpectMissingKeyShare: true, // Check we triggered HRR. }, }, flags: []string{"-enable-ech-grease"}, }) // Random ECH HelloRetryRequest extensions also signal ECH reject. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-RandomHRRExtension", config: Config{ CurvePreferences: []CurveID{CurveP384}, Bugs: ProtocolBugs{ AlwaysSendECHHelloRetryRequest: true, ExpectMissingKeyShare: true, // Check we triggered HRR. }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), }, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) // Test that the client aborts with a decode_error alert when it receives a // syntactically-invalid encrypted_client_hello extension from the server. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-GREASE-Client-TLS13-Invalid-Retry-Configs", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ ExpectClientECH: true, SendECHRetryConfigs: []byte{0xba, 0xdd, 0xec, 0xcc}, }, }, flags: []string{"-enable-ech-grease"}, shouldFail: true, expectedLocalError: "remote error: error decoding message", expectedError: ":ERROR_PARSING_EXTENSION:", }) // Test that the server responds to an inner ECH extension with the // acceptance confirmation. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-ECHInner", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ AlwaysSendECHInner: true, }, }, resumeSession: true, }) testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-ECHInner-HelloRetryRequest", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, // Force a HelloRetryRequest. DefaultCurves: []CurveID{}, Bugs: ProtocolBugs{ AlwaysSendECHInner: true, }, }, resumeSession: true, }) // Test that server fails the handshake when it sees a non-empty // inner ECH extension. testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-ECHInner-NotEmpty", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ AlwaysSendECHInner: true, SendInvalidECHInner: []byte{42, 42, 42}, }, }, shouldFail: true, expectedLocalError: "remote error: error decoding message", expectedError: ":ERROR_PARSING_EXTENSION:", }) // Test that a TLS 1.3 server that receives an inner ECH extension can // negotiate TLS 1.2 without clobbering the downgrade signal. if protocol != quic { testCases = append(testCases, testCase{ testType: serverTest, protocol: protocol, name: prefix + "ECH-Server-ECHInner-Absent-TLS12", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ // Omit supported_versions extension so the server negotiates // TLS 1.2. OmitSupportedVersions: true, AlwaysSendECHInner: true, }, }, // Check that the client sees the TLS 1.3 downgrade signal in // ServerHello.random. shouldFail: true, expectedLocalError: "tls: downgrade from TLS 1.3 detected", }) } // Test the client can negotiate ECH, with and without HelloRetryRequest. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", ExpectOuterServerName: "public.example", }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", }, resumeSession: true, expectations: connectionExpectations{echAccepted: true}, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-HelloRetryRequest", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP384}, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", ExpectOuterServerName: "public.example", ExpectMissingKeyShare: true, // Check we triggered HRR. }, Credential: &echSecretCertificate, }, resumeSession: true, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", "-expect-hrr", // Check we triggered HRR. }, expectations: connectionExpectations{echAccepted: true}, }) // Test the client can negotiate ECH with early data. // TODO(crbug.com/381113363): Enable these tests for DTLS once we // support early data in DTLS 1.3. if protocol != dtls { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-EarlyData", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", }, resumeSession: true, earlyData: true, expectations: connectionExpectations{echAccepted: true}, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-EarlyDataRejected", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", AlwaysRejectEarlyData: true, }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", }, resumeSession: true, earlyData: true, expectEarlyDataRejected: true, expectations: connectionExpectations{echAccepted: true}, }) } if protocol != quic { // Test that an ECH client does not offer a TLS 1.2 session. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-TLS12SessionID", config: Config{ MaxVersion: VersionTLS12, SessionTicketsDisabled: true, }, resumeConfig: &Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectNoTLS12Session: true, }, }, flags: []string{ "-on-resume-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-on-resume-expect-ech-accept", }, resumeSession: true, expectResumeRejected: true, resumeExpectations: &connectionExpectations{echAccepted: true}, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-TLS12SessionTicket", config: Config{ MaxVersion: VersionTLS12, }, resumeConfig: &Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectNoTLS12Session: true, }, }, flags: []string{ "-on-resume-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-on-resume-expect-ech-accept", }, resumeSession: true, expectResumeRejected: true, resumeExpectations: &connectionExpectations{echAccepted: true}, }) } // ClientHelloInner should not include NPN, which is a TLS 1.2-only // extensions. The Go server will enforce this, so this test only needs // to configure the feature on the shim. Other application extensions // are sent implicitly. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-NoNPN", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", // Enable NPN. "-select-next-proto", "foo", }, expectations: connectionExpectations{echAccepted: true}, }) // Test that the client iterates over configurations in the // ECHConfigList and selects the first with supported parameters. unsupportedKEM := generateServerECHConfig(&ECHConfig{ KEM: 0x6666, PublicKey: []byte{1, 2, 3, 4}, }).ECHConfig unsupportedCipherSuites := generateServerECHConfig(&ECHConfig{ CipherSuites: []HPKECipherSuite{{0x1111, 0x2222}}, }).ECHConfig unsupportedMandatoryExtension := generateServerECHConfig(&ECHConfig{ UnsupportedMandatoryExtension: true, }).ECHConfig testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-SelectECHConfig", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList( unsupportedVersion, unsupportedKEM.Raw, unsupportedCipherSuites.Raw, unsupportedMandatoryExtension.Raw, echConfig.ECHConfig.Raw, // |echConfig1| is also supported, but the client should // select the first one. echConfig1.ECHConfig.Raw, )), "-expect-ech-accept", }, expectations: connectionExpectations{ echAccepted: true, }, }) // Test that the client skips sending ECH if all ECHConfigs are // unsupported. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-NoSupportedConfigs", config: Config{ Bugs: ProtocolBugs{ ExpectNoClientECH: true, }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList( unsupportedVersion, unsupportedKEM.Raw, unsupportedCipherSuites.Raw, unsupportedMandatoryExtension.Raw, )), }, }) // If ECH GREASE is enabled, the client should send ECH GREASE when no // configured ECHConfig is suitable. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-NoSupportedConfigs-GREASE", config: Config{ Bugs: ProtocolBugs{ ExpectClientECH: true, }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList( unsupportedVersion, unsupportedKEM.Raw, unsupportedCipherSuites.Raw, unsupportedMandatoryExtension.Raw, )), "-enable-ech-grease", }, }) // If both ECH GREASE and suitable ECHConfigs are available, the // client should send normal ECH. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-GREASE", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", }, resumeSession: true, expectations: connectionExpectations{echAccepted: true}, }) // Test that GREASE extensions correctly interact with ECH. Both the // inner and outer ClientHellos should include GREASE extensions. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-GREASEExtensions", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectGREASE: true, }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", "-enable-grease", }, resumeSession: true, expectations: connectionExpectations{echAccepted: true}, }) // Test that the client tolerates unsupported extensions if the // mandatory bit is not set. unsupportedExtension := generateServerECHConfig(&ECHConfig{UnsupportedExtension: true}) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-UnsupportedExtension", config: Config{ ServerECHConfigs: []ServerECHConfig{unsupportedExtension}, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(unsupportedExtension.ECHConfig.Raw)), "-expect-ech-accept", }, expectations: connectionExpectations{echAccepted: true}, }) // Syntax errors in the ECHConfigList should be rejected. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-InvalidECHConfigList", flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw[1:])), }, shouldFail: true, expectedError: ":INVALID_ECH_CONFIG_LIST:", }) // If the ClientHelloInner has no server_name extension, while the // ClientHelloOuter has one, the client must check for unsolicited // extensions based on the selected ClientHello. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-UnsolicitedInnerServerNameAck", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ // ClientHelloOuter should have a server name. ExpectOuterServerName: "public.example", // The server will acknowledge the server_name extension. // This option runs whether or not the client requested the // extension. SendServerNameAck: true, }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), // No -host-name flag. "-expect-ech-accept", }, shouldFail: true, expectedError: ":UNEXPECTED_EXTENSION:", expectedLocalError: "remote error: unsupported extension", expectations: connectionExpectations{echAccepted: true}, }) // Most extensions are the same between ClientHelloInner and // ClientHelloOuter and can be compressed. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-ExpectECHOuterExtensions", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, NextProtos: []string{"proto"}, Bugs: ProtocolBugs{ ExpectECHOuterExtensions: []uint16{ extensionALPN, extensionKeyShare, extensionPSKKeyExchangeModes, extensionSignatureAlgorithms, extensionSupportedCurves, }, }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", "-advertise-alpn", "\x05proto", "-expect-alpn", "proto", "-host-name", "secret.example", }, expectations: connectionExpectations{ echAccepted: true, nextProto: "proto", }, skipQUICALPNConfig: true, }) // If the server name happens to match the public name, it still should // not be compressed. It is not publicly known that they match. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-NeverCompressServerName", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, NextProtos: []string{"proto"}, Bugs: ProtocolBugs{ ExpectECHUncompressedExtensions: []uint16{extensionServerName}, ExpectServerName: "public.example", ExpectOuterServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", "-host-name", "public.example", }, expectations: connectionExpectations{echAccepted: true}, }) // If the ClientHelloOuter disables TLS 1.3, e.g. in QUIC, the client // should also compress supported_versions. tls13Vers := VersionTLS13 if protocol == dtls { tls13Vers = VersionDTLS13 } testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-CompressSupportedVersions", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectECHOuterExtensions: []uint16{ extensionSupportedVersions, }, }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", "-min-version", strconv.Itoa(int(tls13Vers)), }, expectations: connectionExpectations{echAccepted: true}, }) // Test that the client can still offer server names that exceed the // maximum name length. It is only a padding hint. maxNameLen10 := generateServerECHConfig(&ECHConfig{MaxNameLen: 10}) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-NameTooLong", config: Config{ ServerECHConfigs: []ServerECHConfig{maxNameLen10}, Bugs: ProtocolBugs{ ExpectServerName: "test0123456789.example", }, Credential: &echLongNameCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(maxNameLen10.ECHConfig.Raw)), "-host-name", "test0123456789.example", "-expect-ech-accept", }, expectations: connectionExpectations{echAccepted: true}, }) // Test the client can recognize when ECH is rejected. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig2, echConfig3}, Bugs: ProtocolBugs{ ExpectServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-retry-configs", base64FlagValue(CreateECHConfigList(echConfig2.ECHConfig.Raw, echConfig3.ECHConfig.Raw)), }, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-HelloRetryRequest", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig2, echConfig3}, CurvePreferences: []CurveID{CurveP384}, Bugs: ProtocolBugs{ ExpectServerName: "public.example", ExpectMissingKeyShare: true, // Check we triggered HRR. }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-retry-configs", base64FlagValue(CreateECHConfigList(echConfig2.ECHConfig.Raw, echConfig3.ECHConfig.Raw)), "-expect-hrr", // Check we triggered HRR. }, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-NoRetryConfigs", config: Config{ Bugs: ProtocolBugs{ ExpectServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-no-ech-retry-configs", }, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) if protocol != quic { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-TLS12", config: Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ ExpectServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), // TLS 1.2 cannot provide retry configs. "-expect-no-ech-retry-configs", }, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) // Test that the client disables False Start when ECH is rejected. testCases = append(testCases, testCase{ protocol: protocol, name: prefix + "ECH-Client-Reject-TLS12-NoFalseStart", config: Config{ MaxVersion: VersionTLS12, CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, NextProtos: []string{"foo"}, Bugs: ProtocolBugs{ // The options below cause the server to, immediately // after client Finished, send an alert and try to read // application data without sending server Finished. ExpectFalseStart: true, AlertBeforeFalseStartTest: alertAccessDenied, }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-false-start", "-advertise-alpn", "\x03foo", "-expect-alpn", "foo", }, shimWritesFirst: true, shouldFail: true, // Ensure the client does not send application data at the False // Start point. EOF comes from the client closing the connection // in response ot the alert. expectedLocalError: "tls: peer did not false start: EOF", // Ensures the client picks up the alert before reporting an // authenticated |SSL_R_ECH_REJECTED|. expectedError: ":TLSV1_ALERT_ACCESS_DENIED:", }) } // Test that unsupported retry configs in a valid ECHConfigList are // allowed. They will be skipped when configured in the retry. retryConfigs := CreateECHConfigList( unsupportedVersion, unsupportedKEM.Raw, unsupportedCipherSuites.Raw, unsupportedMandatoryExtension.Raw, echConfig2.ECHConfig.Raw) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-UnsupportedRetryConfigs", config: Config{ Bugs: ProtocolBugs{ SendECHRetryConfigs: retryConfigs, ExpectServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-retry-configs", base64FlagValue(retryConfigs), }, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) // Test that the client rejects ClientHelloOuter handshakes that attempt // to resume the ClientHelloInner's ticket, at TLS 1.2 and TLS 1.3. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-ResumeInnerSession-TLS13", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, resumeConfig: &Config{ MaxVersion: VersionTLS13, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "public.example", UseInnerSessionWithClientHelloOuter: true, }, Credential: &echPublicCertificate, }, resumeSession: true, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-on-initial-expect-ech-accept", }, shouldFail: true, expectedError: ":UNEXPECTED_EXTENSION:", expectations: connectionExpectations{echAccepted: true}, resumeExpectations: &connectionExpectations{echAccepted: false}, }) if protocol == tls { // This is only syntactically possible with TLS. In DTLS, we don't // have middlebox compatibility mode, so the session ID will only // filled in if we are offering a DTLS 1.2 session. But a DTLS 1.2 // would never be offered in ClientHelloInner. Without a session ID, // the server syntactically cannot express a resumption at DTLS 1.2. // In QUIC, the above is true, and 1.2 does not exist anyway. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-ResumeInnerSession-TLS12", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, resumeConfig: &Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "public.example", UseInnerSessionWithClientHelloOuter: true, // The client only ever offers TLS 1.3 sessions in // ClientHelloInner. AcceptAnySession allows them to be // resumed at TLS 1.2. AcceptAnySession: true, }, Credential: &echPublicCertificate, }, resumeSession: true, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-on-initial-expect-ech-accept", }, // From the client's perspective, the server echoed a session ID to // signal resumption, but the selected ClientHello had nothing to // resume. shouldFail: true, expectedError: ":SERVER_ECHOED_INVALID_SESSION_ID:", expectedLocalError: "remote error: illegal parameter", expectations: connectionExpectations{echAccepted: true}, resumeExpectations: &connectionExpectations{echAccepted: false}, }) } // Test that the client can process ECH rejects after an early data reject. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-EarlyDataRejected", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, resumeConfig: &Config{ ServerECHConfigs: []ServerECHConfig{echConfig2}, Bugs: ProtocolBugs{ ExpectServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", // Although the resumption connection does not accept ECH, the // API will report ECH was accepted at the 0-RTT point. "-expect-ech-accept", // -on-retry refers to the retried handshake after 0-RTT reject, // while ech-retry-configs refers to the ECHConfigs to use in // the next connection attempt. "-on-retry-expect-ech-retry-configs", base64FlagValue(CreateECHConfigList(echConfig2.ECHConfig.Raw)), }, resumeSession: true, expectResumeRejected: true, earlyData: true, expectEarlyDataRejected: true, expectations: connectionExpectations{echAccepted: true}, resumeExpectations: &connectionExpectations{echAccepted: false}, shouldFail: true, expectedLocalError: "remote error: ECH required", expectedError: ":ECH_REJECTED:", }) // TODO(crbug.com/381113363): Enable this test for DTLS once we // support early data in DTLS 1.3. if protocol != quic && protocol != dtls { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-EarlyDataRejected-TLS12", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, resumeConfig: &Config{ MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ ExpectServerName: "public.example", }, Credential: &echPublicCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-host-name", "secret.example", // Although the resumption connection does not accept ECH, the // API will report ECH was accepted at the 0-RTT point. "-expect-ech-accept", }, resumeSession: true, expectResumeRejected: true, earlyData: true, expectEarlyDataRejected: true, expectations: connectionExpectations{echAccepted: true}, resumeExpectations: &connectionExpectations{echAccepted: false}, // ClientHellos with early data cannot negotiate TLS 1.2, with // or without ECH. The shim should first report // |SSL_R_WRONG_VERSION_ON_EARLY_DATA|. The caller will then // repair the first error by retrying without early data. That // will look like ECH-Client-Reject-TLS12 and select TLS 1.2 // and ClientHelloOuter. The caller will then trigger a third // attempt, which will succeed. shouldFail: true, expectedError: ":WRONG_VERSION_ON_EARLY_DATA:", }) } // Test that the client ignores ECHConfigs with invalid public names. invalidPublicName := generateServerECHConfig(&ECHConfig{PublicName: "dns_names_have_no_underscores.example"}) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-SkipInvalidPublicName", config: Config{ Bugs: ProtocolBugs{ // No ECHConfigs are supported, so the client should fall // back to cleartext. ExpectNoClientECH: true, ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(invalidPublicName.ECHConfig.Raw)), "-host-name", "secret.example", }, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-SkipInvalidPublicName-2", config: Config{ // The client should skip |invalidPublicName| and use |echConfig|. ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectOuterServerName: "public.example", ExpectServerName: "secret.example", }, Credential: &echSecretCertificate, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(invalidPublicName.ECHConfig.Raw, echConfig.ECHConfig.Raw)), "-host-name", "secret.example", "-expect-ech-accept", }, expectations: connectionExpectations{echAccepted: true}, }) // Test both sync and async mode, to test both with and without the // client certificate callback. for _, async := range []bool{false, true} { var flags []string var suffix string if async { flags = []string{"-async"} suffix = "-Async" } // Test that ECH and client certificates can be used together. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-ClientCertificate" + suffix, config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, ClientAuth: RequireAnyClientCert, }, shimCertificate: &rsaCertificate, flags: append([]string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", }, flags...), expectations: connectionExpectations{echAccepted: true}, }) // Test that, when ECH is rejected, the client does not send a client // certificate. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-NoClientCertificate-TLS13" + suffix, config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, ClientAuth: RequireAnyClientCert, Credential: &echPublicCertificate, }, shimCertificate: &rsaCertificate, flags: append([]string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), }, flags...), shouldFail: true, expectedLocalError: "tls: client didn't provide a certificate", }) if protocol != quic { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-NoClientCertificate-TLS12" + suffix, config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, ClientAuth: RequireAnyClientCert, Credential: &echPublicCertificate, }, shimCertificate: &rsaCertificate, flags: append([]string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), }, flags...), shouldFail: true, expectedLocalError: "tls: client didn't provide a certificate", }) } } // Test that ECH and Channel ID can be used together. Channel ID does // not exist in DTLS. if protocol != dtls { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-ChannelID", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, RequestChannelID: true, }, flags: []string{ "-send-channel-id", channelIDKeyPath, "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", }, resumeSession: true, expectations: connectionExpectations{ channelID: true, echAccepted: true, }, }) // Handshakes where ECH is rejected do not offer or accept Channel ID. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-NoChannelID-TLS13", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Bugs: ProtocolBugs{ AlwaysNegotiateChannelID: true, }, Credential: &echPublicCertificate, }, flags: []string{ "-send-channel-id", channelIDKeyPath, "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), }, shouldFail: true, expectedLocalError: "remote error: unsupported extension", expectedError: ":UNEXPECTED_EXTENSION:", }) if protocol != quic { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-NoChannelID-TLS12", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, Bugs: ProtocolBugs{ AlwaysNegotiateChannelID: true, }, Credential: &echPublicCertificate, }, flags: []string{ "-send-channel-id", channelIDKeyPath, "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), }, shouldFail: true, expectedLocalError: "remote error: unsupported extension", expectedError: ":UNEXPECTED_EXTENSION:", }) } } // Test that ECH correctly overrides the host name for certificate // verification. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-NotOffered-NoOverrideName", flags: []string{ "-verify-peer", "-use-custom-verify-callback", // When not offering ECH, verify the usual name in both full // and resumption handshakes. "-reverify-on-resume", "-expect-no-ech-name-override", }, resumeSession: true, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-GREASE-NoOverrideName", flags: []string{ "-verify-peer", "-use-custom-verify-callback", "-enable-ech-grease", // When offering ECH GREASE, verify the usual name in both full // and resumption handshakes. "-reverify-on-resume", "-expect-no-ech-name-override", }, resumeSession: true, }) if protocol != quic { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Rejected-OverrideName-TLS12", config: Config{ MinVersion: VersionTLS12, MaxVersion: VersionTLS12, }, flags: []string{ "-verify-peer", "-use-custom-verify-callback", "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), // When ECH is rejected, verify the public name. This can // only happen in full handshakes. "-expect-ech-name-override", "public.example", }, shouldFail: true, expectedError: ":ECH_REJECTED:", expectedLocalError: "remote error: ECH required", }) } testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-OverrideName-TLS13", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, Credential: &echPublicCertificate, }, flags: []string{ "-verify-peer", "-use-custom-verify-callback", "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), // When ECH is rejected, verify the public name. This can // only happen in full handshakes. "-expect-ech-name-override", "public.example", }, shouldFail: true, expectedError: ":ECH_REJECTED:", expectedLocalError: "remote error: ECH required", }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Accept-NoOverrideName", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, }, flags: []string{ "-verify-peer", "-use-custom-verify-callback", "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", // When ECH is accepted, verify the usual name in both full and // resumption handshakes. "-reverify-on-resume", "-expect-no-ech-name-override", }, resumeSession: true, expectations: connectionExpectations{echAccepted: true}, }) // TODO(crbug.com/381113363): Enable this test for DTLS once we // support early data in DTLS 1.3. if protocol != dtls { testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-Reject-EarlyDataRejected-OverrideNameOnRetry", config: Config{ ServerECHConfigs: []ServerECHConfig{echConfig}, Credential: &echPublicCertificate, }, resumeConfig: &Config{ Credential: &echPublicCertificate, }, flags: []string{ "-verify-peer", "-use-custom-verify-callback", "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), // Although the resumption connection does not accept ECH, the // API will report ECH was accepted at the 0-RTT point. "-expect-ech-accept", // The resumption connection verifies certificates twice. First, // if reverification is enabled, we verify the 0-RTT certificate // as if ECH as accepted. There should be no name override. // Next, on the post-0-RTT-rejection retry, we verify the new // server certificate. This picks up the ECH reject, so it // should use public.example. "-reverify-on-resume", "-on-resume-expect-no-ech-name-override", "-on-retry-expect-ech-name-override", "public.example", }, resumeSession: true, expectResumeRejected: true, earlyData: true, expectEarlyDataRejected: true, expectations: connectionExpectations{echAccepted: true}, resumeExpectations: &connectionExpectations{echAccepted: false}, shouldFail: true, expectedError: ":ECH_REJECTED:", expectedLocalError: "remote error: ECH required", }) } // Test that the client checks both HelloRetryRequest and ServerHello // for a confirmation signal. testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-HelloRetryRequest-MissingServerHelloConfirmation", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP384}, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectMissingKeyShare: true, // Check we triggered HRR. OmitServerHelloECHConfirmation: true, }, }, resumeSession: true, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-hrr", // Check we triggered HRR. }, shouldFail: true, expectedError: ":INCONSISTENT_ECH_NEGOTIATION:", }) // Test the message callback is correctly reported, with and without // HelloRetryRequest. clientAndServerHello := "write clienthelloinner\nwrite hs 1\nread hs 2\n" clientAndServerHelloInitial := clientAndServerHello if protocol == tls { clientAndServerHelloInitial += "write ccs\n" } // EncryptedExtensions onwards. finishHandshake := `read hs 8 read hs 11 read hs 15 read hs 20 write hs 20 read ack read hs 4 read hs 4 ` if protocol != dtls { finishHandshake = strings.ReplaceAll(finishHandshake, "read ack\n", "") } testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-MessageCallback", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ NoCloseNotify: true, // Align QUIC and TCP traces. }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", "-expect-msg-callback", clientAndServerHelloInitial + finishHandshake, }, expectations: connectionExpectations{echAccepted: true}, }) testCases = append(testCases, testCase{ testType: clientTest, protocol: protocol, name: prefix + "ECH-Client-MessageCallback-HelloRetryRequest", config: Config{ MinVersion: VersionTLS13, MaxVersion: VersionTLS13, CurvePreferences: []CurveID{CurveP384}, ServerECHConfigs: []ServerECHConfig{echConfig}, Bugs: ProtocolBugs{ ExpectMissingKeyShare: true, // Check we triggered HRR. NoCloseNotify: true, // Align QUIC and TCP traces. }, }, flags: []string{ "-ech-config-list", base64FlagValue(CreateECHConfigList(echConfig.ECHConfig.Raw)), "-expect-ech-accept", "-expect-hrr", // Check we triggered HRR. "-expect-msg-callback", clientAndServerHelloInitial + clientAndServerHello + finishHandshake, }, expectations: connectionExpectations{echAccepted: true}, }) } }