| |
| |
| |
|
|
| package tls |
|
|
| import ( |
| "bytes" |
| "crypto/hpke" |
| "errors" |
| "fmt" |
| "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 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) |
| } |
|
|
| func parseECHConfig(enc []byte) (skip bool, ec echConfig, err error) { |
| s := cryptobyte.String(enc) |
| ec.raw = []byte(enc) |
| if !s.ReadUint16(&ec.Version) { |
| return false, echConfig{}, &echConfigErr{"version"} |
| } |
| if !s.ReadUint16(&ec.Length) { |
| return false, echConfig{}, &echConfigErr{"length"} |
| } |
| if len(ec.raw) < int(ec.Length)+4 { |
| return false, echConfig{}, &echConfigErr{"length"} |
| } |
| 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) { |
| return false, echConfig{}, &echConfigErr{"config_id"} |
| } |
| if !s.ReadUint16(&ec.KemID) { |
| return false, echConfig{}, &echConfigErr{"kem_id"} |
| } |
| if !readUint16LengthPrefixed(&s, &ec.PublicKey) { |
| return false, echConfig{}, &echConfigErr{"public_key"} |
| } |
| var cipherSuites cryptobyte.String |
| if !s.ReadUint16LengthPrefixed(&cipherSuites) { |
| return false, echConfig{}, &echConfigErr{"cipher_suites"} |
| } |
| for !cipherSuites.Empty() { |
| var c echCipher |
| if !cipherSuites.ReadUint16(&c.KDFID) { |
| return false, echConfig{}, &echConfigErr{"cipher_suites kdf_id"} |
| } |
| if !cipherSuites.ReadUint16(&c.AEADID) { |
| return false, echConfig{}, &echConfigErr{"cipher_suites aead_id"} |
| } |
| ec.SymmetricCipherSuite = append(ec.SymmetricCipherSuite, c) |
| } |
| if !s.ReadUint8(&ec.MaxNameLength) { |
| return false, echConfig{}, &echConfigErr{"maximum_name_length"} |
| } |
| var publicName cryptobyte.String |
| if !s.ReadUint8LengthPrefixed(&publicName) { |
| return false, echConfig{}, &echConfigErr{"public_name"} |
| } |
| ec.PublicName = publicName |
| var extensions cryptobyte.String |
| if !s.ReadUint16LengthPrefixed(&extensions) { |
| return false, echConfig{}, &echConfigErr{"extensions"} |
| } |
| for !extensions.Empty() { |
| var e echExtension |
| if !extensions.ReadUint16(&e.Type) { |
| return false, echConfig{}, &echConfigErr{"extensions type"} |
| } |
| if !extensions.ReadUint16LengthPrefixed((*cryptobyte.String)(&e.Data)) { |
| return false, echConfig{}, &echConfigErr{"extensions data"} |
| } |
| ec.Extensions = append(ec.Extensions, e) |
| } |
|
|
| return false, ec, nil |
| } |
|
|
| |
| |
| |
| func parseECHConfigList(data []byte) ([]echConfig, error) { |
| s := cryptobyte.String(data) |
| var length uint16 |
| if !s.ReadUint16(&length) { |
| return nil, errMalformedECHConfigList |
| } |
| if length != uint16(len(data)-2) { |
| return nil, errMalformedECHConfigList |
| } |
| var configs []echConfig |
| for len(s) > 0 { |
| if len(s) < 4 { |
| return nil, errors.New("tls: malformed ECHConfig") |
| } |
| configLen := uint16(s[2])<<8 | uint16(s[3]) |
| skip, ec, err := parseECHConfig(s) |
| if err != nil { |
| return nil, err |
| } |
| s = s[configLen+4:] |
| if !skip { |
| configs = append(configs, ec) |
| } |
| } |
| return configs, nil |
| } |
|
|
| func pickECHConfig(list []echConfig) (*echConfig, hpke.PublicKey, hpke.KDF, hpke.AEAD) { |
| for _, ec := range list { |
| if !validDNSName(string(ec.PublicName)) { |
| continue |
| } |
| var unsupportedExt bool |
| for _, ext := range ec.Extensions { |
| |
| |
| |
| if ext.Type&uint16(1<<15) != 0 { |
| unsupportedExt = true |
| } |
| } |
| if unsupportedExt { |
| continue |
| } |
| kem, err := hpke.NewKEM(ec.KemID) |
| if err != nil { |
| continue |
| } |
| pub, err := kem.NewPublicKey(ec.PublicKey) |
| if err != nil { |
| |
| |
| continue |
| } |
| for _, cs := range ec.SymmetricCipherSuite { |
| |
| |
| |
| kdf, err := hpke.NewKDF(cs.KDFID) |
| if err != nil { |
| continue |
| } |
| aead, err := hpke.NewAEAD(cs.AEADID) |
| if err != nil { |
| continue |
| } |
| return &ec, pub, kdf, aead |
| } |
| } |
| return nil, nil, nil, nil |
| } |
|
|
| func encodeInnerClientHello(inner *clientHelloMsg, maxNameLength int) ([]byte, error) { |
| h, err := inner.marshalMsg(true) |
| if err != nil { |
| return nil, err |
| } |
| h = h[4:] |
|
|
| 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 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) || |
| !skipUint8LengthPrefixed(&s) || |
| !skipUint16LengthPrefixed(&s) || |
| !skipUint8LengthPrefixed(&s) { |
| 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) { |
| |
| |
| |
| |
| |
| |
| |
| |
| 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") |
| } |
|
|
| |
| |
| |
| 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)}) { |
| return nil, errInvalidECHExt |
| } |
|
|
| hasTLS13 := false |
| for _, v := range inner.supportedVersions { |
| |
| |
| |
| |
| |
| if v&0x0F0F == 0x0A0A && v&0xff == v>>8 { |
| continue |
| } |
|
|
| |
| if v == VersionTLS13 { |
| hasTLS13 = true |
| } else if v < VersionTLS13 { |
| |
| 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") |
| } |
|
|
| return inner, nil |
| } |
|
|
| func decryptECHPayload(context *hpke.Recipient, hello, payload []byte) ([]byte, error) { |
| outerAAD := bytes.Replace(hello[4:], payload, make([]byte, len(payload)), 1) |
| return context.Open(outerAAD, payload) |
| } |
|
|
| func generateOuterECHExt(id uint8, kdfID, aeadID uint16, encodedKey []byte, payload []byte) ([]byte, error) { |
| var b cryptobyte.Builder |
| b.AddUint8(0) |
| 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 *echClientContext, useKey bool) error { |
| var encapKey []byte |
| if useKey { |
| encapKey = ech.encapsulatedKey |
| } |
| encodedInner, err := encodeInnerClientHello(inner, int(ech.config.MaxNameLength)) |
| if err != nil { |
| return err |
| } |
| |
| |
| |
| encryptedLen := len(encodedInner) + 16 |
| 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:] |
| 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 |
| } |
|
|
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| type ECHRejectionError struct { |
| RetryConfigList []byte |
| } |
|
|
| func (e *ECHRejectionError) Error() string { |
| return "tls: server rejected ECH" |
| } |
|
|
| var errMalformedECHExt = errors.New("tls: malformed encrypted_client_hello extension") |
| var errInvalidECHExt = errors.New("tls: client sent invalid encrypted_client_hello extension") |
|
|
| 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 { |
| err = errInvalidECHExt |
| 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 |
| } |
|
|
| |
| |
| return echType, cs, configID, bytes.Clone(encap), bytes.Clone(payload), nil |
| } |
|
|
| func (c *Conn) processECHClientHello(outer *clientHelloMsg, echKeys []EncryptedClientHelloKey) (*clientHelloMsg, *echServerContext, error) { |
| echType, echCiphersuite, configID, encap, payload, err := parseECHExt(outer.encryptedClientHello) |
| if err != nil { |
| if errors.Is(err, errInvalidECHExt) { |
| c.sendAlert(alertIllegalParameter) |
| } else { |
| c.sendAlert(alertDecodeError) |
| } |
|
|
| return nil, nil, errInvalidECHExt |
| } |
|
|
| if echType == innerECHExt { |
| return outer, &echServerContext{inner: true}, nil |
| } |
|
|
| if len(echKeys) == 0 { |
| return outer, nil, nil |
| } |
|
|
| for _, echKey := range echKeys { |
| skip, config, err := parseECHConfig(echKey.Config) |
| if err != nil || skip { |
| c.sendAlert(alertInternalError) |
| return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config: %s", err) |
| } |
| if skip { |
| continue |
| } |
| 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) |
| 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) |
| if err != nil { |
| c.sendAlert(alertInternalError) |
| return nil, nil, fmt.Errorf("tls: invalid EncryptedClientHelloKey Config AEAD: %s", err) |
| } |
| info := append([]byte("tls ech\x00"), echKey.Config...) |
| hpkeContext, err := hpke.NewRecipient(encap, echPriv, kdf, aead, info) |
| if err != nil { |
| |
| continue |
| } |
|
|
| encodedInner, err := decryptECHPayload(hpkeContext, outer.original, payload) |
| if err != nil { |
| |
| continue |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| echInner, err := decodeInnerClientHello(outer, encodedInner) |
| if err != nil { |
| c.sendAlert(alertIllegalParameter) |
| return nil, nil, errInvalidECHExt |
| } |
|
|
| 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() |
| } |
|
|