Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP ECH test update #1693

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 5 additions & 1 deletion cmd/ooniprobe/internal/nettests/echcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@ func (n ECHCheck) Run(ctl *Controller) error {
}
// providing an input containing an empty string causes the experiment
// to recognize the empty string and use the default URL
return ctl.Run(builder, []model.ExperimentTarget{model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("")})
return ctl.Run(builder, []model.ExperimentTarget{
model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://cloudflare-ech.com/cdn-cgi/trace"),
// Use ECH on a non-standard port.
model.NewOOAPIURLInfoWithDefaultCategoryAndCountry("https://min-ng.test.defo.ie:15443"),
})
}
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/ooni/probe-cli/v3

go 1.21.0

toolchain go1.22.3
go 1.23.4

require (
filippo.io/age v1.2.1
Expand Down
131 changes: 57 additions & 74 deletions internal/experiment/echcheck/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,91 +4,74 @@ package echcheck
// ietf.org/archive/id/draft-ietf-tls-esni-14.html

import (
"fmt"
"io"

"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")
// ECH Config List per:
// https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html#name-encrypted-clienthello-confi
func generateGreaseyECHConfigList(rand io.Reader, publicName string) ([]byte, error) {
// Start ECHConfig
var c cryptobyte.Builder
version := uint16(0xfe0d)
c.AddUint16(version)

// Start ECHConfigContents
var ecc cryptobyte.Builder
// Start HpkeKeyConfig
randConfigId := make([]byte, 1)
if _, err := io.ReadFull(rand, randConfigId); err != nil {
return nil, err
}

defaultHPKESuite := hpke.NewSuite(kem, kdf, aead)

// generate a public key to place in 'enc' field
ecc.AddUint8(randConfigId[0])
ecc.AddUint16(uint16(hpke.KEM_X25519_HKDF_SHA256))
// Generate a public key
kem := hpke.KEM(uint16(hpke.KEM_X25519_HKDF_SHA256))
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)
publicKeyBytes, err := publicKey.MarshalBinary()
if err != nil {
return nil, err
}
ecc.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(publicKeyBytes)
})
// Start HpkeSymmetricCipherSuite
kdf := hpke.KDF(uint16(hpke.KDF_HKDF_SHA256))
aead := hpke.AEAD(uint16(hpke.AEAD_AES128GCM))
var cs cryptobyte.Builder
cs.AddUint16(uint16(kdf))
cs.AddUint16(uint16(aead))
ecc.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(cs.BytesOrPanic())
})
// End HpkeSymmetricCipherSuite
// End HpkeKeyConfig
maxNameLength := uint8(42)
ecc.AddUint8(maxNameLength)
publicNameBytes := []byte(publicName)
ecc.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(publicNameBytes)
})
// Start ECHConfigExtension
var ece cryptobyte.Builder
ecc.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(ece.BytesOrPanic())
})
// End ECHConfigExtension
// End ECHConfigContents
c.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(ecc.BytesOrPanic())
})
// End ECHConfig
var l cryptobyte.Builder
l.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
b.AddBytes(c.BytesOrPanic())
})

// 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
return l.BytesOrPanic(), nil
}
17 changes: 17 additions & 0 deletions internal/experiment/echcheck/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package echcheck

import (
"crypto/rand"
"testing"
)

func TestParseableGREASEConfigList(t *testing.T) {
// A GREASE extension that can't be parsed is invalid.
grease, err := generateGreaseyECHConfigList(rand.Reader, "example.com")
if err != nil {
t.Fatal(err)
}
if _, err := parseECHConfigList(grease); err != nil {
t.Fatal(err)
}
}
Loading
Loading