| // 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" |
| } |