diff --git a/.gitignore b/.gitignore index 1c96588..104bf84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .*.swp bin/ -testdata/ +generated/ diff --git a/Makefile b/Makefile index 79cec35..32dd976 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,25 @@ -TESTDATA_DIR = testdata +TESTDATA_DIR = generated/test-inputs BIN_DIR = bin UTIL = ${BIN_DIR}/util UTIL_SRCS = $(wildcard cmd/util/*.go) -all: testdata +VALIDATEPCAP = ${BIN_DIR}/validatepcap +VALIDATEPCAP_SRCS = $(wildcard cmd/validatepcap/*.go) + +all: testinputs util: $(UTIL_SRCS) mkdir -p ${BIN_DIR} go get ./cmd/util/... go build -o ${UTIL} ./cmd/util/... -.PHONY: testdata -testdata: util +validatepcap: $(VALIDATEPCAP_SRCS) + mkdir -p ${BIN_DIR} + go get ./cmd/validatepcap/... + go build -o ${VALIDATEPCAP} ./cmd/validatepcap/... + +.PHONY: testinputs +testinputs: util mkdir -p ${TESTDATA_DIR} ${UTIL} -make-root -out ${TESTDATA_DIR}/root.crt -key-out ${TESTDATA_DIR}/root.key -host root.com ${UTIL} -make-intermediate -cert-in ${TESTDATA_DIR}/root.crt -key-in ${TESTDATA_DIR}/root.key -out ${TESTDATA_DIR}/example.crt -key-out ${TESTDATA_DIR}/example.key -host example.com @@ -24,4 +32,5 @@ clean: rm -fr ${TESTDATA_DIR} clean-docker: + docker network prune docker builder prune diff --git a/README.md b/README.md index 24de09b..ad3303a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Tests require certificates and other cryptographic artifacts to be generated beforehand. ``` -make testdata +make testinputs ``` This command will generate: diff --git a/cmd/validatepcap/parse.go b/cmd/validatepcap/parse.go new file mode 100644 index 0000000..66a3c59 --- /dev/null +++ b/cmd/validatepcap/parse.go @@ -0,0 +1,145 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" + "strconv" +) + +type clientHelloMsg struct { + version uint16 + supportsDC bool + serverName string + supportedVersions []uint16 +} + +type serverHelloMsg struct { + version uint16 +} + +type tlsTranscript struct { + clientHello clientHelloMsg + serverHello serverHelloMsg +} + +func parsePCap(tsharkPath string, pcapPath string, keylogPath string) (transcript tlsTranscript, err error) { + rawJSON, err := exec.Command(tsharkPath, + "-r", pcapPath, + "-d", "tcp.port==4433,tls", + "-2R", "tls", + "-o", fmt.Sprintf("tls.keylog_file:%s", keylogPath), + "-T", "ek", + "-J", "tls", + "-l").Output() + if err != nil { + return transcript, err + } + + // rawJSON is formatted as newline-delimited JSON. + decoder := json.NewDecoder(bytes.NewReader(rawJSON)) + for decoder.More() { + var obj map[string]interface{} + if err = decoder.Decode(&obj); err != nil { + return transcript, err + } + // "obj" can have one of two following forms: + // { + // "index": { + // "_index": "packets-1969-12-31", + // "_type": "doc" + // } + // } + // + // or + // + // { + // "timestamp": "0981" + // "layers": { + // "frame": { ... }, + // "ppp": { ... }, + // ... + // ... + // "tls": { ... } + // } + // } + // + // We ignore objects of the first form. + if obj["layers"] == nil { + continue + } + + // We're only interested in the TLS layer. + // The "tls" object is a collection of key-value + // pairs where the values are either strings + // or arrays of strings. + tls := obj["layers"].(map[string]interface{})["tls"].(map[string]interface{}) + + // If "tls_tls_handshake_type" is an array + // of strings, then the tls object contains + // key-value pairs for multiple TLS messages. + // + // We first take stock of the types of TLS messages + // present before parsing the messages out. + messageTypes := map[string]bool{} + switch value := tls["tls_tls_handshake_type"].(type) { + case string: + messageTypes[value] = true + case []interface{}: + for _, messageType := range value { + messageTypes[messageType.(string)] = true + } + } + + if messageTypes["1"] { + err = parseOutClientHello(tls, &transcript) + if err != nil { + return transcript, err + } + + } else if messageTypes["2"] { + err = parseOutServerHello(tls, &transcript) + if err != nil { + return transcript, err + } + } + } + + return transcript, err +} + +func parseOutClientHello(raw map[string]interface{}, transcript *tlsTranscript) error { + version, err := strconv.ParseUint(raw["tls_tls_handshake_version"].(string), 0, 16) + if err != nil { + return err + } + transcript.clientHello.version = uint16(version) + + transcript.clientHello.serverName = raw["tls_tls_handshake_extensions_server_name"].(string) + + for _, val := range raw["tls_tls_handshake_extension_type"].([]interface{}) { + if val == "34" { + transcript.clientHello.supportsDC = true + } + } + + for _, val := range raw["tls_tls_handshake_extensions_supported_version"].([]interface{}) { + version, err := strconv.ParseUint(val.(string), 0, 16) + if err != nil { + return err + } + transcript.clientHello.supportedVersions = append(transcript.clientHello.supportedVersions, uint16(version)) + } + return nil +} + +func parseOutServerHello(raw map[string]interface{}, transcript *tlsTranscript) error { + version, err := strconv.ParseUint(raw["tls_tls_handshake_version"].(string), 0, 16) + if err != nil { + return err + } + transcript.serverHello.version = uint16(version) + + return nil +} diff --git a/cmd/validatepcap/validate.go b/cmd/validatepcap/validate.go new file mode 100644 index 0000000..d359ab6 --- /dev/null +++ b/cmd/validatepcap/validate.go @@ -0,0 +1,27 @@ +package main + +import ( + "errors" +) + +func validateTranscript(transcript tlsTranscript, testCase string) error { + switch testCase { + case "dc": + if transcript.clientHello.version != 0x0303 { + return errors.New("ClientHello: legacy_version is not TLS 1.2.") + } + if !transcript.clientHello.supportsDC { + return errors.New("ClientHello: support for delegated credentials not indicated.") + } + if transcript.clientHello.serverName != "example.com" { + return errors.New("ClientHello: SNI should specify example.com") + } + for _, v := range transcript.clientHello.supportedVersions { + if v == 0x0304 { + return nil + } + } + return errors.New("ClientHello: supported_versions does not include TLS 1.3.") + } + return nil +} diff --git a/cmd/validatepcap/validatepcap.go b/cmd/validatepcap/validatepcap.go new file mode 100644 index 0000000..f4c42a2 --- /dev/null +++ b/cmd/validatepcap/validatepcap.go @@ -0,0 +1,68 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os/exec" + "strconv" + "strings" +) + +const usage = `Usage: + + $ validatepcap [-help] {-pcap-in} {-keylog-in} {-testcase} + + Requires tshark with version >= 3.2.0 +` + +func main() { + log.SetFlags(0) + var ( + pcapPath = flag.String("pcap-in", "", "") + keylogPath = flag.String("keylog-in", "", "") + testcase = flag.String("testcase", "", "") + help = flag.Bool("help", false, "") + ) + + flag.Parse() + if *help || *pcapPath == "" || *keylogPath == "" || *testcase == "" { + fmt.Print(usage) + return + } + + tsharkPath, err := exec.LookPath("tshark") + fatalIfErr(err, "tshark not found in PATH.") + + tsharkConfiguration, err := exec.Command(tsharkPath, "--version").Output() + fatalIfErr(err, "Could not retrieve tshark configuration.") + + tsharkVersionLine := strings.Split(string(tsharkConfiguration), "\n")[0] + tsharkVersionFields := strings.Split(strings.Fields(tsharkVersionLine)[2], ".") + tsharkMajorVersion, err := strconv.Atoi(tsharkVersionFields[0]) + fatalIfErr(err, "Could not retrieve tshark major version.") + + tsharkMinorVersion, err := strconv.Atoi(tsharkVersionFields[1]) + fatalIfErr(err, "Could not retrieve tshark minor version.") + + if tsharkMajorVersion < 3 || tsharkMinorVersion < 2 { + log.Fatalf("Requires tshark with version >= 3.2.0.") + } + + transcript, err := parsePCap(tsharkPath, *pcapPath, *keylogPath) + fatalIfErr(err, "Could not parse supplied PCap") + + err = validateTranscript(transcript, *testcase) + if err != nil { + log.Fatalf("Testcase %s failed: %s", *testcase, err) + } else { + fmt.Printf("Testcase %s passed.\n", *testcase) + } + +} + +func fatalIfErr(err error, msg string) { + if err != nil { + log.Fatalf("ERROR: %s: %s\n", msg, err) + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 5b39940..2465edc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: hostname: network stdin_open: true tty: true + volumes: + - ./generated/test-outputs:/test-outputs cap_add: - NET_ADMIN expose: @@ -28,8 +30,9 @@ services: stdin_open: true tty: true volumes: - - ./testdata:/testdata:ro + - ./generated/test-inputs:/test-inputs:ro - ./network/endpoint-route-setup.sh:/setup-routes.sh:ro + - ./generated/test-outputs:/test-outputs cap_add: - NET_ADMIN ulimits: @@ -54,8 +57,9 @@ services: stdin_open: true tty: true volumes: - - ./testdata:/testdata:ro + - ./generated/test-inputs:/test-inputs:ro - ./network/endpoint-route-setup.sh:/setup-routes.sh:ro + - ./generated/test-outputs:/test-outputs cap_add: - NET_ADMIN ulimits: diff --git a/impl-endpoints/boringssl/run_endpoint.sh b/impl-endpoints/boringssl/run_endpoint.sh index a03fb60..ad7adbf 100644 --- a/impl-endpoints/boringssl/run_endpoint.sh +++ b/impl-endpoints/boringssl/run_endpoint.sh @@ -10,5 +10,5 @@ else echo "Running BoringSSL server." echo "Server params: $SERVER_PARAMS" echo "Test case: $TESTCASE" - bssl server -accept 4433 -cert /testdata/example.crt -key /testdata/example.key -subcert /testdata/dc.txt + bssl server -accept 4433 -cert /test-inputs/example.crt -key /test-inputs/example.key -subcert /test-inputs/dc.txt fi diff --git a/impl-endpoints/cloudflare-go/runner.go b/impl-endpoints/cloudflare-go/runner.go index b222655..d2792a3 100644 --- a/impl-endpoints/cloudflare-go/runner.go +++ b/impl-endpoints/cloudflare-go/runner.go @@ -23,39 +23,49 @@ var baseServerConfig, baseClientConfig *tls.Config func init() { // Setup the base client configuration. - pem, err := ioutil.ReadFile("/testdata/root.crt") + pem, err := ioutil.ReadFile("/test-inputs/root.crt") if err != nil { log.Fatal(err) } + clientKeyLog, err := os.OpenFile("/test-outputs/client_keylog", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Fatal(err) + } certPool := x509.NewCertPool() certPool.AppendCertsFromPEM(pem) baseClientConfig = &tls.Config{ RootCAs: certPool, + KeyLogWriter: clientKeyLog, } // Setup the base server configuration. serverCert, err := tls.LoadX509KeyPair( - "/testdata/example.crt", - "/testdata/example.key", + "/test-inputs/example.crt", + "/test-inputs/example.key", ) if err != nil { log.Fatal(err) } clientFacingCert, err := tls.LoadX509KeyPair( - "/testdata/client-facing.crt", - "/testdata/client-facing.key", + "/test-inputs/client-facing.crt", + "/test-inputs/client-facing.key", ) if err != nil { log.Fatal(err) } + serverKeyLog, err := os.OpenFile("/test-outputs/server_keylog", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + log.Fatal(err) + } baseServerConfig = &tls.Config{ Certificates: []tls.Certificate{ serverCert, clientFacingCert, }, + KeyLogWriter: serverKeyLog, } } @@ -98,7 +108,7 @@ func (r *echTestResult) eventHandler(event tls.EXP_Event) { type testCaseECHAccept struct{} func (t testCaseECHAccept) ClientConfig() (*tls.Config, error) { - base64ECHConfigs, err := ioutil.ReadFile("/testdata/ech_configs") + base64ECHConfigs, err := ioutil.ReadFile("/test-inputs/ech_configs") if err != nil { return nil, err } @@ -120,7 +130,7 @@ func (t testCaseECHAccept) ClientConfig() (*tls.Config, error) { } func (t testCaseECHAccept) ServerConfig() (*tls.Config, error) { - base64ECHKeys, err := ioutil.ReadFile("/testdata/ech_key") + base64ECHKeys, err := ioutil.ReadFile("/test-inputs/ech_key") if err != nil { return nil, err } diff --git a/impl-endpoints/nss/run_endpoint.sh b/impl-endpoints/nss/run_endpoint.sh index db5625b..3e66af7 100644 --- a/impl-endpoints/nss/run_endpoint.sh +++ b/impl-endpoints/nss/run_endpoint.sh @@ -12,12 +12,12 @@ mkdir nss_testdata if [ "$TESTCASE" = "ech-accept" ]; then # Create a PKCS8 file for the ECH keypair - python3 /ech_key_converter.py testdata/ech_key nss_testdata/ech_key_converted + python3 /ech_key_converter.py /test-inputs/ech_key nss_testdata/ech_key_converted # Create pfx files for pk12util - openssl pkcs12 -export -out nss_testdata/root.pfx -name root.com -inkey testdata/root.key -in testdata/root.crt -passout pass:"$P12_PASS" - openssl pkcs12 -export -out nss_testdata/example.pfx -name example.com -inkey testdata/example.key -in testdata/example.crt -passout pass:"$P12_PASS" - openssl pkcs12 -export -out nss_testdata/client-facing.pfx -name client-facing.com -inkey testdata/client-facing.key -in testdata/client-facing.crt -passout pass:"$P12_PASS" + openssl pkcs12 -export -out nss_testdata/root.pfx -name root.com -inkey /test-inputs/root.key -in /test-inputs/root.crt -passout pass:"$P12_PASS" + openssl pkcs12 -export -out nss_testdata/example.pfx -name example.com -inkey /test-inputs/example.key -in /test-inputs/example.crt -passout pass:"$P12_PASS" + openssl pkcs12 -export -out nss_testdata/client-facing.pfx -name client-facing.com -inkey /test-inputs/client-facing.key -in /test-inputs/client-facing.crt -passout pass:"$P12_PASS" else echo "Test case not supported." return true @@ -31,7 +31,7 @@ do # Trust the root if [ "$i" = "root" ]; then - certutil -A -n "$i".com -t "C,C,C" -i testdata/"$i".crt -d "$DB_DIR" + certutil -A -n "$i".com -t "C,C,C" -i /test-inputs/"$i".crt -d "$DB_DIR" fi done diff --git a/impl-endpoints/rustls/run_endpoint.sh b/impl-endpoints/rustls/run_endpoint.sh index 6385fed..4910c83 100644 --- a/impl-endpoints/rustls/run_endpoint.sh +++ b/impl-endpoints/rustls/run_endpoint.sh @@ -12,5 +12,5 @@ else echo "Running rustls server." echo "Server params: $SERVER_PARAMS" echo "Test case: $TESTCASE" - tlsserver --certs /testdata/example.crt --key /testdata/example.key -p 4433 echo + tlsserver --certs /test-inputs/example.crt --key /test-inputs/example.key -p 4433 echo fi diff --git a/network/Dockerfile b/network/Dockerfile index 90c83d9..0b45722 100644 --- a/network/Dockerfile +++ b/network/Dockerfile @@ -11,7 +11,12 @@ RUN mv /ns-allinone-$VERS/ns-$VERS /ns3 WORKDIR /ns3 RUN mkdir out/ -RUN ./waf configure --build-profile=release --out=out/ +# TODO(xvzcf): Change build profile back to release once +# https://gitlab.com/nsnam/ns-3-dev/-/issues/102 +# is fixed. +# TODO(xvzcf): Investigate whether a debug build profile +# affects performance testing. +RUN ./waf configure --build-profile=debug --out=out/ RUN ./waf build RUN cd / && \ @@ -53,6 +58,6 @@ ENV LD_LIBRARY_PATH="/ns3/lib" COPY run.sh . RUN chmod +x run.sh -RUN mkdir /logs +RUN mkdir /test-outputs ENTRYPOINT [ "./run.sh" ] diff --git a/network/scenarios/helper/point-to-point-helper.cc b/network/scenarios/helper/point-to-point-helper.cc index 9889e08..ef1318c 100644 --- a/network/scenarios/helper/point-to-point-helper.cc +++ b/network/scenarios/helper/point-to-point-helper.cc @@ -17,8 +17,8 @@ void RunnerPointToPointHelper::SetQueueSize(StringValue size) { NetDeviceContainer RunnerPointToPointHelper::Install(Ptr a, Ptr b) { NetDeviceContainer devices = PointToPointHelper::Install(a, b); // capture a pcap of all packets - EnablePcap("/logs/client_node_trace.pcap", devices.Get(0), false, true); - EnablePcap("/logs/server_node_trace.pcap", devices.Get(1), false, true); + EnablePcap("/test-outputs/client_node_trace.pcap", devices.Get(0), false, true); + EnablePcap("/test-outputs/server_node_trace.pcap", devices.Get(1), false, true); TrafficControlHelper tch; tch.SetRootQueueDisc("ns3::PfifoFastQueueDisc", "MaxSize", queue_size_);