mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-29 12:01:29 +00:00
1095 lines
33 KiB
Go
1095 lines
33 KiB
Go
// Copyright 2020 Cloudflare, Inc. 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 (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/cloudflare/circl/hpke"
|
|
"golang.org/x/crypto/cryptobyte"
|
|
)
|
|
|
|
const (
|
|
// Constants for TLS operations
|
|
echAcceptConfLabel = "ech accept confirmation"
|
|
echAcceptConfHRRLabel = "hrr ech accept confirmation"
|
|
|
|
// Constants for HPKE operations
|
|
echHpkeInfoSetup = "tls ech"
|
|
|
|
// When sent in the ClientHello, the first byte of the payload of the ECH
|
|
// extension indicates whether the message is the ClientHelloOuter or
|
|
// ClientHelloInner.
|
|
echClientHelloOuterVariant uint8 = 0
|
|
echClientHelloInnerVariant uint8 = 1
|
|
)
|
|
|
|
var zeros = [8]byte{}
|
|
|
|
// echOfferOrGrease is called by the client after generating its ClientHello
|
|
// message to decide if it will offer or GREASE ECH. It does neither if ECH is
|
|
// disabled. Returns a pair of ClientHello messages, hello and helloInner. If
|
|
// offering ECH, these are the ClienthelloOuter and ClientHelloInner
|
|
// respectively. Otherwise, hello is the ClientHello and helloInner == nil.
|
|
//
|
|
// TODO(cjpatton): "[When offering ECH, the client] MUST NOT offer to resume any
|
|
// session for TLS 1.2 and below [in ClientHelloInner]."
|
|
func (c *Conn) echOfferOrGrease(ctx context.Context, helloBase *clientHelloMsg) (hello, helloInner *clientHelloMsg, err error) {
|
|
config := c.config
|
|
|
|
if !config.ECHEnabled || testingECHTriggerBypassBeforeHRR {
|
|
// Bypass ECH.
|
|
return helloBase, nil, nil
|
|
}
|
|
|
|
// Choose the ECHConfig to use for this connection. If none is available, or
|
|
// if we're not offering TLS 1.3 or above, then GREASE.
|
|
echConfig, err := config.echSelectConfig(ctx, helloBase.serverName)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("tls: ech: fetch ech config: %s", err)
|
|
}
|
|
if echConfig == nil || config.maxSupportedVersion(roleClient) < VersionTLS13 {
|
|
var err error
|
|
|
|
// Generate a dummy ClientECH.
|
|
helloBase.ech, err = echGenerateGreaseExt(config.rand())
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("tls: ech: failed to generate grease ECH: %s", err)
|
|
}
|
|
|
|
// GREASE ECH.
|
|
c.ech.offered = false
|
|
c.ech.greased = true
|
|
helloBase.raw = nil
|
|
return helloBase, nil, nil
|
|
}
|
|
|
|
// Store the ECH config parameters that are needed later.
|
|
c.ech.configId = echConfig.configId
|
|
c.ech.maxNameLen = int(echConfig.maxNameLen)
|
|
|
|
// Generate the HPKE context. Store it in case of HRR.
|
|
var enc []byte
|
|
enc, c.ech.sealer, err = echConfig.setupSealer(config.rand())
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("tls: ech: %s", err)
|
|
}
|
|
|
|
// ClientHelloInner is constructed from the base ClientHello. The payload of
|
|
// the "encrypted_client_hello" extension is a single 1 byte indicating that
|
|
// this is the ClientHelloInner.
|
|
helloInner = helloBase
|
|
helloInner.ech = []byte{echClientHelloInnerVariant}
|
|
|
|
// Ensure that only TLS 1.3 and above are offered in the inner handshake.
|
|
if v := helloInner.supportedVersions; len(v) == 0 || v[len(v)-1] < VersionTLS13 {
|
|
return nil, nil, errors.New("tls: ech: only TLS 1.3 is allowed in ClientHelloInner")
|
|
}
|
|
|
|
// ClientHelloOuter is constructed by generating a fresh ClientHello and
|
|
// copying "session_id" from ClientHelloInner, setting "server_name" to the
|
|
// client-facing server, and adding the "encrypted_client_hello" extension.
|
|
//
|
|
// In addition, we discard the "key_share" and instead use the one from
|
|
// ClientHelloInner.
|
|
hello, _, err = c.makeClientHello(config.MinVersion)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("tls: ech: %s", err)
|
|
}
|
|
hello.sessionId = helloBase.sessionId
|
|
hello.serverName = hostnameInSNI(string(echConfig.rawPublicName))
|
|
if err := c.echUpdateClientHelloOuter(hello, helloInner, enc); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Offer ECH.
|
|
c.ech.offered = true
|
|
helloInner.raw = nil
|
|
hello.raw = nil
|
|
return hello, helloInner, nil
|
|
}
|
|
|
|
// echUpdateClientHelloOuter is called by the client to construct the payload of
|
|
// the ECH extension in the outer handshake.
|
|
func (c *Conn) echUpdateClientHelloOuter(hello, helloInner *clientHelloMsg, enc []byte) error {
|
|
var (
|
|
ech echClientOuter
|
|
err error
|
|
)
|
|
|
|
// Copy all compressed extensions from ClientHelloInner into
|
|
// ClientHelloOuter.
|
|
for _, ext := range echOuterExtensions() {
|
|
echCopyExtensionFromClientHelloInner(hello, helloInner, ext)
|
|
}
|
|
|
|
// Always copy the "key_shares" extension from ClientHelloInner, regardless
|
|
// of whether it gets compressed.
|
|
hello.keyShares = helloInner.keyShares
|
|
|
|
_, kdf, aead := c.ech.sealer.Suite().Params()
|
|
ech.handle.suite.kdfId = uint16(kdf)
|
|
ech.handle.suite.aeadId = uint16(aead)
|
|
ech.handle.configId = c.ech.configId
|
|
ech.handle.enc = enc
|
|
|
|
// EncodedClientHelloInner
|
|
helloInner.raw = nil
|
|
encodedHelloInner := echEncodeClientHelloInner(
|
|
helloInner.marshal(),
|
|
len(helloInner.serverName),
|
|
c.ech.maxNameLen)
|
|
if encodedHelloInner == nil {
|
|
return errors.New("tls: ech: encoding of EncodedClientHelloInner failed")
|
|
}
|
|
|
|
// ClientHelloOuterAAD
|
|
hello.raw = nil
|
|
hello.ech = ech.marshal()
|
|
helloOuterAad := echEncodeClientHelloOuterAAD(hello.marshal(),
|
|
aead.CipherLen(uint(len(encodedHelloInner))))
|
|
if helloOuterAad == nil {
|
|
return errors.New("tls: ech: encoding of ClientHelloOuterAAD failed")
|
|
}
|
|
|
|
ech.payload, err = c.ech.sealer.Seal(encodedHelloInner, helloOuterAad)
|
|
if err != nil {
|
|
return fmt.Errorf("tls: ech: seal failed: %s", err)
|
|
}
|
|
if testingECHTriggerPayloadDecryptError {
|
|
ech.payload[0] ^= 0xff // Inauthentic ciphertext
|
|
}
|
|
ech.raw = nil
|
|
hello.ech = ech.marshal()
|
|
|
|
helloInner.raw = nil
|
|
hello.raw = nil
|
|
return nil
|
|
}
|
|
|
|
// echAcceptOrReject is called by the client-facing server to determine whether
|
|
// ECH was offered by the client, and if so, whether to accept or reject. The
|
|
// return value is the ClientHello that will be used for the connection.
|
|
//
|
|
// This function is called prior to processing the ClientHello. In case of
|
|
// HelloRetryRequest, it is also called before processing the second
|
|
// ClientHello. This is indicated by the afterHRR flag.
|
|
func (c *Conn) echAcceptOrReject(hello *clientHelloMsg, afterHRR bool) (*clientHelloMsg, error) {
|
|
config := c.config
|
|
p := config.ServerECHProvider
|
|
|
|
if !config.echCanAccept() {
|
|
// Bypass ECH.
|
|
return hello, nil
|
|
}
|
|
|
|
if len(hello.ech) > 0 { // The ECH extension is present
|
|
switch hello.ech[0] {
|
|
case echClientHelloInnerVariant: // inner handshake
|
|
if len(hello.ech) > 1 {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, errors.New("ech: inner handshake has non-empty payload")
|
|
}
|
|
|
|
// Continue as the backend server.
|
|
return hello, nil
|
|
case echClientHelloOuterVariant: // outer handshake
|
|
default:
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, errors.New("ech: inner handshake has non-empty payload")
|
|
}
|
|
} else {
|
|
if c.ech.offered {
|
|
// This occurs if the server accepted prior to HRR, but the client
|
|
// failed to send the ECH extension in the second ClientHelloOuter. This
|
|
// would cause ClientHelloOuter to be used after ClientHelloInner, which
|
|
// is illegal.
|
|
c.sendAlert(alertMissingExtension)
|
|
return nil, errors.New("ech: hrr: bypass after offer")
|
|
}
|
|
|
|
// Bypass ECH.
|
|
return hello, nil
|
|
}
|
|
|
|
if afterHRR && !c.ech.offered && !c.ech.greased {
|
|
// The client bypassed ECH prior to HRR, but not after. This could
|
|
// cause ClientHelloInner to be used after ClientHelloOuter, which is
|
|
// illegal.
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, errors.New("ech: hrr: offer or grease after bypass")
|
|
}
|
|
|
|
// Parse ClientECH.
|
|
ech, err := echUnmarshalClientOuter(hello.ech)
|
|
if err != nil {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, fmt.Errorf("ech: failed to parse extension: %s", err)
|
|
}
|
|
|
|
// Make sure that the HPKE suite and config id don't change across HRR and
|
|
// that the encapsulated key is not present after HRR.
|
|
if afterHRR && c.ech.offered {
|
|
_, kdf, aead := c.ech.opener.Suite().Params()
|
|
if ech.handle.suite.kdfId != uint16(kdf) ||
|
|
ech.handle.suite.aeadId != uint16(aead) ||
|
|
ech.handle.configId != c.ech.configId ||
|
|
len(ech.handle.enc) > 0 {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, errors.New("ech: hrr: illegal handle in second hello")
|
|
}
|
|
}
|
|
|
|
// Store the config id in case of HRR.
|
|
c.ech.configId = ech.handle.configId
|
|
|
|
// Ask the ECH provider for the HPKE context.
|
|
if c.ech.opener == nil {
|
|
res := p.GetDecryptionContext(ech.handle.marshal(), extensionECH)
|
|
|
|
// Compute retry configurations, skipping those indicating an
|
|
// unsupported version.
|
|
if len(res.RetryConfigs) > 0 {
|
|
configs, err := UnmarshalECHConfigs(res.RetryConfigs) // skips unrecognized versions
|
|
if err != nil {
|
|
c.sendAlert(alertInternalError)
|
|
return nil, fmt.Errorf("ech: %s", err)
|
|
}
|
|
|
|
if len(configs) > 0 {
|
|
c.ech.retryConfigs, err = echMarshalConfigs(configs)
|
|
if err != nil {
|
|
c.sendAlert(alertInternalError)
|
|
return nil, fmt.Errorf("ech: %s", err)
|
|
}
|
|
}
|
|
|
|
// Check if the outer SNI matches the public name of any ECH config
|
|
// advertised by the client-facing server. As of
|
|
// draft-ietf-tls-esni-10, the client is required to use the ECH
|
|
// config's public name as the outer SNI. Although there's no real
|
|
// reason for the server to enforce this, it's worth noting it when
|
|
// it happens.
|
|
pubNameMatches := false
|
|
for _, config := range configs {
|
|
if hello.serverName == string(config.rawPublicName) {
|
|
pubNameMatches = true
|
|
}
|
|
}
|
|
if !pubNameMatches {
|
|
c.handleCFEvent(CFEventECHPublicNameMismatch{})
|
|
}
|
|
}
|
|
|
|
switch res.Status {
|
|
case ECHProviderSuccess:
|
|
c.ech.opener, err = hpke.UnmarshalOpener(res.Context)
|
|
if err != nil {
|
|
c.sendAlert(alertInternalError)
|
|
return nil, fmt.Errorf("ech: %s", err)
|
|
}
|
|
case ECHProviderReject:
|
|
// Reject ECH. We do not know at this point whether the client
|
|
// intended to offer or grease ECH, so we presume grease until the
|
|
// client indicates rejection by sending an "ech_required" alert.
|
|
c.ech.greased = true
|
|
return hello, nil
|
|
case ECHProviderAbort:
|
|
c.sendAlert(alert(res.Alert))
|
|
return nil, fmt.Errorf("ech: provider aborted: %s", res.Error)
|
|
default:
|
|
c.sendAlert(alertInternalError)
|
|
return nil, errors.New("ech: unexpected provider status")
|
|
}
|
|
}
|
|
|
|
// ClientHelloOuterAAD
|
|
rawHelloOuterAad := echEncodeClientHelloOuterAAD(hello.marshal(), uint(len(ech.payload)))
|
|
if rawHelloOuterAad == nil {
|
|
// This occurs if the ClientHelloOuter is malformed. This values was
|
|
// already parsed into `hello`, so this should not happen.
|
|
c.sendAlert(alertInternalError)
|
|
return nil, fmt.Errorf("ech: failed to encode ClientHelloOuterAAD")
|
|
}
|
|
|
|
// EncodedClientHelloInner
|
|
rawEncodedHelloInner, err := c.ech.opener.Open(ech.payload, rawHelloOuterAad)
|
|
if err != nil {
|
|
if afterHRR && c.ech.accepted {
|
|
// Don't reject after accept, as this would result in processing the
|
|
// ClientHelloOuter after processing the ClientHelloInner.
|
|
c.sendAlert(alertDecryptError)
|
|
return nil, fmt.Errorf("ech: hrr: reject after accept: %s", err)
|
|
}
|
|
|
|
// Reject ECH. We do not know at this point whether the client
|
|
// intended to offer or grease ECH, so we presume grease until the
|
|
// client indicates rejection by sending an "ech_required" alert.
|
|
c.ech.greased = true
|
|
return hello, nil
|
|
}
|
|
|
|
// ClientHelloInner
|
|
rawHelloInner := echDecodeClientHelloInner(rawEncodedHelloInner, hello.marshal(), hello.sessionId)
|
|
if rawHelloInner == nil {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, fmt.Errorf("ech: failed to decode EncodedClientHelloInner")
|
|
}
|
|
helloInner := new(clientHelloMsg)
|
|
if !helloInner.unmarshal(rawHelloInner) {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, fmt.Errorf("ech: failed to parse ClientHelloInner")
|
|
}
|
|
|
|
// Check for a well-formed ECH extension.
|
|
if len(helloInner.ech) != 1 ||
|
|
helloInner.ech[0] != echClientHelloInnerVariant {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, fmt.Errorf("ech: ClientHelloInner does not have a well-formed ECH extension")
|
|
}
|
|
|
|
// Check that the client did not offer TLS 1.2 or below in the inner
|
|
// handshake.
|
|
helloInnerSupportsTLS12OrBelow := len(helloInner.supportedVersions) == 0
|
|
for _, v := range helloInner.supportedVersions {
|
|
if v < VersionTLS13 {
|
|
helloInnerSupportsTLS12OrBelow = true
|
|
}
|
|
}
|
|
if helloInnerSupportsTLS12OrBelow {
|
|
c.sendAlert(alertIllegalParameter)
|
|
return nil, errors.New("ech: ClientHelloInner offers TLS 1.2 or below")
|
|
}
|
|
|
|
// Accept ECH.
|
|
c.ech.offered = true
|
|
c.ech.accepted = true
|
|
return helloInner, nil
|
|
}
|
|
|
|
// echClientOuter represents a ClientECH structure, the payload of the client's
|
|
// "encrypted_client_hello" extension that appears in the outer handshake.
|
|
type echClientOuter struct {
|
|
raw []byte
|
|
|
|
// Parsed from raw
|
|
handle echContextHandle
|
|
payload []byte
|
|
}
|
|
|
|
// echUnmarshalClientOuter parses a ClientECH structure. The caller provides the
|
|
// ECH version indicated by the client.
|
|
func echUnmarshalClientOuter(raw []byte) (*echClientOuter, error) {
|
|
s := cryptobyte.String(raw)
|
|
ech := new(echClientOuter)
|
|
ech.raw = raw
|
|
|
|
// Make sure this is the outer handshake.
|
|
var variant uint8
|
|
if !s.ReadUint8(&variant) {
|
|
return nil, fmt.Errorf("error parsing ClientECH.type")
|
|
}
|
|
if variant != echClientHelloOuterVariant {
|
|
return nil, fmt.Errorf("unexpected ClientECH.type (want outer (0))")
|
|
}
|
|
|
|
// Parse the context handle.
|
|
if !echReadContextHandle(&s, &ech.handle) {
|
|
return nil, fmt.Errorf("error parsing context handle")
|
|
}
|
|
endOfContextHandle := len(raw) - len(s)
|
|
ech.handle.raw = raw[1:endOfContextHandle]
|
|
|
|
// Parse the payload.
|
|
var t cryptobyte.String
|
|
if !s.ReadUint16LengthPrefixed(&t) ||
|
|
!t.ReadBytes(&ech.payload, len(t)) || !s.Empty() {
|
|
return nil, fmt.Errorf("error parsing payload")
|
|
}
|
|
|
|
return ech, nil
|
|
}
|
|
|
|
func (ech *echClientOuter) marshal() []byte {
|
|
if ech.raw != nil {
|
|
return ech.raw
|
|
}
|
|
var b cryptobyte.Builder
|
|
b.AddUint8(echClientHelloOuterVariant)
|
|
b.AddBytes(ech.handle.marshal())
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(ech.payload)
|
|
})
|
|
return b.BytesOrPanic()
|
|
}
|
|
|
|
// echContextHandle represents the prefix of a ClientECH structure used by
|
|
// the server to compute the HPKE context.
|
|
type echContextHandle struct {
|
|
raw []byte
|
|
|
|
// Parsed from raw
|
|
suite hpkeSymmetricCipherSuite
|
|
configId uint8
|
|
enc []byte
|
|
}
|
|
|
|
func (handle *echContextHandle) marshal() []byte {
|
|
if handle.raw != nil {
|
|
return handle.raw
|
|
}
|
|
var b cryptobyte.Builder
|
|
b.AddUint16(handle.suite.kdfId)
|
|
b.AddUint16(handle.suite.aeadId)
|
|
b.AddUint8(handle.configId)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(handle.enc)
|
|
})
|
|
return b.BytesOrPanic()
|
|
}
|
|
|
|
func echReadContextHandle(s *cryptobyte.String, handle *echContextHandle) bool {
|
|
var t cryptobyte.String
|
|
if !s.ReadUint16(&handle.suite.kdfId) || // cipher_suite.kdf_id
|
|
!s.ReadUint16(&handle.suite.aeadId) || // cipher_suite.aead_id
|
|
!s.ReadUint8(&handle.configId) || // config_id
|
|
!s.ReadUint16LengthPrefixed(&t) || // enc
|
|
!t.ReadBytes(&handle.enc, len(t)) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// hpkeSymmetricCipherSuite represents an ECH ciphersuite, a KDF/AEAD algorithm pair. This
|
|
// is different from an HPKE ciphersuite, which represents a KEM/KDF/AEAD
|
|
// triple.
|
|
type hpkeSymmetricCipherSuite struct {
|
|
kdfId, aeadId uint16
|
|
}
|
|
|
|
// Generates a grease ECH extension using a hard-coded KEM public key.
|
|
func echGenerateGreaseExt(rand io.Reader) ([]byte, error) {
|
|
var err error
|
|
dummyX25519PublicKey := []byte{
|
|
143, 38, 37, 36, 12, 6, 229, 30, 140, 27, 167, 73, 26, 100, 203, 107, 216,
|
|
81, 163, 222, 52, 211, 54, 210, 46, 37, 78, 216, 157, 97, 241, 244,
|
|
}
|
|
dummyEncodedHelloInnerLen := 100 // TODO(cjpatton): Compute this correctly.
|
|
kem, kdf, aead := defaultHPKESuite.Params()
|
|
|
|
pk, err := kem.Scheme().UnmarshalBinaryPublicKey(dummyX25519PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tls: grease ech: failed to parse dummy public key: %s", err)
|
|
}
|
|
sender, err := defaultHPKESuite.NewSender(pk, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tls: grease ech: failed to create sender: %s", err)
|
|
}
|
|
|
|
var ech echClientOuter
|
|
ech.handle.suite.kdfId = uint16(kdf)
|
|
ech.handle.suite.aeadId = uint16(aead)
|
|
randomByte := make([]byte, 1)
|
|
_, err = io.ReadFull(rand, randomByte)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tls: grease ech: %s", err)
|
|
}
|
|
ech.handle.configId = randomByte[0]
|
|
ech.handle.enc, _, err = sender.Setup(rand)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tls: grease ech: %s", err)
|
|
}
|
|
ech.payload = make([]byte,
|
|
int(aead.CipherLen(uint(dummyEncodedHelloInnerLen))))
|
|
if _, err = io.ReadFull(rand, ech.payload); err != nil {
|
|
return nil, fmt.Errorf("tls: grease ech: %s", err)
|
|
}
|
|
return ech.marshal(), nil
|
|
}
|
|
|
|
// echEncodeClientHelloInner interprets innerData as a ClientHelloInner message
|
|
// and transforms it into an EncodedClientHelloInner. Returns nil if parsing
|
|
// innerData fails.
|
|
func echEncodeClientHelloInner(innerData []byte, serverNameLen, maxNameLen int) []byte {
|
|
var (
|
|
errIllegalParameter = errors.New("illegal parameter")
|
|
outerExtensions = echOuterExtensions()
|
|
msgType uint8
|
|
legacyVersion uint16
|
|
random []byte
|
|
legacySessionId cryptobyte.String
|
|
cipherSuites cryptobyte.String
|
|
legacyCompressionMethods cryptobyte.String
|
|
extensions cryptobyte.String
|
|
s cryptobyte.String
|
|
b cryptobyte.Builder
|
|
)
|
|
|
|
u := cryptobyte.String(innerData)
|
|
if !u.ReadUint8(&msgType) ||
|
|
!u.ReadUint24LengthPrefixed(&s) || !u.Empty() {
|
|
return nil
|
|
}
|
|
|
|
if !s.ReadUint16(&legacyVersion) ||
|
|
!s.ReadBytes(&random, 32) ||
|
|
!s.ReadUint8LengthPrefixed(&legacySessionId) ||
|
|
!s.ReadUint16LengthPrefixed(&cipherSuites) ||
|
|
!s.ReadUint8LengthPrefixed(&legacyCompressionMethods) {
|
|
return nil
|
|
}
|
|
|
|
if s.Empty() {
|
|
// Extensions field must be present in TLS 1.3.
|
|
return nil
|
|
}
|
|
|
|
if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() {
|
|
return nil
|
|
}
|
|
|
|
b.AddUint16(legacyVersion)
|
|
b.AddBytes(random)
|
|
b.AddUint8(0) // 0-length legacy_session_id
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(cipherSuites)
|
|
})
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(legacyCompressionMethods)
|
|
})
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
if testingECHOuterExtIncorrectOrder {
|
|
// Replace outer extensions with "outer_extension" extension, but in
|
|
// the incorrect order.
|
|
echAddOuterExtensions(b, outerExtensions)
|
|
}
|
|
|
|
for !extensions.Empty() {
|
|
var ext uint16
|
|
var extData cryptobyte.String
|
|
if !extensions.ReadUint16(&ext) ||
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
|
|
if len(outerExtensions) > 0 && ext == outerExtensions[0] {
|
|
if !testingECHOuterExtIncorrectOrder {
|
|
// Replace outer extensions with "outer_extension" extension.
|
|
echAddOuterExtensions(b, outerExtensions)
|
|
}
|
|
|
|
// Consume the remaining outer extensions.
|
|
for _, outerExt := range outerExtensions[1:] {
|
|
if !extensions.ReadUint16(&ext) ||
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
if ext != outerExt {
|
|
panic("internal error: malformed ClientHelloInner")
|
|
}
|
|
}
|
|
|
|
} else {
|
|
b.AddUint16(ext)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(extData)
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
encodedData, err := b.Bytes()
|
|
if err == errIllegalParameter {
|
|
return nil // Input malformed
|
|
} else if err != nil {
|
|
panic(err) // Host encountered internal error
|
|
}
|
|
|
|
// Add padding.
|
|
paddingLen := 0
|
|
if serverNameLen > 0 {
|
|
// draft-ietf-tls-esni-13, Section 6.1.3:
|
|
//
|
|
// If the ClientHelloInner contained a "server_name" extension with a
|
|
// name of length D, add max(0, L - D) bytes of padding.
|
|
if n := maxNameLen - serverNameLen; n > 0 {
|
|
paddingLen += n
|
|
}
|
|
} else {
|
|
// draft-ietf-tls-esni-13, Section 6.1.3:
|
|
//
|
|
// If the ClientHelloInner did not contain a "server_name" extension
|
|
// (e.g., if the client is connecting to an IP address), add L + 9 bytes
|
|
// of padding. This is the length of a "server_name" extension with an
|
|
// L-byte name.
|
|
const sniPaddingLen = 9
|
|
paddingLen += sniPaddingLen + maxNameLen
|
|
}
|
|
paddingLen = 31 - ((len(encodedData) + paddingLen - 1) % 32)
|
|
for i := 0; i < paddingLen; i++ {
|
|
encodedData = append(encodedData, 0)
|
|
}
|
|
|
|
return encodedData
|
|
}
|
|
|
|
func echAddOuterExtensions(b *cryptobyte.Builder, outerExtensions []uint16) {
|
|
b.AddUint16(extensionECHOuterExtensions)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
for _, outerExt := range outerExtensions {
|
|
b.AddUint16(outerExt)
|
|
}
|
|
if testingECHOuterExtIllegal {
|
|
// This is not allowed.
|
|
b.AddUint16(extensionECH)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// echDecodeClientHelloInner interprets encodedData as an EncodedClientHelloInner
|
|
// message and substitutes the "outer_extension" extension with extensions from
|
|
// outerData, interpreted as the ClientHelloOuter message. Returns nil if
|
|
// parsing encodedData fails.
|
|
func echDecodeClientHelloInner(encodedData, outerData, outerSessionId []byte) []byte {
|
|
var (
|
|
errIllegalParameter = errors.New("illegal parameter")
|
|
legacyVersion uint16
|
|
random []byte
|
|
legacySessionId cryptobyte.String
|
|
cipherSuites cryptobyte.String
|
|
legacyCompressionMethods cryptobyte.String
|
|
extensions cryptobyte.String
|
|
b cryptobyte.Builder
|
|
)
|
|
|
|
s := cryptobyte.String(encodedData)
|
|
if !s.ReadUint16(&legacyVersion) ||
|
|
!s.ReadBytes(&random, 32) ||
|
|
!s.ReadUint8LengthPrefixed(&legacySessionId) ||
|
|
!s.ReadUint16LengthPrefixed(&cipherSuites) ||
|
|
!s.ReadUint8LengthPrefixed(&legacyCompressionMethods) {
|
|
return nil
|
|
}
|
|
|
|
if len(legacySessionId) > 0 {
|
|
return nil
|
|
}
|
|
|
|
if s.Empty() {
|
|
// Extensions field must be present in TLS 1.3.
|
|
return nil
|
|
}
|
|
|
|
if !s.ReadUint16LengthPrefixed(&extensions) {
|
|
return nil
|
|
}
|
|
|
|
b.AddUint8(typeClientHello)
|
|
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddUint16(legacyVersion)
|
|
b.AddBytes(random)
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(outerSessionId) // ClientHelloOuter.legacy_session_id
|
|
})
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(cipherSuites)
|
|
})
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(legacyCompressionMethods)
|
|
})
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
var handledOuterExtensions bool
|
|
for !extensions.Empty() {
|
|
var ext uint16
|
|
var extData cryptobyte.String
|
|
if !extensions.ReadUint16(&ext) ||
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
|
|
if ext == extensionECHOuterExtensions {
|
|
if handledOuterExtensions {
|
|
// It is an error to send any extension more than once in a
|
|
// single message.
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
handledOuterExtensions = true
|
|
|
|
// Read the referenced outer extensions.
|
|
referencedExts := make([]uint16, 0, 10)
|
|
var outerExtData cryptobyte.String
|
|
if !extData.ReadUint8LengthPrefixed(&outerExtData) ||
|
|
len(outerExtData)%2 != 0 ||
|
|
!extData.Empty() {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
for !outerExtData.Empty() {
|
|
if !outerExtData.ReadUint16(&ext) ||
|
|
ext == extensionECH {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
referencedExts = append(referencedExts, ext)
|
|
}
|
|
|
|
// Add the outer extensions from the ClientHelloOuter into the
|
|
// ClientHelloInner.
|
|
outerCt := 0
|
|
r := processClientHelloExtensions(outerData, func(ext uint16, extData cryptobyte.String) bool {
|
|
if outerCt < len(referencedExts) && ext == referencedExts[outerCt] {
|
|
outerCt++
|
|
b.AddUint16(ext)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(extData)
|
|
})
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Ensure that all outer extensions have been incorporated
|
|
// exactly once, and in the correct order.
|
|
if !r || outerCt != len(referencedExts) {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
} else {
|
|
b.AddUint16(ext)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(extData)
|
|
})
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
innerData, err := b.Bytes()
|
|
if err == errIllegalParameter {
|
|
return nil // Input malformed
|
|
} else if err != nil {
|
|
panic(err) // Host encountered internal error
|
|
}
|
|
|
|
// Read the padding.
|
|
for !s.Empty() {
|
|
var zero uint8
|
|
if !s.ReadUint8(&zero) || zero != 0 {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return innerData
|
|
}
|
|
|
|
// echEncodeClientHelloOuterAAD interprets outerData as ClientHelloOuter and
|
|
// constructs a ClientHelloOuterAAD. The output doesn't have the 4-byte prefix
|
|
// that indicates the handshake message type and its length.
|
|
func echEncodeClientHelloOuterAAD(outerData []byte, payloadLen uint) []byte {
|
|
var (
|
|
errIllegalParameter = errors.New("illegal parameter")
|
|
msgType uint8
|
|
legacyVersion uint16
|
|
random []byte
|
|
legacySessionId cryptobyte.String
|
|
cipherSuites cryptobyte.String
|
|
legacyCompressionMethods cryptobyte.String
|
|
extensions cryptobyte.String
|
|
s cryptobyte.String
|
|
b cryptobyte.Builder
|
|
)
|
|
|
|
u := cryptobyte.String(outerData)
|
|
if !u.ReadUint8(&msgType) ||
|
|
!u.ReadUint24LengthPrefixed(&s) || !u.Empty() {
|
|
return nil
|
|
}
|
|
|
|
if !s.ReadUint16(&legacyVersion) ||
|
|
!s.ReadBytes(&random, 32) ||
|
|
!s.ReadUint8LengthPrefixed(&legacySessionId) ||
|
|
!s.ReadUint16LengthPrefixed(&cipherSuites) ||
|
|
!s.ReadUint8LengthPrefixed(&legacyCompressionMethods) {
|
|
return nil
|
|
}
|
|
|
|
if s.Empty() {
|
|
// Extensions field must be present in TLS 1.3.
|
|
return nil
|
|
}
|
|
|
|
if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() {
|
|
return nil
|
|
}
|
|
|
|
b.AddUint16(legacyVersion)
|
|
b.AddBytes(random)
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(legacySessionId)
|
|
})
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(cipherSuites)
|
|
})
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(legacyCompressionMethods)
|
|
})
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
for !extensions.Empty() {
|
|
var ext uint16
|
|
var extData cryptobyte.String
|
|
if !extensions.ReadUint16(&ext) ||
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
|
|
// If this is the ECH extension and the payload is the outer variant
|
|
// of ClientECH, then replace the payloadLen 0 bytes.
|
|
if ext == extensionECH {
|
|
ech, err := echUnmarshalClientOuter(extData)
|
|
if err != nil {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
ech.payload = make([]byte, payloadLen)
|
|
ech.raw = nil
|
|
extData = ech.marshal()
|
|
}
|
|
|
|
b.AddUint16(ext)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(extData)
|
|
})
|
|
}
|
|
})
|
|
|
|
outerAadData, err := b.Bytes()
|
|
if err == errIllegalParameter {
|
|
return nil // Input malformed
|
|
} else if err != nil {
|
|
panic(err) // Host encountered internal error
|
|
}
|
|
|
|
return outerAadData
|
|
}
|
|
|
|
// echEncodeAcceptConfHelloRetryRequest interprets data as a ServerHello message
|
|
// and replaces the payload of the ECH extension with 8 zero bytes. The output
|
|
// includes the 4-byte prefix that indicates the message type and its length.
|
|
func echEncodeAcceptConfHelloRetryRequest(data []byte) []byte {
|
|
var (
|
|
errIllegalParameter = errors.New("illegal parameter")
|
|
vers uint16
|
|
random []byte
|
|
sessionId []byte
|
|
cipherSuite uint16
|
|
compressionMethod uint8
|
|
s cryptobyte.String
|
|
b cryptobyte.Builder
|
|
)
|
|
|
|
s = cryptobyte.String(data)
|
|
if !s.Skip(4) || // message type and uint24 length field
|
|
!s.ReadUint16(&vers) || !s.ReadBytes(&random, 32) ||
|
|
!readUint8LengthPrefixed(&s, &sessionId) ||
|
|
!s.ReadUint16(&cipherSuite) ||
|
|
!s.ReadUint8(&compressionMethod) {
|
|
return nil
|
|
}
|
|
|
|
if s.Empty() {
|
|
// ServerHello is optionally followed by extension data
|
|
return nil
|
|
}
|
|
|
|
var extensions cryptobyte.String
|
|
if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() {
|
|
return nil
|
|
}
|
|
|
|
b.AddUint8(typeServerHello)
|
|
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddUint16(vers)
|
|
b.AddBytes(random)
|
|
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
b.AddBytes(sessionId)
|
|
})
|
|
b.AddUint16(cipherSuite)
|
|
b.AddUint8(compressionMethod)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
for !extensions.Empty() {
|
|
var extension uint16
|
|
var extData cryptobyte.String
|
|
if !extensions.ReadUint16(&extension) ||
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
panic(cryptobyte.BuildError{Err: errIllegalParameter})
|
|
}
|
|
|
|
b.AddUint16(extension)
|
|
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
|
if extension == extensionECH {
|
|
b.AddBytes(zeros[:8])
|
|
} else {
|
|
b.AddBytes(extData)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
|
|
encodedData, err := b.Bytes()
|
|
if err == errIllegalParameter {
|
|
return nil // Input malformed
|
|
} else if err != nil {
|
|
panic(err) // Host encountered internal error
|
|
}
|
|
|
|
return encodedData
|
|
}
|
|
|
|
// processClientHelloExtensions interprets data as a ClientHello and applies a
|
|
// function proc to each extension. Returns a bool indicating whether parsing
|
|
// succeeded.
|
|
func processClientHelloExtensions(data []byte, proc func(ext uint16, extData cryptobyte.String) bool) bool {
|
|
_, extensionsData := splitClientHelloExtensions(data)
|
|
if extensionsData == nil {
|
|
return false
|
|
}
|
|
|
|
s := cryptobyte.String(extensionsData)
|
|
if s.Empty() {
|
|
// Extensions field not present.
|
|
return true
|
|
}
|
|
|
|
var extensions cryptobyte.String
|
|
if !s.ReadUint16LengthPrefixed(&extensions) || !s.Empty() {
|
|
return false
|
|
}
|
|
|
|
for !extensions.Empty() {
|
|
var ext uint16
|
|
var extData cryptobyte.String
|
|
if !extensions.ReadUint16(&ext) ||
|
|
!extensions.ReadUint16LengthPrefixed(&extData) {
|
|
return false
|
|
}
|
|
if ok := proc(ext, extData); !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// splitClientHelloExtensions interprets data as a ClientHello message and
|
|
// returns two strings: the first contains the start of the ClientHello up to
|
|
// the start of the extensions; and the second is the length-prefixed
|
|
// extensions. Returns (nil, nil) if parsing of data fails.
|
|
func splitClientHelloExtensions(data []byte) ([]byte, []byte) {
|
|
s := cryptobyte.String(data)
|
|
|
|
var ignored uint16
|
|
var t cryptobyte.String
|
|
if !s.Skip(4) || // message type and uint24 length field
|
|
!s.ReadUint16(&ignored) || !s.Skip(32) || // vers, random
|
|
!s.ReadUint8LengthPrefixed(&t) { // session_id
|
|
return nil, nil
|
|
}
|
|
|
|
if !s.ReadUint16LengthPrefixed(&t) { // cipher_suites
|
|
return nil, nil
|
|
}
|
|
|
|
if !s.ReadUint8LengthPrefixed(&t) { // compression_methods
|
|
return nil, nil
|
|
}
|
|
|
|
return data[:len(data)-len(s)], s
|
|
}
|
|
|
|
// TODO(cjpatton): Handle public name as described in draft-ietf-tls-esni-13,
|
|
// Section 4.
|
|
//
|
|
// TODO(cjpatton): Implement ECH config extensions as described in
|
|
// draft-ietf-tls-esni-13, Section 4.1.
|
|
func (c *Config) echSelectConfig(ctx context.Context, serverName string) (*ECHConfig, error) {
|
|
for _, echConfig := range c.ClientECHConfigs {
|
|
if _, err := echConfig.selectSuite(); err == nil &&
|
|
echConfig.version == extensionECH {
|
|
return &echConfig, nil
|
|
}
|
|
}
|
|
if c.GetClientECHConfigs != nil {
|
|
echConfigs, err := c.GetClientECHConfigs(ctx, serverName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, echConfig := range echConfigs {
|
|
if _, err = echConfig.selectSuite(); err == nil &&
|
|
echConfig.version == extensionECH {
|
|
return &echConfig, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (c *Config) echCanOffer() bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
return c.ECHEnabled &&
|
|
c.maxSupportedVersion(roleClient) >= VersionTLS13
|
|
}
|
|
|
|
func (c *Config) echCanAccept() bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
return c.ECHEnabled &&
|
|
c.ServerECHProvider != nil &&
|
|
c.maxSupportedVersion(roleServer) >= VersionTLS13
|
|
}
|
|
|
|
// echOuterExtensions returns the list of extensions of the ClientHelloOuter
|
|
// that will be incorporated into the CleintHelloInner.
|
|
func echOuterExtensions() []uint16 {
|
|
// NOTE(cjpatton): It would be nice to incorporate more extensions, but
|
|
// "key_share" is the last extension to appear in the ClientHello before
|
|
// "pre_shared_key". As a result, the only contiguous sequence of outer
|
|
// extensions that contains "key_share" is "key_share" itself. Note that
|
|
// we cannot change the order of extensions in the ClientHello, as the
|
|
// unit tests expect "key_share" to be the second to last extension.
|
|
outerExtensions := []uint16{extensionKeyShare}
|
|
if testingECHOuterExtMany {
|
|
// NOTE(cjpatton): Incorporating this particular sequence does not
|
|
// yield significant savings. However, it's useful to test that our
|
|
// server correctly handles a sequence of compressed extensions and
|
|
// not just one.
|
|
outerExtensions = []uint16{
|
|
extensionStatusRequest,
|
|
extensionSupportedCurves,
|
|
extensionSupportedPoints,
|
|
}
|
|
} else if testingECHOuterExtNone {
|
|
outerExtensions = []uint16{}
|
|
}
|
|
|
|
return outerExtensions
|
|
}
|
|
|
|
func echCopyExtensionFromClientHelloInner(hello, helloInner *clientHelloMsg, ext uint16) {
|
|
switch ext {
|
|
case extensionStatusRequest:
|
|
hello.ocspStapling = helloInner.ocspStapling
|
|
case extensionSupportedCurves:
|
|
hello.supportedCurves = helloInner.supportedCurves
|
|
case extensionSupportedPoints:
|
|
hello.supportedPoints = helloInner.supportedPoints
|
|
case extensionKeyShare:
|
|
hello.keyShares = helloInner.keyShares
|
|
default:
|
|
panic(fmt.Errorf("tried to copy unrecognized extension: %04x", ext))
|
|
}
|
|
}
|