blob: e91f4050211cf6e87d9ed9f5ac9c5cccd8959245 [file] [log] [blame]
// 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},
})
}
}