Skip to content

Instantly share code, notes, and snippets.

@cjpatton
Last active December 17, 2023 21:12
Show Gist options
  • Save cjpatton/da8814704b8daa48cb6c16eafdb8e402 to your computer and use it in GitHub Desktop.
Save cjpatton/da8814704b8daa48cb6c16eafdb8e402 to your computer and use it in GitHub Desktop.
ECH test client and server

Contents

  • client.go - ECH test client (uses a fixed ECHConfigsList).
  • server.go - ECH test server (uses a fixed set of ECH keys).
  • backend.crt, baackend.key - Test certificate and key for "example.com" (the backend server).
  • client_facing.crt, backend.key - Test certificate and key for "cloudflare-esni.com" (the client-facing server).
  • root.crt - Root certificate for backend.crt and client_facing.crt.
  • get_configs.py - Script for fetching the real ECHConfigsList for "crypto.cloudflare.com".

Testing

To run the client and server, you'll need to download and build Cloudflare's fork of Go.

git clone https://github.com/cloudflare/go ~/cfgo
cd ~/cfgo/src
./make.bash # You'll need to have Go already installed.

In the directory with the test client and server, do

~/cfgo/bin/go run server.go

and

~/cfgo/bin/go run client.go
-----BEGIN CERTIFICATE-----
MIICGTCCAcCgAwIBAgIUQJSSdOZs9wag1Toanlt9lol0uegwCgYIKoZIzj0EAwIw
fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh
biBGcmFuY2lzY28xHzAdBgNVBAoTFkludGVybmV0IFdpZGdldHMsIEluYy4xDDAK
BgNVBAsTA1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjAwOTIyMTcwOTAw
WhcNMjEwOTIyMTcwOTAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElq+q
E01Z87KIPHWdEAk0cWssHkRnS4aQCDfstoxDIWQ4rMwHvrWGFy/vytRwyjhHuX9n
tc5ArCpwbAmY+oW/46OBmDCBlTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI
KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPz9Ct9U
EIjBEcUpv/yxHYccUDo1MB8GA1UdIwQYMBaAFDYYkhb7ibf+qmRqB7m0S8yDc3A9
MBYGA1UdEQQPMA2CC2V4YW1wbGUuY29tMAoGCCqGSM49BAMCA0cAMEQCICDBEzzE
DF529x9Z4BkOKVxNDicfWSjxrcMohevjeCWDAiBaxXS5+6I2fcred0JGMsJgo7ts
S8GYhuKE99mQA0/mug==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MHcCAQEEIIJsLXmfzw6FDlqyRRLhY6lVB6ws5ewjUQjnS4DXsQ60oAoGCCqGSM49
AwEHoUQDQgAElq+qE01Z87KIPHWdEAk0cWssHkRnS4aQCDfstoxDIWQ4rMwHvrWG
Fy/vytRwyjhHuX9ntc5ArCpwbAmY+oW/4w==
-----END PRIVATE KEY-----
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
)
const echConfigsListString = `-----BEGIN ECH CONFIGS-----
AEb+DQBCGwAgACDSupslkfIkg/C0be/yDdZqtUJs4ssKG5IgWHadWXn4KQAEAAEA
ASUTY2xvdWRmbGFyZS1lc25pLmNvbQAA
-----END ECH CONFIGS-----`
func main() {
block, rest := pem.Decode([]byte(echConfigsListString))
if block == nil || block.Type != "ECH CONFIGS" || len(rest) > 0 {
log.Fatal("Failet to PEM-decode the ECH configs")
}
echConfigsList, err := tls.UnmarshalECHConfigs(block.Bytes)
if err != nil {
log.Fatal("Failed to parse ECH configs:", err)
}
rootData, err := ioutil.ReadFile("root.crt")
if err != nil {
log.Fatal("Failed to load root cert:", err)
}
block, rest = pem.Decode(rootData)
if block == nil || block.Type != "CERTIFICATE" || len(rest) > 0 {
log.Fatal("Failed to PEM-decode the root cert")
}
root, err := x509.ParseCertificate(block.Bytes)
if err != nil {
log.Fatal("failed to parse root cert:", err)
}
rootCAs := x509.NewCertPool()
rootCAs.AddCert(root)
config := &tls.Config{
ServerName: "example.com", // This SNI is protected by ECH.
ECHEnabled: true,
ClientECHConfigs: echConfigsList,
MinVersion: tls.VersionTLS13,
RootCAs: rootCAs,
}
if tlsKeyLogFile := os.Getenv("SSLKEYLOGFILE"); tlsKeyLogFile != "" {
kw, err := os.OpenFile(tlsKeyLogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
log.Printf("Cannot open key log file: %s\n", err)
}
config.KeyLogWriter = kw
}
req, err := http.NewRequest("GET", "https://example.com/hello", nil)
if err != nil {
log.Fatal(err)
}
client := &http.Client{
Transport: &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return tls.Dial(network, ":8080", config)
},
},
}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
out, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v %v %v\n\n", resp.Status, resp.Proto, resp.ContentLength)
for name, val := range resp.Header {
fmt.Printf("%v: %v\n", name, val)
}
fmt.Println()
fmt.Printf("%v\n", string(out))
}
-----BEGIN CERTIFICATE-----
MIICIjCCAcigAwIBAgIUCXySp2MadlDlcvFrSm4BtLUY70owCgYIKoZIzj0EAwIw
fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh
biBGcmFuY2lzY28xHzAdBgNVBAoTFkludGVybmV0IFdpZGdldHMsIEluYy4xDDAK
BgNVBAsTA1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjAwOTIyMTcxMDAw
WhcNMjEwOTIyMTcxMDAwWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7nP/
Txinb0JPE/xdjv5d3zrWJqXo7qwP67oVaMKJp5ausJ+0IZfiMWz8pa6T7pyyLrC5
xvQNkfVkpP9/FxmNFaOBoDCBnTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI
KwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFNN7Afv+
CgPAxRr4QdZn8JFvQ9nTMB8GA1UdIwQYMBaAFDYYkhb7ibf+qmRqB7m0S8yDc3A9
MB4GA1UdEQQXMBWCE2Nsb3VkZmxhcmUtZXNuaS5jb20wCgYIKoZIzj0EAwIDSAAw
RQIgZ4VlBtjTRludP/JwfaNQyGKZFWFqRsECvGPbk+ZHLZwCIQCTjuMAFrnjf/j5
3RNw67l7+QQPrmurSO86l1IlDWNtcA==
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MHcCAQEEIPpCcU8mu+h4xHAm18NJvn73Ko9fjH9QxDCpRt7kCIq9oAoGCCqGSM49
AwEHoUQDQgAE7nP/Txinb0JPE/xdjv5d3zrWJqXo7qwP67oVaMKJp5ausJ+0IZfi
MWz8pa6T7pyyLrC5xvQNkfVkpP9/FxmNFQ==
-----END PRIVATE KEY-----
#!/usr/bin/env python3
#
# Installing dependencies (on Ubuntu):
# git clone https://github.com/rthalley/dnspython ~/test_ech/dnspython
# cd ~/test_ech/dnspython && sudo python3 setup.py install
import dns.resolver
resolver = dns.resolver.Resolver(configure=False)
resolver.nameservers = ['1.1.1.1']
answer = resolver.resolve('crypto.cloudflare.com', 'TYPE65')
print(answer.rrset)
-----BEGIN CERTIFICATE-----
MIICQTCCAeigAwIBAgIUYGSqOFcpxSleCzSCaveKL8lV4N0wCgYIKoZIzj0EAwIw
fzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh
biBGcmFuY2lzY28xHzAdBgNVBAoTFkludGVybmV0IFdpZGdldHMsIEluYy4xDDAK
BgNVBAsTA1dXVzEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjAwOTIyMTcwNjAw
WhcNMjUwOTIxMTcwNjAwWjB/MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZv
cm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEfMB0GA1UEChMWSW50ZXJuZXQg
V2lkZ2V0cywgSW5jLjEMMAoGA1UECxMDV1dXMRQwEgYDVQQDEwtleGFtcGxlLmNv
bTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNcFaBtPRgekRBKTBvuKdTy3raqs
4IizMLFup434MfQ5oH71mYpKndfBzxcZDTMYeocKlt1pVYwvZ3ZdpRsW6yWjQjBA
MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQ2GJIW
+4m3/qpkage5tEvMg3NwPTAKBggqhkjOPQQDAgNHADBEAiB6J8UqRvdhLOiaDYqH
KG+TuveHOqlfQqQgXo4/hNKMiAIgV79TTPHu+Ymn/tcCy9LVWZcpgnCEjrZi0ou5
et8BX9s=
-----END CERTIFICATE-----
package main
import (
"crypto/tls"
"encoding/pem"
"fmt"
"log"
"net/http"
)
const echKeysString = `-----BEGIN ECH KEYS-----
ACBmishQAYmMkIcdeYOEWZnp8X+wv+jBeIea3gQLJ2bClABG/g0AQhsAIAAg0rqb
JZHyJIPwtG3v8g3WarVCbOLLChuSIFh2nVl5+CkABAABAAElE2Nsb3VkZmxhcmUt
ZXNuaS5jb20AAA==
-----END ECH KEYS-----`
var echProvider tls.ECHProvider
func main() {
clientFacingCert, err := tls.LoadX509KeyPair("client_facing.crt", "client_facing.key")
if err != nil {
log.Fatal("failed to load client-facing certificate:", err)
}
backendCert, err := tls.LoadX509KeyPair("backend.crt", "backend.key")
if err != nil {
log.Fatal("failed to load backend certificate:", err)
}
block, rest := pem.Decode([]byte(echKeysString))
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
panic("failed to initialize ECH provider: PEM-decoding failed")
}
keys, err := tls.EXP_UnmarshalECHKeys(block.Bytes)
if err != nil {
panic(fmt.Errorf("failed to initialize ECH provider: %s", err))
}
log.Printf("Configured %d keys for ECH", len(keys))
echProvider, err = tls.EXP_NewECHKeySet(keys)
if err != nil {
panic(fmt.Errorf("failed to initialize ECH provider: %s", err))
}
s := &http.Server{
Addr: ":8080",
TLSConfig: &tls.Config{
ECHEnabled: true,
ServerECHProvider: echProvider,
Certificates: []tls.Certificate{clientFacingCert, backendCert},
},
}
http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
fmt.Fprint(res, "We did it!")
})
log.Fatal(s.ListenAndServeTLS("", ""))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment