crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
// Copyright 2024 The Go Authors. All rights reserved.
|
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
package tls
|
|
|
|
|
|
|
|
|
|
import (
|
2024-10-29 20:22:27 -07:00
|
|
|
"bytes"
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
"crypto/internal/hpke"
|
|
|
|
|
"errors"
|
2024-10-29 20:22:27 -07:00
|
|
|
"fmt"
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/cryptobyte"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type echCipher struct {
|
|
|
|
|
KDFID uint16
|
|
|
|
|
AEADID uint16
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type echExtension struct {
|
|
|
|
|
Type uint16
|
|
|
|
|
Data []byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type echConfig struct {
|
|
|
|
|
raw []byte
|
|
|
|
|
|
|
|
|
|
Version uint16
|
|
|
|
|
Length uint16
|
|
|
|
|
|
|
|
|
|
ConfigID uint8
|
|
|
|
|
KemID uint16
|
|
|
|
|
PublicKey []byte
|
|
|
|
|
SymmetricCipherSuite []echCipher
|
|
|
|
|
|
|
|
|
|
MaxNameLength uint8
|
|
|
|
|
PublicName []byte
|
|
|
|
|
Extensions []echExtension
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-19 14:27:33 -08:00
|
|
|
var errMalformedECHConfigList = errors.New("tls: malformed ECHConfigList")
|
|
|
|
|
|
|
|
|
|
type echConfigErr struct {
|
|
|
|
|
field string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *echConfigErr) Error() string {
|
|
|
|
|
if e.field == "" {
|
|
|
|
|
return "tls: malformed ECHConfig"
|
|
|
|
|
}
|
|
|
|
|
return fmt.Sprintf("tls: malformed ECHConfig, invalid %s field", e.field)
|
|
|
|
|
}
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
|
2024-10-29 20:22:27 -07:00
|
|
|
func parseECHConfig(enc []byte) (skip bool, ec echConfig, err error) {
|
|
|
|
|
s := cryptobyte.String(enc)
|
|
|
|
|
ec.raw = []byte(enc)
|
|
|
|
|
if !s.ReadUint16(&ec.Version) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"version"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if !s.ReadUint16(&ec.Length) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"length"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if len(ec.raw) < int(ec.Length)+4 {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"length"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
ec.raw = ec.raw[:ec.Length+4]
|
|
|
|
|
if ec.Version != extensionEncryptedClientHello {
|
|
|
|
|
s.Skip(int(ec.Length))
|
|
|
|
|
return true, echConfig{}, nil
|
|
|
|
|
}
|
|
|
|
|
if !s.ReadUint8(&ec.ConfigID) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"config_id"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if !s.ReadUint16(&ec.KemID) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"kem_id"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if !readUint16LengthPrefixed(&s, &ec.PublicKey) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"public_key"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
var cipherSuites cryptobyte.String
|
|
|
|
|
if !s.ReadUint16LengthPrefixed(&cipherSuites) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"cipher_suites"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
for !cipherSuites.Empty() {
|
|
|
|
|
var c echCipher
|
|
|
|
|
if !cipherSuites.ReadUint16(&c.KDFID) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"cipher_suites kdf_id"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if !cipherSuites.ReadUint16(&c.AEADID) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"cipher_suites aead_id"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c)
|
|
|
|
|
}
|
|
|
|
|
if !s.ReadUint8(&ec.MaxNameLength) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"maximum_name_length"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
var publicName cryptobyte.String
|
|
|
|
|
if !s.ReadUint8LengthPrefixed(&publicName) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"public_name"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
ec.PublicName = publicName
|
|
|
|
|
var extensions cryptobyte.String
|
|
|
|
|
if !s.ReadUint16LengthPrefixed(&extensions) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"extensions"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
for !extensions.Empty() {
|
|
|
|
|
var e echExtension
|
|
|
|
|
if !extensions.ReadUint16(&e.Type) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"extensions type"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return false, echConfig{}, &echConfigErr{"extensions data"}
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
ec.Extensions = append(ec.Extensions, e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false, ec, nil
|
|
|
|
|
}
|
|
|
|
|
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
// parseECHConfigList parses a draft-ietf-tls-esni-18 ECHConfigList, returning a
|
|
|
|
|
// slice of parsed ECHConfigs, in the same order they were parsed, or an error
|
|
|
|
|
// if the list is malformed.
|
|
|
|
|
func parseECHConfigList(data []byte) ([]echConfig, error) {
|
|
|
|
|
s := cryptobyte.String(data)
|
|
|
|
|
var length uint16
|
|
|
|
|
if !s.ReadUint16(&length) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return nil, errMalformedECHConfigList
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
|
|
|
|
if length != uint16(len(data)-2) {
|
2025-02-19 14:27:33 -08:00
|
|
|
return nil, errMalformedECHConfigList
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
|
|
|
|
var configs []echConfig
|
|
|
|
|
for len(s) > 0 {
|
2024-10-29 20:22:27 -07:00
|
|
|
if len(s) < 4 {
|
|
|
|
|
return nil, errors.New("tls: malformed ECHConfig")
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
2024-10-29 20:22:27 -07:00
|
|
|
configLen := uint16(s[2])<<8 | uint16(s[3])
|
|
|
|
|
skip, ec, err := parseECHConfig(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
2024-10-29 20:22:27 -07:00
|
|
|
s = s[configLen+4:]
|
|
|
|
|
if !skip {
|
|
|
|
|
configs = append(configs, ec)
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return configs, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-11 13:10:17 +01:00
|
|
|
func pickECHConfig(list []echConfig) (*echConfig, hpke.PublicKey, hpke.KDF, hpke.AEAD) {
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
for _, ec := range list {
|
|
|
|
|
if !validDNSName(string(ec.PublicName)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var unsupportedExt bool
|
|
|
|
|
for _, ext := range ec.Extensions {
|
|
|
|
|
// If high order bit is set to 1 the extension is mandatory.
|
|
|
|
|
// Since we don't support any extensions, if we see a mandatory
|
|
|
|
|
// bit, we skip the config.
|
|
|
|
|
if ext.Type&uint16(1<<15) != 0 {
|
|
|
|
|
unsupportedExt = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if unsupportedExt {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-11 13:10:17 +01:00
|
|
|
kem, err := hpke.NewKEM(ec.KemID)
|
2025-09-06 21:37:09 +02:00
|
|
|
if err != nil {
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
continue
|
|
|
|
|
}
|
2025-11-11 13:10:17 +01:00
|
|
|
pub, err := kem.NewPublicKey(ec.PublicKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// This is an error in the config, but killing the connection feels
|
|
|
|
|
// excessive.
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-09-06 21:37:09 +02:00
|
|
|
for _, cs := range ec.SymmetricCipherSuite {
|
|
|
|
|
// All of the supported AEADs and KDFs are fine, rather than
|
|
|
|
|
// imposing some sort of preference here, we just pick the first
|
|
|
|
|
// valid suite.
|
|
|
|
|
kdf, err := hpke.NewKDF(cs.KDFID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
aead, err := hpke.NewAEAD(cs.AEADID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-11 13:10:17 +01:00
|
|
|
return &ec, pub, kdf, aead
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-06 21:37:09 +02:00
|
|
|
return nil, nil, nil, nil
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func encodeInnerClientHello(inner *clientHelloMsg, maxNameLength int) ([]byte, error) {
|
|
|
|
|
h, err := inner.marshalMsg(true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
h = h[4:] // strip four byte prefix
|
|
|
|
|
|
|
|
|
|
var paddingLen int
|
|
|
|
|
if inner.serverName != "" {
|
|
|
|
|
paddingLen = max(0, maxNameLength-len(inner.serverName))
|
|
|
|
|
} else {
|
|
|
|
|
paddingLen = maxNameLength + 9
|
|
|
|
|
}
|
|
|
|
|
paddingLen = 31 - ((len(h) + paddingLen - 1) % 32)
|
|
|
|
|
|
|
|
|
|
return append(h, make([]byte, paddingLen)...), nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-29 20:22:27 -07:00
|
|
|
func skipUint8LengthPrefixed(s *cryptobyte.String) bool {
|
|
|
|
|
var skip uint8
|
|
|
|
|
if !s.ReadUint8(&skip) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return s.Skip(int(skip))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func skipUint16LengthPrefixed(s *cryptobyte.String) bool {
|
|
|
|
|
var skip uint16
|
|
|
|
|
if !s.ReadUint16(&skip) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return s.Skip(int(skip))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type rawExtension struct {
|
|
|
|
|
extType uint16
|
|
|
|
|
data []byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractRawExtensions(hello *clientHelloMsg) ([]rawExtension, error) {
|
|
|
|
|
s := cryptobyte.String(hello.original)
|
|
|
|
|
if !s.Skip(4+2+32) || // header, version, random
|
|
|
|
|
!skipUint8LengthPrefixed(&s) || // session ID
|
|
|
|
|
!skipUint16LengthPrefixed(&s) || // cipher suites
|
|
|
|
|
!skipUint8LengthPrefixed(&s) { // compression methods
|
|
|
|
|
return nil, errors.New("tls: malformed outer client hello")
|
|
|
|
|
}
|
|
|
|
|
var rawExtensions []rawExtension
|
|
|
|
|
var extensions cryptobyte.String
|
|
|
|
|
if !s.ReadUint16LengthPrefixed(&extensions) {
|
|
|
|
|
return nil, errors.New("tls: malformed outer client hello")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for !extensions.Empty() {
|
|
|
|
|
var extension uint16
|
|
|
|
|
var extData cryptobyte.String
|
|
|
|
|
if !extensions.ReadUint16(&extension) ||
|
|
|
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
|
|
|
return nil, errors.New("tls: invalid inner client hello")
|
|
|
|
|
}
|
|
|
|
|
rawExtensions = append(rawExtensions, rawExtension{extension, extData})
|
|
|
|
|
}
|
|
|
|
|
return rawExtensions, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func decodeInnerClientHello(outer *clientHelloMsg, encoded []byte) (*clientHelloMsg, error) {
|
|
|
|
|
// Reconstructing the inner client hello from its encoded form is somewhat
|
|
|
|
|
// complicated. It is missing its header (message type and length), session
|
|
|
|
|
// ID, and the extensions may be compressed. Since we need to put the
|
|
|
|
|
// extensions back in the same order as they were in the raw outer hello,
|
|
|
|
|
// and since we don't store the raw extensions, or the order we parsed them
|
|
|
|
|
// in, we need to reparse the raw extensions from the outer hello in order
|
|
|
|
|
// to properly insert them into the inner hello. This _should_ result in raw
|
|
|
|
|
// bytes which match the hello as it was generated by the client.
|
|
|
|
|
innerReader := cryptobyte.String(encoded)
|
|
|
|
|
var versionAndRandom, sessionID, cipherSuites, compressionMethods []byte
|
|
|
|
|
var extensions cryptobyte.String
|
|
|
|
|
if !innerReader.ReadBytes(&versionAndRandom, 2+32) ||
|
|
|
|
|
!readUint8LengthPrefixed(&innerReader, &sessionID) ||
|
|
|
|
|
len(sessionID) != 0 ||
|
|
|
|
|
!readUint16LengthPrefixed(&innerReader, &cipherSuites) ||
|
|
|
|
|
!readUint8LengthPrefixed(&innerReader, &compressionMethods) ||
|
|
|
|
|
!innerReader.ReadUint16LengthPrefixed(&extensions) {
|
|
|
|
|
return nil, errors.New("tls: invalid inner client hello")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The specification says we must verify that the trailing padding is all
|
|
|
|
|
// zeros. This is kind of weird for TLS messages, where we generally just
|
|
|
|
|
// throw away any trailing garbage.
|
|
|
|
|
for _, p := range innerReader {
|
|
|
|
|
if p != 0 {
|
|
|
|
|
return nil, errors.New("tls: invalid inner client hello")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rawOuterExts, err := extractRawExtensions(outer)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recon := cryptobyte.NewBuilder(nil)
|
|
|
|
|
recon.AddUint8(typeClientHello)
|
|
|
|
|
recon.AddUint24LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
recon.AddBytes(versionAndRandom)
|
|
|
|
|
recon.AddUint8LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
recon.AddBytes(outer.sessionId)
|
|
|
|
|
})
|
|
|
|
|
recon.AddUint16LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
recon.AddBytes(cipherSuites)
|
|
|
|
|
})
|
|
|
|
|
recon.AddUint8LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
recon.AddBytes(compressionMethods)
|
|
|
|
|
})
|
|
|
|
|
recon.AddUint16LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
for !extensions.Empty() {
|
|
|
|
|
var extension uint16
|
|
|
|
|
var extData cryptobyte.String
|
|
|
|
|
if !extensions.ReadUint16(&extension) ||
|
|
|
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
|
|
|
recon.SetError(errors.New("tls: invalid inner client hello"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if extension == extensionECHOuterExtensions {
|
|
|
|
|
if !extData.ReadUint8LengthPrefixed(&extData) {
|
|
|
|
|
recon.SetError(errors.New("tls: invalid inner client hello"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var i int
|
|
|
|
|
for !extData.Empty() {
|
|
|
|
|
var extType uint16
|
|
|
|
|
if !extData.ReadUint16(&extType) {
|
|
|
|
|
recon.SetError(errors.New("tls: invalid inner client hello"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if extType == extensionEncryptedClientHello {
|
|
|
|
|
recon.SetError(errors.New("tls: invalid outer extensions"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for ; i <= len(rawOuterExts); i++ {
|
|
|
|
|
if i == len(rawOuterExts) {
|
|
|
|
|
recon.SetError(errors.New("tls: invalid outer extensions"))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if rawOuterExts[i].extType == extType {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
recon.AddUint16(rawOuterExts[i].extType)
|
|
|
|
|
recon.AddUint16LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
recon.AddBytes(rawOuterExts[i].data)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
recon.AddUint16(extension)
|
|
|
|
|
recon.AddUint16LengthPrefixed(func(recon *cryptobyte.Builder) {
|
|
|
|
|
recon.AddBytes(extData)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
reconBytes, err := recon.Bytes()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
inner := &clientHelloMsg{}
|
|
|
|
|
if !inner.unmarshal(reconBytes) {
|
|
|
|
|
return nil, errors.New("tls: invalid reconstructed inner client hello")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !bytes.Equal(inner.encryptedClientHello, []byte{uint8(innerECHExt)}) {
|
2024-12-30 19:28:35 +00:00
|
|
|
return nil, errInvalidECHExt
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
|
2025-02-10 12:49:15 +08:00
|
|
|
hasTLS13 := false
|
|
|
|
|
for _, v := range inner.supportedVersions {
|
|
|
|
|
// Skip GREASE values (values of the form 0x?A0A).
|
|
|
|
|
// GREASE (Generate Random Extensions And Sustain Extensibility) is a mechanism used by
|
|
|
|
|
// browsers like Chrome to ensure TLS implementations correctly ignore unknown values.
|
|
|
|
|
// GREASE values follow a specific pattern: 0x?A0A, where ? can be any hex digit.
|
|
|
|
|
// These values should be ignored when processing supported TLS versions.
|
|
|
|
|
if v&0x0F0F == 0x0A0A && v&0xff == v>>8 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure at least TLS 1.3 is offered.
|
|
|
|
|
if v == VersionTLS13 {
|
|
|
|
|
hasTLS13 = true
|
|
|
|
|
} else if v < VersionTLS13 {
|
|
|
|
|
// Reject if any non-GREASE value is below TLS 1.3, as ECH requires TLS 1.3+.
|
|
|
|
|
return nil, errors.New("tls: client sent encrypted_client_hello extension with unsupported versions")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !hasTLS13 {
|
|
|
|
|
return nil, errors.New("tls: client sent encrypted_client_hello extension but did not offer TLS 1.3")
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return inner, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 21:08:06 +00:00
|
|
|
func decryptECHPayload(context *hpke.Recipient, hello, payload []byte) ([]byte, error) {
|
2024-10-29 20:22:27 -07:00
|
|
|
outerAAD := bytes.Replace(hello[4:], payload, make([]byte, len(payload)), 1)
|
|
|
|
|
return context.Open(outerAAD, payload)
|
|
|
|
|
}
|
|
|
|
|
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
func generateOuterECHExt(id uint8, kdfID, aeadID uint16, encodedKey []byte, payload []byte) ([]byte, error) {
|
|
|
|
|
var b cryptobyte.Builder
|
|
|
|
|
b.AddUint8(0) // outer
|
|
|
|
|
b.AddUint16(kdfID)
|
|
|
|
|
b.AddUint16(aeadID)
|
|
|
|
|
b.AddUint8(id)
|
|
|
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(encodedKey) })
|
|
|
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(payload) })
|
|
|
|
|
return b.Bytes()
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-29 20:22:27 -07:00
|
|
|
func computeAndUpdateOuterECHExtension(outer, inner *clientHelloMsg, ech *echClientContext, useKey bool) error {
|
crypto/tls: add ech client support
This CL adds a (very opinionated) client-side ECH implementation.
In particular, if a user configures a ECHConfigList, by setting the
Config.EncryptedClientHelloConfigList, but we determine that none of
the configs are appropriate, we will not fallback to plaintext SNI, and
will instead return an error. It is then up to the user to decide if
they wish to fallback to plaintext themselves (by removing the config
list).
Additionally if Config.EncryptedClientHelloConfigList is provided, we
will not offer TLS support lower than 1.3, since negotiating any other
version, while offering ECH, is a hard error anyway. Similarly, if a
user wishes to fallback to plaintext SNI by using 1.2, they may do so
by removing the config list.
With regard to PSK GREASE, we match the boringssl behavior, which does
not include PSK identities/binders in the outer hello when doing ECH.
If the server rejects ECH, we will return a ECHRejectionError error,
which, if provided by the server, will contain a ECHConfigList in the
RetryConfigList field containing configs that should be used if the user
wishes to retry. It is up to the user to replace their existing
Config.EncryptedClientHelloConfigList with the retry config list.
Fixes #63369
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest
Change-Id: I9bc373c044064221a647a388ac61624efd6bbdbf
Reviewed-on: https://go-review.googlesource.com/c/go/+/578575
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Than McIntosh <thanm@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2024-04-11 08:50:36 -07:00
|
|
|
var encapKey []byte
|
|
|
|
|
if useKey {
|
|
|
|
|
encapKey = ech.encapsulatedKey
|
|
|
|
|
}
|
|
|
|
|
encodedInner, err := encodeInnerClientHello(inner, int(ech.config.MaxNameLength))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
// NOTE: the tag lengths for all of the supported AEADs are the same (16
|
|
|
|
|
// bytes), so we have hardcoded it here. If we add support for another AEAD
|
|
|
|
|
// with a different tag length, we will need to change this.
|
|
|
|
|
encryptedLen := len(encodedInner) + 16 // AEAD tag length
|
|
|
|
|
outer.encryptedClientHello, err = generateOuterECHExt(ech.config.ConfigID, ech.kdfID, ech.aeadID, encapKey, make([]byte, encryptedLen))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
serializedOuter, err := outer.marshal()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
serializedOuter = serializedOuter[4:] // strip the four byte prefix
|
|
|
|
|
encryptedInner, err := ech.hpkeContext.Seal(serializedOuter, encodedInner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
outer.encryptedClientHello, err = generateOuterECHExt(ech.config.ConfigID, ech.kdfID, ech.aeadID, encapKey, encryptedInner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// validDNSName is a rather rudimentary check for the validity of a DNS name.
|
|
|
|
|
// This is used to check if the public_name in a ECHConfig is valid when we are
|
|
|
|
|
// picking a config. This can be somewhat lax because even if we pick a
|
|
|
|
|
// valid-looking name, the DNS layer will later reject it anyway.
|
|
|
|
|
func validDNSName(name string) bool {
|
|
|
|
|
if len(name) > 253 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
labels := strings.Split(name, ".")
|
|
|
|
|
if len(labels) <= 1 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, l := range labels {
|
|
|
|
|
labelLen := len(l)
|
|
|
|
|
if labelLen == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for i, r := range l {
|
|
|
|
|
if r == '-' && (i == 0 || i == labelLen-1) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (r < '0' || r > '9') && (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '-' {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ECHRejectionError is the error type returned when ECH is rejected by a remote
|
|
|
|
|
// server. If the server offered a ECHConfigList to use for retries, the
|
|
|
|
|
// RetryConfigList field will contain this list.
|
|
|
|
|
//
|
|
|
|
|
// The client may treat an ECHRejectionError with an empty set of RetryConfigs
|
|
|
|
|
// as a secure signal from the server.
|
|
|
|
|
type ECHRejectionError struct {
|
|
|
|
|
RetryConfigList []byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *ECHRejectionError) Error() string {
|
|
|
|
|
return "tls: server rejected ECH"
|
|
|
|
|
}
|
2024-10-29 20:22:27 -07:00
|
|
|
|
|
|
|
|
var errMalformedECHExt = errors.New("tls: malformed encrypted_client_hello extension")
|
2024-12-30 19:28:35 +00:00
|
|
|
var errInvalidECHExt = errors.New("tls: client sent invalid encrypted_client_hello extension")
|
2024-10-29 20:22:27 -07:00
|
|
|
|
|
|
|
|
type echExtType uint8
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
innerECHExt echExtType = 1
|
|
|
|
|
outerECHExt echExtType = 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func parseECHExt(ext []byte) (echType echExtType, cs echCipher, configID uint8, encap []byte, payload []byte, err error) {
|
|
|
|
|
data := make([]byte, len(ext))
|
|
|
|
|
copy(data, ext)
|
|
|
|
|
s := cryptobyte.String(data)
|
|
|
|
|
var echInt uint8
|
|
|
|
|
if !s.ReadUint8(&echInt) {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
echType = echExtType(echInt)
|
|
|
|
|
if echType == innerECHExt {
|
|
|
|
|
if !s.Empty() {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
return echType, cs, 0, nil, nil, nil
|
|
|
|
|
}
|
|
|
|
|
if echType != outerECHExt {
|
2024-12-30 19:28:35 +00:00
|
|
|
err = errInvalidECHExt
|
2024-10-29 20:22:27 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !s.ReadUint16(&cs.KDFID) {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !s.ReadUint16(&cs.AEADID) {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !s.ReadUint8(&configID) {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !readUint16LengthPrefixed(&s, &encap) {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !readUint16LengthPrefixed(&s, &payload) {
|
|
|
|
|
err = errMalformedECHExt
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: clone encap and payload so that mutating them does not mutate the
|
|
|
|
|
// raw extension bytes.
|
|
|
|
|
return echType, cs, configID, bytes.Clone(encap), bytes.Clone(payload), nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 11:37:52 -07:00
|
|
|
func (c *Conn) processECHClientHello(outer *clientHelloMsg, echKeys []EncryptedClientHelloKey) (*clientHelloMsg, *echServerContext, error) {
|
2024-10-29 20:22:27 -07:00
|
|
|
echType, echCiphersuite, configID, encap, payload, err := parseECHExt(outer.encryptedClientHello)
|
|
|
|
|
if err != nil {
|
2024-12-30 19:28:35 +00:00
|
|
|
if errors.Is(err, errInvalidECHExt) {
|
|
|
|
|
c.sendAlert(alertIllegalParameter)
|
|
|
|
|
} else {
|
|
|
|
|
c.sendAlert(alertDecodeError)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, nil, errInvalidECHExt
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if echType == innerECHExt {
|
|
|
|
|
return outer, &echServerContext{inner: true}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 11:37:52 -07:00
|
|
|
if len(echKeys) == 0 {
|
2024-10-29 20:22:27 -07:00
|
|
|
return outer, nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-07 11:37:52 -07:00
|
|
|
for _, echKey := range echKeys {
|
2024-10-29 20:22:27 -07:00
|
|
|
skip, config, err := parseECHConfig(echKey.Config)
|
|
|
|
|
if err != nil || skip {
|
|
|
|
|
c.sendAlert(alertInternalError)
|
2025-09-06 21:37:09 +02:00
|
|
|
return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config: %s", err)
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
if skip {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-11-11 13:10:17 +01:00
|
|
|
kem, err := hpke.NewKEM(config.KemID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.sendAlert(alertInternalError)
|
|
|
|
|
return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config KEM: %s", err)
|
|
|
|
|
}
|
|
|
|
|
echPriv, err := kem.NewPrivateKey(echKey.PrivateKey)
|
2025-09-06 21:37:09 +02:00
|
|
|
if err != nil {
|
|
|
|
|
c.sendAlert(alertInternalError)
|
|
|
|
|
return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey PrivateKey: %s", err)
|
|
|
|
|
}
|
|
|
|
|
kdf, err := hpke.NewKDF(echCiphersuite.KDFID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.sendAlert(alertInternalError)
|
|
|
|
|
return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config KDF: %s", err)
|
|
|
|
|
}
|
|
|
|
|
aead, err := hpke.NewAEAD(echCiphersuite.AEADID)
|
2024-10-29 20:22:27 -07:00
|
|
|
if err != nil {
|
|
|
|
|
c.sendAlert(alertInternalError)
|
2025-09-06 21:37:09 +02:00
|
|
|
return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config AEAD: %s", err)
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
info := append([]byte("tls ech\x00"), echKey.Config...)
|
2025-09-06 21:37:09 +02:00
|
|
|
hpkeContext, err := hpke.NewRecipient(encap, echPriv, kdf, aead, info)
|
2024-10-29 20:22:27 -07:00
|
|
|
if err != nil {
|
|
|
|
|
// attempt next trial decryption
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
encodedInner, err := decryptECHPayload(hpkeContext, outer.original, payload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// attempt next trial decryption
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOTE: we do not enforce that the sent server_name matches the ECH
|
|
|
|
|
// configs PublicName, since this is not particularly important, and
|
|
|
|
|
// the client already had to know what it was in order to properly
|
|
|
|
|
// encrypt the payload. This is only a MAY in the spec, so we're not
|
|
|
|
|
// doing anything revolutionary.
|
|
|
|
|
|
|
|
|
|
echInner, err := decodeInnerClientHello(outer, encodedInner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.sendAlert(alertIllegalParameter)
|
2024-12-30 19:28:35 +00:00
|
|
|
return nil, nil, errInvalidECHExt
|
2024-10-29 20:22:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.echAccepted = true
|
|
|
|
|
|
|
|
|
|
return echInner, &echServerContext{
|
|
|
|
|
hpkeContext: hpkeContext,
|
|
|
|
|
configID: configID,
|
|
|
|
|
ciphersuite: echCiphersuite,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return outer, nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildRetryConfigList(keys []EncryptedClientHelloKey) ([]byte, error) {
|
|
|
|
|
var atLeastOneRetryConfig bool
|
|
|
|
|
var retryBuilder cryptobyte.Builder
|
|
|
|
|
retryBuilder.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
|
|
|
for _, c := range keys {
|
|
|
|
|
if !c.SendAsRetry {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
atLeastOneRetryConfig = true
|
|
|
|
|
b.AddBytes(c.Config)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if !atLeastOneRetryConfig {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
return retryBuilder.Bytes()
|
|
|
|
|
}
|