// 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 ( "errors" "fmt" "io" "github.com/cloudflare/circl/hpke" "github.com/cloudflare/circl/kem" "golang.org/x/crypto/cryptobyte" ) // ECHConfig represents an ECH configuration. type ECHConfig struct { pk kem.PublicKey raw []byte // Parsed from raw version uint16 configId uint8 rawPublicName []byte rawPublicKey []byte kemId uint16 suites []hpkeSymmetricCipherSuite maxNameLen uint8 ignoredExtensions []byte } // UnmarshalECHConfigs parses a sequence of ECH configurations. func UnmarshalECHConfigs(raw []byte) ([]ECHConfig, error) { var ( err error config ECHConfig t, contents cryptobyte.String ) configs := make([]ECHConfig, 0) s := cryptobyte.String(raw) if !s.ReadUint16LengthPrefixed(&t) || !s.Empty() { return configs, errors.New("error parsing configs") } raw = raw[2:] ConfigsLoop: for !t.Empty() { l := len(t) if !t.ReadUint16(&config.version) || !t.ReadUint16LengthPrefixed(&contents) { return nil, errors.New("error parsing config") } n := l - len(t) config.raw = raw[:n] raw = raw[n:] if config.version != extensionECH { continue ConfigsLoop } if !readConfigContents(&contents, &config) { return nil, errors.New("error parsing config contents") } kem := hpke.KEM(config.kemId) if !kem.IsValid() { continue ConfigsLoop } config.pk, err = kem.Scheme().UnmarshalBinaryPublicKey(config.rawPublicKey) if err != nil { return nil, fmt.Errorf("error parsing public key: %s", err) } configs = append(configs, config) } return configs, nil } func echMarshalConfigs(configs []ECHConfig) ([]byte, error) { var b cryptobyte.Builder b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { for _, config := range configs { if config.raw == nil { panic("config.raw not set") } b.AddBytes(config.raw) } }) return b.Bytes() } func readConfigContents(contents *cryptobyte.String, config *ECHConfig) bool { var t cryptobyte.String if !contents.ReadUint8(&config.configId) || !contents.ReadUint16(&config.kemId) || !contents.ReadUint16LengthPrefixed(&t) || !t.ReadBytes(&config.rawPublicKey, len(t)) || !contents.ReadUint16LengthPrefixed(&t) || len(t)%4 != 0 { return false } config.suites = nil for !t.Empty() { var kdfId, aeadId uint16 if !t.ReadUint16(&kdfId) || !t.ReadUint16(&aeadId) { // This indicates an internal bug. panic("internal error while parsing contents.cipher_suites") } config.suites = append(config.suites, hpkeSymmetricCipherSuite{kdfId, aeadId}) } if !contents.ReadUint8(&config.maxNameLen) || !contents.ReadUint8LengthPrefixed(&t) || !t.ReadBytes(&config.rawPublicName, len(t)) || !contents.ReadUint16LengthPrefixed(&t) || !t.ReadBytes(&config.ignoredExtensions, len(t)) || !contents.Empty() { return false } return true } // setupSealer generates the client's HPKE context for use with the ECH // extension. It returns the context and corresponding encapsulated key. func (config *ECHConfig) setupSealer(rand io.Reader) (enc []byte, sealer hpke.Sealer, err error) { if config.raw == nil { panic("config.raw not set") } hpkeSuite, err := config.selectSuite() if err != nil { return nil, nil, err } info := append(append([]byte(echHpkeInfoSetup), 0), config.raw...) sender, err := hpkeSuite.NewSender(config.pk, info) if err != nil { return nil, nil, err } return sender.Setup(rand) } // isPeerCipherSuiteSupported returns true if this configuration indicates // support for the given ciphersuite. func (config *ECHConfig) isPeerCipherSuiteSupported(suite hpkeSymmetricCipherSuite) bool { for _, configSuite := range config.suites { if suite == configSuite { return true } } return false } // selectSuite returns the first ciphersuite indicated by this // configuration that is supported by the caller. func (config *ECHConfig) selectSuite() (hpke.Suite, error) { for _, suite := range config.suites { hpkeSuite, err := hpkeAssembleSuite( config.kemId, suite.kdfId, suite.aeadId, ) if err == nil { return hpkeSuite, nil } } return hpke.Suite{}, errors.New("could not negotiate a ciphersuite") }