blob: 7bf68589f8702c474da6eb5838e1f7ee493aab21 [file] [log] [blame]
// 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 (
"crypto/internal/hpke"
"errors"
"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
}
var errMalformedECHConfig = errors.New("tls: malformed ECHConfigList")
// 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)
// Skip the length prefix
var length uint16
if !s.ReadUint16(&length) {
return nil, errMalformedECHConfig
}
if length != uint16(len(data)-2) {
return nil, errMalformedECHConfig
}
var configs []echConfig
for len(s) > 0 {
var ec echConfig
ec.raw = []byte(s)
if !s.ReadUint16(&ec.Version) {
return nil, errMalformedECHConfig
}
if !s.ReadUint16(&ec.Length) {
return nil, errMalformedECHConfig
}
if len(ec.raw) < int(ec.Length)+4 {
return nil, errMalformedECHConfig
}
ec.raw = ec.raw[:ec.Length+4]
if ec.Version != extensionEncryptedClientHello {
s.Skip(int(ec.Length))
continue
}
if !s.ReadUint8(&ec.ConfigID) {
return nil, errMalformedECHConfig
}
if !s.ReadUint16(&ec.KemID) {
return nil, errMalformedECHConfig
}
if !s.ReadUint16LengthPrefixed((*cryptobyte.String)(&ec.PublicKey)) {
return nil, errMalformedECHConfig
}
var cipherSuites cryptobyte.String
if !s.ReadUint16LengthPrefixed(&cipherSuites) {
return nil, errMalformedECHConfig
}
for !cipherSuites.Empty() {
var c echCipher
if !cipherSuites.ReadUint16(&c.KDFID) {
return nil, errMalformedECHConfig
}
if !cipherSuites.ReadUint16(&c.AEADID) {
return nil, errMalformedECHConfig
}
ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c)
}
if !s.ReadUint8(&ec.MaxNameLength) {
return nil, errMalformedECHConfig
}
var publicName cryptobyte.String
if !s.ReadUint8LengthPrefixed(&publicName) {
return nil, errMalformedECHConfig
}
ec.PublicName = publicName
var extensions cryptobyte.String
if !s.ReadUint16LengthPrefixed(&extensions) {
return nil, errMalformedECHConfig
}
for !extensions.Empty() {
var e echExtension
if !extensions.ReadUint16(&e.Type) {
return nil, errMalformedECHConfig
}
if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) {
return nil, errMalformedECHConfig
}
ec.Extensions = append(ec.Extensions, e)
}
configs = append(configs, ec)
}
return configs, nil
}
func pickECHConfig(list []echConfig) *echConfig {
for _, ec := range list {
if _, ok := hpke.SupportedKEMs[ec.KemID]; !ok {
continue
}
var validSCS bool
for _, cs := range ec.SymmetricCipherSuite {
if _, ok := hpke.SupportedAEADs[cs.AEADID]; !ok {
continue
}
if _, ok := hpke.SupportedKDFs[cs.KDFID]; !ok {
continue
}
validSCS = true
break
}
if !validSCS {
continue
}
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
}
return &ec
}
return nil
}
func pickECHCipherSuite(suites []echCipher) (echCipher, error) {
for _, s := range suites {
// NOTE: all of the supported AEADs and KDFs are fine, rather than
// imposing some sort of preference here, we just pick the first valid
// suite.
if _, ok := hpke.SupportedAEADs[s.AEADID]; !ok {
continue
}
if _, ok := hpke.SupportedKDFs[s.KDFID]; !ok {
continue
}
return s, nil
}
return echCipher{}, errors.New("tls: no supported symmetric ciphersuites for ECH")
}
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
}
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()
}
func computeAndUpdateOuterECHExtension(outer, inner *clientHelloMsg, ech *echContext, useKey bool) error {
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"
}