-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(echcheck): add an Encrypted Client Hello (ECH) experiment (#970)
See ooni/probe#1453
- Loading branch information
Showing
12 changed files
with
583 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package echcheck | ||
|
||
const ( | ||
defaultResolver = "https://mozilla.cloudflare-dns.com/dns-query" | ||
) | ||
|
||
// Config contains the experiment config. | ||
type Config struct { | ||
// ResolverURL is the default DoH resolver | ||
ResolverURL string `ooni:"URL for DoH resolver"` | ||
} | ||
|
||
func (c Config) resolverURL() string { | ||
if c.ResolverURL != "" { | ||
return c.ResolverURL | ||
} | ||
return defaultResolver | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package echcheck | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestConfig(t *testing.T) { | ||
|
||
c := Config{ | ||
ResolverURL: "", | ||
} | ||
s1 := c.resolverURL() | ||
if s1 != defaultResolver { | ||
t.Fatalf("expected: %s, got %s", defaultResolver, s1) | ||
} | ||
|
||
testResolver := "testResolver" | ||
|
||
c = Config{ | ||
ResolverURL: testResolver, | ||
} | ||
s1 = c.resolverURL() | ||
if s1 != testResolver { | ||
t.Fatalf("expected: %s, got %s", testResolver, s1) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Package echcheck contains the ECH blocking network experiment. | ||
// | ||
// https://github.com/ooni/spec/pull/263 | ||
package echcheck |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package echcheck | ||
|
||
// Generates a 'GREASE ECH' extension, as described in section 6.2 of | ||
// ietf.org/archive/id/draft-ietf-tls-esni-14.html | ||
|
||
import ( | ||
"fmt" | ||
"github.com/cloudflare/circl/hpke" | ||
"golang.org/x/crypto/cryptobyte" | ||
"io" | ||
) | ||
|
||
const clientHelloOuter uint8 = 0 | ||
|
||
// echExtension is the Encrypted Client Hello extension that is part of | ||
// ClientHelloOuter as specified in: | ||
// ietf.org/archive/id/draft-ietf-tls-esni-14.html#section-5 | ||
type echExtension struct { | ||
kdfID uint16 | ||
aeadID uint16 | ||
configID uint8 | ||
enc []byte | ||
payload []byte | ||
} | ||
|
||
func (ech *echExtension) marshal() []byte { | ||
var b cryptobyte.Builder | ||
b.AddUint8(clientHelloOuter) | ||
b.AddUint16(ech.kdfID) | ||
b.AddUint16(ech.aeadID) | ||
b.AddUint8(ech.configID) | ||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { | ||
b.AddBytes(ech.enc) | ||
}) | ||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { | ||
b.AddBytes(ech.payload) | ||
}) | ||
return b.BytesOrPanic() | ||
} | ||
|
||
// generateGreaseExtension generates an ECH extension with random values as | ||
// specified in ietf.org/archive/id/draft-ietf-tls-esni-14.html#section-6.2 | ||
func generateGreaseExtension(rand io.Reader) ([]byte, error) { | ||
// initialize HPKE suite parameters | ||
kem := hpke.KEM(uint16(hpke.KEM_X25519_HKDF_SHA256)) | ||
kdf := hpke.KDF(uint16(hpke.KDF_HKDF_SHA256)) | ||
aead := hpke.AEAD(uint16(hpke.AEAD_AES128GCM)) | ||
|
||
if !kem.IsValid() || !kdf.IsValid() || !aead.IsValid() { | ||
return nil, fmt.Errorf("required parameters not supported") | ||
} | ||
|
||
defaultHPKESuite := hpke.NewSuite(kem, kdf, aead) | ||
|
||
// generate a public key to place in 'enc' field | ||
publicKey, _, err := kem.Scheme().GenerateKeyPair() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to generate key pair: %s", err) | ||
} | ||
|
||
// initiate HPKE Sender | ||
sender, err := defaultHPKESuite.NewSender(publicKey, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create sender: %s", err) | ||
} | ||
|
||
// Set ECH Extension Fields | ||
var ech echExtension | ||
|
||
ech.kdfID = uint16(kdf) | ||
ech.aeadID = uint16(aead) | ||
|
||
randomByte := make([]byte, 1) | ||
_, err = io.ReadFull(rand, randomByte) | ||
if err != nil { | ||
return nil, err | ||
} | ||
ech.configID = randomByte[0] | ||
|
||
ech.enc, _, err = sender.Setup(rand) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// TODO: compute this correctly as per https://www.ietf.org/archive/id/draft-ietf-tls-esni-14.html#name-recommended-padding-scheme | ||
randomEncodedClientHelloInnerLen := 100 | ||
cipherLen := int(aead.CipherLen(uint(randomEncodedClientHelloInnerLen))) | ||
ech.payload = make([]byte, randomEncodedClientHelloInnerLen+cipherLen) | ||
if _, err = io.ReadFull(rand, ech.payload); err != nil { | ||
return nil, err | ||
} | ||
|
||
return ech.marshal(), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package echcheck | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"crypto/tls" | ||
"github.com/apex/log" | ||
"github.com/ooni/probe-cli/v3/internal/measurexlite" | ||
"github.com/ooni/probe-cli/v3/internal/model" | ||
"github.com/ooni/probe-cli/v3/internal/netxlite" | ||
utls "gitlab.com/yawning/utls.git" | ||
"net" | ||
"time" | ||
) | ||
|
||
const echExtensionType uint16 = 0xfe0d | ||
|
||
func handshake(ctx context.Context, conn net.Conn, zeroTime time.Time, address string, sni string) *model.ArchivalTLSOrQUICHandshakeResult { | ||
return handshakeWithExtension(ctx, conn, zeroTime, address, sni, []utls.TLSExtension{}) | ||
} | ||
|
||
func handshakeWithEch(ctx context.Context, conn net.Conn, zeroTime time.Time, address string, sni string) *model.ArchivalTLSOrQUICHandshakeResult { | ||
payload, err := generateGreaseExtension(rand.Reader) | ||
if err != nil { | ||
panic("failed to generate grease ECH: " + err.Error()) | ||
} | ||
|
||
var utlsEchExtension utls.GenericExtension | ||
|
||
utlsEchExtension.Id = echExtensionType | ||
utlsEchExtension.Data = payload | ||
|
||
return handshakeWithExtension(ctx, conn, zeroTime, address, sni, []utls.TLSExtension{&utlsEchExtension}) | ||
} | ||
|
||
func handshakeWithExtension(ctx context.Context, conn net.Conn, zeroTime time.Time, address string, sni string, extensions []utls.TLSExtension) *model.ArchivalTLSOrQUICHandshakeResult { | ||
tlsConfig := genTLSConfig(sni) | ||
|
||
handshakerConstructor := newHandshakerWithExtensions(extensions) | ||
tracedHandshaker := handshakerConstructor(log.Log, &utls.HelloFirefox_Auto) | ||
|
||
start := time.Now() | ||
_, connState, err := tracedHandshaker.Handshake(ctx, conn, tlsConfig) | ||
finish := time.Now() | ||
|
||
return measurexlite.NewArchivalTLSOrQUICHandshakeResult(0, start.Sub(zeroTime), "tcp", address, tlsConfig, | ||
connState, err, finish.Sub(zeroTime)) | ||
} | ||
|
||
// genTLSConfig generates tls.Config from a given SNI | ||
func genTLSConfig(sni string) *tls.Config { | ||
return &tls.Config{ | ||
RootCAs: netxlite.NewDefaultCertPool(), | ||
ServerName: sni, | ||
NextProtos: []string{"h2", "http/1.1"}, | ||
InsecureSkipVerify: true, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package echcheck | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net" | ||
"net/http" | ||
"net/http/httptest" | ||
"net/url" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestHandshake(t *testing.T) { | ||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
fmt.Fprintln(w, "success") | ||
})) | ||
defer ts.Close() | ||
|
||
parsed, err := url.Parse(ts.URL) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
|
||
var dialer net.Dialer | ||
conn, err := dialer.DialContext(ctx, "tcp", parsed.Host) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
result := handshakeWithEch(ctx, conn, time.Now(), parsed.Host, "example.org") | ||
if result == nil { | ||
t.Fatal("expected result") | ||
} | ||
|
||
if result.SoError != nil { | ||
t.Fatal("did not expect error, got: ", result.SoError) | ||
} | ||
|
||
if result.Failure != nil { | ||
t.Fatal("did not expect error, got: ", *result.Failure) | ||
} | ||
|
||
conn.Close() | ||
} |
Oops, something went wrong.