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

Add "ech-reject" test case for nss and cloudflare-go #36

Merged
merged 1 commit into from
Feb 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ testinputs: util
${UTIL} -make-intermediate -cert-in ${TESTDATA_DIR}/root.crt -key-in ${TESTDATA_DIR}/root.key -out ${TESTDATA_DIR}/client-facing.crt -key-out ${TESTDATA_DIR}/client-facing.key -host client-facing.com
${UTIL} -make-dc -cert-in ${TESTDATA_DIR}/example.crt -key-in ${TESTDATA_DIR}/example.key -out ${TESTDATA_DIR}/dc.txt
${UTIL} -make-ech -out ${TESTDATA_DIR}/ech_configs -key-out ${TESTDATA_DIR}/ech_key -host client-facing.com
${UTIL} -make-ech -out ${TESTDATA_DIR}/ech_configs_invalid -key-out /dev/null -host client-facing.com

clean:
rm -fr ${BIN_DIR}
Expand Down
2 changes: 0 additions & 2 deletions impl-endpoints/cloudflare-go/run_endpoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ sh /setup-routes.sh

if [ "$ROLE" = "client" ]; then
echo "Running Cloudflare-Go client."
echo "Client params: $CLIENT_PARAMS"
echo "Test case: $TESTCASE"
runner -role=client -test=${TESTCASE}
else
echo "Running Cloudflare-Go server."
echo "Server params: $SERVER_PARAMS"
echo "Test case: $TESTCASE"
runner -role=server -test=${TESTCASE}
fi
161 changes: 107 additions & 54 deletions impl-endpoints/cloudflare-go/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/base64"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
Expand All @@ -17,9 +18,9 @@ import (

type testCase interface {
ClientConfig() (*tls.Config, error)
ClientConnectionHandler(*tls.Conn) error
ClientConnectionHandler(*tls.Conn, error) error
ServerConfig() (*tls.Config, error)
ServerConnectionHandler(*tls.Conn) error
ServerConnectionHandler(*tls.Conn, error) error
}

var baseServerConfig, baseClientConfig *tls.Config
Expand All @@ -38,7 +39,7 @@ func init() {
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(pem)
baseClientConfig = &tls.Config{
RootCAs: certPool,
RootCAs: certPool,
KeyLogWriter: clientKeyLog,
}

Expand Down Expand Up @@ -72,6 +73,8 @@ func init() {
}
}

// The "dc" test case in which the client indicates support for DCs and the
// server uses a DC for authentication. See draft-ietf-tls-subcerts.
type testCaseDC struct{}

func (t testCaseDC) ClientConfig() (*tls.Config, error) {
Expand All @@ -85,55 +88,102 @@ func (t testCaseDC) ServerConfig() (*tls.Config, error) {
return config, nil
}

func (t testCaseDC) ClientConnectionHandler(conn *tls.Conn) error {
func (t testCaseDC) ClientConnectionHandler(conn *tls.Conn, err error) error {
if err != nil {
return fmt.Errorf("Unexpected error: \"%s\"", err)
}
if conn.ConnectionState().VerifiedDC == nil {
return errors.New("Failed to verify a delegated credential")
}
return nil
}

func (t testCaseDC) ServerConnectionHandler(conn *tls.Conn) error {
func (t testCaseDC) ServerConnectionHandler(conn *tls.Conn, err error) error {
// TODO
return errors.New("NOT IMPLEMENTED")
}

type echTestResult struct {
serverStatus tls.EXP_EventECHServerStatus
}
// The "ech-accept" test case in which a client offers the ECH extension and
// the server accepts. The client verifies server name "backend.com". See
// draft-ietf-tls-esni.
//
// TODO(cjpatton): Check via validatepcap that:
// (1) client sent ECH in CH; and
// (2) SNI = "example.com".
type testCaseECHAccept struct{}

func (r *echTestResult) eventHandler(event tls.EXP_Event) {
switch e := event.(type) {
case tls.EXP_EventECHServerStatus:
r.serverStatus = e
}
func (t testCaseECHAccept) ClientConfig() (*tls.Config, error) {
return echClientConfig("/test-inputs/ech_configs")
}

type testCaseECHAccept struct{}
func (t testCaseECHAccept) ServerConfig() (*tls.Config, error) {
return echServerConfig("/test-inputs/ech_key")
}

func (t testCaseECHAccept) ClientConfig() (*tls.Config, error) {
base64ECHConfigs, err := ioutil.ReadFile("/test-inputs/ech_configs")
func (t testCaseECHAccept) connectionHandler(conn *tls.Conn, err error) error {
if err != nil {
return nil, err
return fmt.Errorf("Unexpected error: \"%s\"", err)
}

rawECHConfigs, err := base64.StdEncoding.DecodeString(string(base64ECHConfigs))
if err != nil {
return nil, err
st := conn.ConnectionState()
if !st.ECHAccepted {
return errors.New("ECH not accepted")
}
log.Printf("Connection completed with backend server \"%s\"", st.ServerName)
return nil
}

echConfigs, err := tls.UnmarshalECHConfigs(rawECHConfigs)
if err != nil {
return nil, err
func (t testCaseECHAccept) ClientConnectionHandler(conn *tls.Conn, err error) error {
return t.connectionHandler(conn, err)
}

func (t testCaseECHAccept) ServerConnectionHandler(conn *tls.Conn, err error) error {
return t.connectionHandler(conn, err)
}

// The "ech-reject" test case in which the client offers ECH with stale configs
// and the server rejects. The client verifies server name "client-facing.com",
// then aborts the handshake with an "ech_required" alert. See
// draft-ietf-tls-esni.
//
// TODO(cjpatton): Check via validatepcap that:
// (1) client sent ECH in CH;
// (2) server sent ECH in EE; and
// (3) SNI = "client-facing.com".
type testCaseECHReject struct{}

func (t testCaseECHReject) ClientConfig() (*tls.Config, error) {
return echClientConfig("/test-inputs/ech_configs_invalid")
}

func (t testCaseECHReject) ServerConfig() (*tls.Config, error) {
return echServerConfig("/test-inputs/ech_key")
}

func (t testCaseECHReject) ClientConnectionHandler(conn *tls.Conn, err error) error {
if err == nil {
return errors.New("Handshake completed successfully; expected error")
}
if err.Error() != "tls: ech: rejected" {
return fmt.Errorf("Unexpected error: \"%s\"", err)
}
log.Print("Aborted the connection as expected")
return nil
}

config := baseClientConfig.Clone()
config.ClientECHConfigs = echConfigs
config.ECHEnabled = true
return config, nil
func (t testCaseECHReject) ServerConnectionHandler(conn *tls.Conn, err error) error {
if err != nil {
return fmt.Errorf("Unexpected error: \"%s\"", err)
}
if conn.ConnectionState().ECHAccepted {
return errors.New("ECH accepted; expected rejection")
}
log.Printf("Connection completed with client-facing server \"%s\"", conn.ConnectionState().ServerName)
return nil
}

func (t testCaseECHAccept) ServerConfig() (*tls.Config, error) {
base64ECHKeys, err := ioutil.ReadFile("/test-inputs/ech_key")
func echServerConfig(keysFile string) (*tls.Config, error) {
base64ECHKeys, err := ioutil.ReadFile(keysFile)
if err != nil {
return nil, err
}
Expand All @@ -159,24 +209,32 @@ func (t testCaseECHAccept) ServerConfig() (*tls.Config, error) {
return config, nil
}

func (t testCaseECHAccept) connectionHandler(conn *tls.Conn) error {
if !conn.ConnectionState().ECHAccepted {
return errors.New("ECH not accepted")
func echClientConfig(configsFile string) (*tls.Config, error) {
base64ECHConfigs, err := ioutil.ReadFile(configsFile)
if err != nil {
return nil, err
}
return nil
}

func (t testCaseECHAccept) ClientConnectionHandler(conn *tls.Conn) error {
return t.connectionHandler(conn)
}
rawECHConfigs, err := base64.StdEncoding.DecodeString(string(base64ECHConfigs))
if err != nil {
return nil, err
}

echConfigs, err := tls.UnmarshalECHConfigs(rawECHConfigs)
if err != nil {
return nil, err
}

func (t testCaseECHAccept) ServerConnectionHandler(conn *tls.Conn) error {
return t.connectionHandler(conn)
config := baseClientConfig.Clone()
config.ClientECHConfigs = echConfigs
config.ECHEnabled = true
return config, nil
}

var testCaseHandlers = map[string]testCase{
"dc": testCaseDC{},
"ech-accept": testCaseECHAccept{},
"ech-reject": testCaseECHReject{},
}

func doClient(t testCase) error {
Expand All @@ -186,13 +244,10 @@ func doClient(t testCase) error {
}

c, err := tls.Dial("tcp", "example.com:4433", config)
if err != nil {
return err
if err == nil {
defer c.Close()
}
defer c.Close()

log.Print("Handshake completed")
return t.ClientConnectionHandler(c)
return t.ClientConnectionHandler(c, err)
}

func doServer(t testCase) error {
Expand All @@ -215,13 +270,10 @@ func doServer(t testCase) error {

s := tls.Server(conn, config)
err = s.Handshake()
if err != nil {
return err
if err == nil {
defer s.Close()
}
defer s.Close()

log.Print("Handshake completed")
return t.ServerConnectionHandler(s)
return t.ServerConnectionHandler(s, err)
}

func main() {
Expand All @@ -235,15 +287,16 @@ func main() {
os.Exit(64)
}

log.SetFlags(0)
if *role == "client" {
if err := doClient(handler); err != nil {
log.Fatal("Error: ", err)
log.Fatal(err)
}
} else if *role == "server" {
if err := doServer(handler); err != nil {
log.Fatal("Error: ", err)
log.Fatal(err)
}
} else {
log.Fatalf("unknown role \"%s\"", *role)
log.Fatalf("Unknown role \"%s\"", *role)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me wonder if we should have a sentinel return value for invalid parameters. @xvzcf, thoughts?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will address this in the follow-up PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #39, though I'm not sure this totally addresses the point!

}
}
21 changes: 18 additions & 3 deletions impl-endpoints/nss/run_endpoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ PORT=4433
rm -rf nss_testdata
mkdir nss_testdata

if [ "$TESTCASE" = "ech-accept" ]; then
if [ "$TESTCASE" = "ech-accept" ] || [ "$TESTCASE" = "ech-reject" ]; then
# Create a PKCS8 file for the ECH keypair
python3 /ech_key_converter.py /test-inputs/ech_key nss_testdata/ech_key_converted

Expand Down Expand Up @@ -42,9 +42,24 @@ if [ "$ROLE" = "client" ]; then
echo "Running NSS client."
echo "Client params: $SERVER_PARAMS"
echo "Test case: $TESTCASE"
ECH_CONFIGS=$(<testdata/ech_configs)
echo "GET / HTTP/1.0" > req.txt
tstclnt -d "$DB_DIR" -h example.com -p "$PORT" -N "$ECH_CONFIGS" -A req.txt
if [ "$TESTCASE" = "ech-reject" ]; then
ECH_CONFIGS=$(</test-inputs/ech_configs_invalid)

# Default cert verifier (which is used by tstclnt) is not ECH-aware.
# Override failures since the hostnames won't match.
tstclnt -d "$DB_DIR" -h example.com -p "$PORT" -N "$ECH_CONFIGS" -A req.txt -o &> err.txt || true
ECH_CONFIGS=$(sed '4q;d' err.txt)
if [ "$ECH_CONFIGS" != "$(</test-inputs/ech_configs)" ]; then
echo "Unexpected error:"
cat err.txt
else
echo "Aborted the connection as expected"
fi
else
ECH_CONFIGS=$(</test-inputs/ech_configs)
tstclnt -d "$DB_DIR" -h example.com -p "$PORT" -N "$ECH_CONFIGS" -A req.txt
fi
else
echo "Running NSS server."
echo "Server params: $SERVER_PARAMS"
Expand Down