golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.34k stars 17.71k forks source link

crypto/tls: After the ech key verification fails, the new key sent by the ech server will not be used for handshake. #70073

Open xdown-dev opened 1 month ago

xdown-dev commented 1 month ago

Go version

go1.23.2 linux/amd64

Output of go env in your module/workspace:

GO111MODULE='off'
GOARCH='amd64'
GOBIN=''
GOCACHE='/root/.cache/go-build'
GOENV='/root/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/gopkg/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/gopkg'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.23.2'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/root/.config/go/telemetry'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD=''
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2771492357=/tmp/go-build -gno-record-gcc-switches'

What did you do?

package main

import (
    "crypto/tls"
    "encoding/base64"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

func main() {
    clientConfig := &tls.Config{}
    clientConfig.MinVersion = tls.VersionTLS13

    echValue := "AEX+DQBBBQAgACBlLBm5Ur9BQXNm0X50TDLFPd1YWz8s0Gx35z8T2ukeewAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="

    echBytes, err := base64.StdEncoding.DecodeString(echValue)
    if err != nil {
        log.Fatalf("解码Ech失败: %v", err)
    }
    clientConfig.EncryptedClientHelloConfigList = echBytes

    // 配置HTTP客户端
    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: clientConfig,
        },
    }

    // cloudflare-ech.com
    // 发起HTTPS请求
    resp, err := client.Get("https://ok-ssl.xyz/cdn-cgi/trace")
    if err != nil {
        log.Fatalf("请求失败: %v", err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("读取响应失败: %v", err)
    }

    fmt.Printf("响应内容: %s\n", body)
}

What did you see happen?

2024/10/28 10:05:19 请求失败: Get "https://ok-ssl.xyz/cdn-cgi/trace": tls: server rejected ECH

What did you expect to see?

ECH 的工作原理
ECH 同样采用 DoH (后文会进行详细介绍,此处可暂时理解成 DNS 安全层的一种实现)进行密钥的分发,但是在分发过程上进行了改进。如果解密失败,ESNI 服务器会中止连接,**而 ECH 服务器会提供给客户端一个公钥供客户端重试连接,**以期可以完成握手。

ECH 协议实际上涉及两个 ClientHello 消息:ClientHelloOuter,它像往常一样以明文形式发送;ClientHelloInner,它被加密并作为 [ Client](https://hijiangtao.github.io/2022/10/09/Secure-Your-Browsing-Experience-with-More-Encrypted-Tools/#)HelloOuter 的扩展发送。服务器仅使用其中一个 ClientHello 完成握手:如果解密成功,则继续使用 ClientHelloInner;否则,它只使用 ClientHelloOuter。

If I use an expired key for handshake, an error will be returned. This should not be the case. The key is obtained through DNS, but DNS may have cache errors. At this time, the new key returned by ECH should be used to re-handshake. I don't know if this understanding is correct?

gabyhelp commented 1 month ago

Related Issues and Documentation

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Jorropo commented 1 month ago

I don't think your example is a good demonstration of your point.

In your example you manually set the ECH configuration values, then crypto/tls returns (wrapped) crypto/tls.ECHRejectionError, you can use errors.As to grab the server's retry list. Given you manually configured ECH I think it make sense you also manually configure retry.

I think your point would be strong if you can show an example where your code "just" do a plain HTTP query, and go setups ECH Client for you under the hood and that still fails.


Also I changed the code:

        if err != nil {
+               var tgt *tls.ECHRejectionError
+               if !errors.As(err, &tgt) { panic("wrong error type") }
+               log.Fatalf("请求失败: %v", tgt.RetryConfigList)
-               log.Fatalf("请求失败: %v", err)
        }

And it shows an empty RetryConfigList:

2024/10/28 15:23:44 请求失败: []
exit status 1

I've also tried looking at the connection through wireshark and I do not find anything of that sort (I could have missed it). So I'm not even sure go could even retry anything as it looks like the server is not sending any other key.

xdown-dev commented 1 month ago

resp, err := client.Get("https://tls-ech.dev/")

2024/10/29 14:20:58 请求失败: Get "https://tls-ech.dev/": tls: failed to verify certificate: x509: certificate is valid for public.tls-ech.dev, stale.tls-ech.dev, tls-ech.dev, tls12.tls-ech.dev, wrong.tls-ech.dev, not cloudflare-ech.com

What if we test it with https://tls-ech.dev/? Is it because the cloudflare ech server did not return the result?

Jorropo commented 1 month ago

What ECH Key should I be using for tls-ech.dev ?

cagedmantis commented 4 weeks ago

cc @FiloSottile @rolandshoemaker @golang/security