diff --git a/README.md b/README.md index 4f219f9..92785b0 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ make runner 2. Tests are run with `docker-compose`, with the artifacts copied into a virtual volume. To run a test with, say, Cloudflare-Go as server and Boringssl as client, you must first build the necessary docker images, and run the appropiate -test (for example, the delegated credentials one): +test (for example, `ech-accept`): ``` -./bin/runner --client=cloudflare-go --server=boringssl --build --testcase=dc +./bin/runner --client=cloudflare-go --server=boringssl --build --testcase=ech-accept ``` diff --git a/cmd/runner/endpoint.go b/cmd/runner/endpoint.go index 0287082..34df9ea 100644 --- a/cmd/runner/endpoint.go +++ b/cmd/runner/endpoint.go @@ -23,10 +23,4 @@ var endpoints = map[string]endpoint{ client: true, server: true, }, - "tls-attacker": { - name: "tls-attacker", - regression: true, - client: true, - server: false, - }, } diff --git a/cmd/runner/runner.go b/cmd/runner/runner.go index 904bfa8..e4a677e 100644 --- a/cmd/runner/runner.go +++ b/cmd/runner/runner.go @@ -161,7 +161,7 @@ func doBuildEndpoints(client endpoint, server endpoint, verbose bool) error { } func doTestcase(t testcase, testcaseName string, client endpoint, server endpoint, verbose bool, allTestsMode bool) error { - log.Println("\nTesting: " + testcaseName) + log.Println("Testing: " + testcaseName) var result resultType err := t.setup(verbose) diff --git a/cmd/runner/testcase.go b/cmd/runner/testcase.go index 4057f7c..ac33a39 100644 --- a/cmd/runner/testcase.go +++ b/cmd/runner/testcase.go @@ -16,6 +16,7 @@ import ( "strings" "time" + "github.com/cloudflare/circl/hpke" "github.com/xvzcf/tls-interop-runner/internal/pcap" "github.com/xvzcf/tls-interop-runner/internal/utils" ) @@ -89,10 +90,15 @@ type testcaseDC struct { } func (t *testcaseDC) setup(verbose bool) error { - err := os.MkdirAll(testInputsDir, os.ModePerm) + err := os.RemoveAll(testInputsDir) if err != nil { return err } + err = os.MkdirAll(testInputsDir, os.ModePerm) + if err != nil { + return err + } + err = os.RemoveAll(testOutputsDir) if err != nil { return err @@ -278,8 +284,239 @@ func (t *testcaseDC) teardown(moveOutputs bool) error { return nil } +type testcaseECHAccept struct { + name string + timeout time.Duration + outputDir string + logger *log.Logger + logFile *os.File +} + +func (t *testcaseECHAccept) setup(verbose bool) error { + err := os.RemoveAll(testInputsDir) + if err != nil { + return err + } + err = os.MkdirAll(testInputsDir, os.ModePerm) + if err != nil { + return err + } + + err = os.RemoveAll(testOutputsDir) + if err != nil { + return err + } + err = os.MkdirAll(testOutputsDir, os.ModePerm) + if err != nil { + return err + } + runLog, err := os.Create(filepath.Join(testOutputsDir, "run-log.txt")) + if err != nil { + runLog.Close() + return err + } + if verbose { + t.logger = log.New(io.MultiWriter(os.Stdout, runLog), + "", + log.Ldate|log.Ltime|log.Lshortfile) + } else { + t.logger = log.New(io.Writer(runLog), + "", + log.Ldate|log.Ltime|log.Lshortfile) + } + + rootSignatureAlgorithm, err := utils.MakeRootCertificate( + &utils.Config{ + Hostnames: []string{"root.com"}, + ValidFrom: time.Now(), + ValidFor: 365 * 25 * time.Hour, + }, + filepath.Join(testInputsDir, "root.crt"), + filepath.Join(testInputsDir, "root.key"), + ) + if err != nil { + runLog.Close() + return err + } + t.logger.Printf("Root certificate algorithm: 0x%X\n", rootSignatureAlgorithm) + + intermediateSignatureAlgorithm, err := utils.MakeIntermediateCertificate( + &utils.Config{ + Hostnames: []string{"example.com"}, + ValidFrom: time.Now(), + ValidFor: 365 * 25 * time.Hour, + ForDC: true, + }, + filepath.Join(testInputsDir, "root.crt"), + filepath.Join(testInputsDir, "root.key"), + filepath.Join(testInputsDir, "example.crt"), + filepath.Join(testInputsDir, "example.key"), + ) + if err != nil { + runLog.Close() + return err + } + t.logger.Printf("example.com intermediate certificate algorithm: 0x%X\n", intermediateSignatureAlgorithm) + + intermediateSignatureAlgorithm, err = utils.MakeIntermediateCertificate( + &utils.Config{ + Hostnames: []string{"client-facing.com"}, + ValidFrom: time.Now(), + ValidFor: 365 * 25 * time.Hour, + ForDC: true, + }, + filepath.Join(testInputsDir, "root.crt"), + filepath.Join(testInputsDir, "root.key"), + filepath.Join(testInputsDir, "client-facing.crt"), + filepath.Join(testInputsDir, "client-facing.key"), + ) + if err != nil { + runLog.Close() + return err + } + t.logger.Printf("client-facing.com intermediate certificate algorithm: 0x%X\n", intermediateSignatureAlgorithm) + + err = utils.MakeECHKey( + utils.ECHConfigTemplate{ + Id: 123, // This is chosen at random by the client-facing server. + PublicName: "client-facing.com", + Version: utils.ECHVersionDraft13, + KemId: uint16(hpke.KEM_X25519_HKDF_SHA256), + KdfIds: []uint16{ + uint16(hpke.KDF_HKDF_SHA256), + }, + AeadIds: []uint16{ + uint16(hpke.AEAD_AES128GCM), + }, + MaximumNameLength: 0, + }, + filepath.Join(testInputsDir, "ech_configs"), + filepath.Join(testInputsDir, "ech_key"), + ) + if err != nil { + runLog.Close() + return err + } + + t.logFile = runLog + return nil + +} + +func (t *testcaseECHAccept) run(client endpoint, server endpoint) (result resultType, err error) { + pc, _, _, _ := runtime.Caller(0) + fn := runtime.FuncForPC(pc) + + cmd := exec.Command("docker-compose", "up", + "-V", + "--no-build", + "--abort-on-container-exit") + + env := os.Environ() + env = append(env, fmt.Sprintf("SERVER=%s", server.name)) + env = append(env, fmt.Sprintf("CLIENT=%s", client.name)) + env = append(env, fmt.Sprintf("TESTCASE=%s", t.name)) + // SERVER_SRC and CLIENT_SRC should not be needed, since the images + // should be built and tagged by this point. They're just set to suppress + // unset variable warnings by docker-compose. + env = append(env, "SERVER_SRC=\"\"") + env = append(env, "CLIENT_SRC=\"\"") + cmd.Env = env + + var cmdOut bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &cmdOut + cmd.Stderr = &stderr + + err = cmd.Start() + if err != nil { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + result = resultError + goto runUnsuccessful + } + + err = waitWithTimeout(cmd, t.timeout) + if err != nil { + if strings.Contains(err.Error(), "exit status 64") { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + result = resultUnsupported + goto runUnsuccessful + } + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + result = resultFailure + goto runUnsuccessful + } + + t.logger.Println("OUT: " + cmdOut.String()) + t.logger.Printf("%s completed without error.\n", fn.Name()) + return resultSuccess, nil + +runUnsuccessful: + t.logger.Println("OUT: " + cmdOut.String()) + t.logger.Println("ERROR: " + fmt.Sprint(err) + ": " + stderr.String()) + return result, err +} + +func (t *testcaseECHAccept) verify() (resultType, error) { + pc, _, _, _ := runtime.Caller(0) + fn := runtime.FuncForPC(pc) + + err := pcap.FindTshark() + if err != nil { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + t.logger.Println(err) + return resultError, err + } + + pcapPath := filepath.Join(testOutputsDir, "client_node_trace.pcap") + keylogPath := filepath.Join(testOutputsDir, "client_keylog") + transcript, err := pcap.Parse(pcapPath, keylogPath) + if err != nil { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + t.logger.Println(err) + return resultFailure, err + } + + err = pcap.Validate(transcript, t.name) + if err != nil { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + t.logger.Println(err) + return resultFailure, err + } + + t.logger.Printf("%s completed without error.\n", fn.Name()) + return resultSuccess, nil +} + +func (t *testcaseECHAccept) teardown(moveOutputs bool) error { + pc, _, _, _ := runtime.Caller(0) + fn := runtime.FuncForPC(pc) + + t.logFile.Close() + + if moveOutputs { + destDir := filepath.Join("generated", fmt.Sprintf("%s-out", t.name)) + err := os.RemoveAll(destDir) + if err != nil { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + t.logger.Println(err) + return err + } + err = os.Rename(testOutputsDir, destDir) + if err != nil { + err = &errorWithFnName{err: err.Error(), fnName: fn.Name()} + t.logger.Println(err) + return err + } + } + return nil +} + var testcases = map[string]testcase{ "dc": &testcaseDC{ name: "dc", timeout: 100 * time.Second}, + "ech-accept": &testcaseECHAccept{ + name: "ech-accept", + timeout: 100 * time.Second}, } diff --git a/cmd/util/util.go b/cmd/util/util.go index caacb70..3dfd75b 100644 --- a/cmd/util/util.go +++ b/cmd/util/util.go @@ -115,8 +115,9 @@ func main() { } else if *makeECH { err := utils.MakeECHKey( utils.ECHConfigTemplate{ + Id: 123, PublicName: *hostName, - Version: utils.ECHVersionDraft09, + Version: utils.ECHVersionDraft13, KemId: uint16(hpke.KEM_X25519_HKDF_SHA256), KdfIds: []uint16{ uint16(hpke.KDF_HKDF_SHA256), diff --git a/go.mod b/go.mod index ed57da8..f2c9555 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/xvzcf/tls-interop-runner -go 1.15 +go 1.16 require ( - github.com/cloudflare/circl v1.0.1-0.20210104183656-96a0695de3c3 - golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + github.com/cloudflare/circl v1.0.1-0.20210909160006-8e341dceb53a + golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 ) diff --git a/go.sum b/go.sum index 5257df5..653c423 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -github.com/cloudflare/circl v1.0.1-0.20210104183656-96a0695de3c3 h1:tpTW2GMi0DOdFJswbXNG6f45rOAgowhgPdofAWDKLwI= -github.com/cloudflare/circl v1.0.1-0.20210104183656-96a0695de3c3/go.mod h1:l2CvGr3DNS9Egif8pwQqJ45Ci9Y/PPs0XJHTcRKbGBQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f h1:QdHQnPce6K4XQewki9WNbG5KOROuDzqO3NaYjI1cXJ0= -golang.org/x/sys v0.0.0-20201211090839-8ad439b19e0f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.0.1-0.20210909160006-8e341dceb53a h1:T7NhgEceBBRSlVcNHzPfT3MY39Kn77Fnfpoi6ORcuf0= +github.com/cloudflare/circl v1.0.1-0.20210909160006-8e341dceb53a/go.mod h1:wqo+yhCGS0T5Ldpb0f4hdJqVGwsEBYDE3MrO6W/RACc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/impl-endpoints/boringssl/Dockerfile b/impl-endpoints/boringssl/Dockerfile index 12f5337..451d45c 100644 --- a/impl-endpoints/boringssl/Dockerfile +++ b/impl-endpoints/boringssl/Dockerfile @@ -5,10 +5,10 @@ FROM golang:latest AS builder RUN apt-get update && \ apt-get install -y git cmake ninja-build perl && \ - git clone --branch dc-spec-update https://github.com/xvzcf/boringssl /boringssl + git clone --branch master https://boringssl.googlesource.com/boringssl /boringssl WORKDIR /boringssl -RUN git checkout aac1a2d0fb616cab6a331c2534f4b1b7b8aebfa6 +RUN git checkout 295b31324f8c557dcd3c1c831857e33a7f23bc52 RUN mkdir /boringssl/build WORKDIR /boringssl/build @@ -21,11 +21,13 @@ RUN make FROM ubuntu:20.04 RUN apt-get update && \ - apt-get install -y net-tools tcpdump ethtool iproute2 + apt-get install -y net-tools tcpdump ethtool iproute2 python3 COPY --from=builder /boringssl/build/tool/bssl /usr/bin/ COPY --from=builder /runner-src/runner /usr/bin/ +COPY ech_key_converter.py / + COPY run_endpoint.sh /run_endpoint.sh RUN chmod +x /run_endpoint.sh diff --git a/impl-endpoints/boringssl/ech_key_converter.py b/impl-endpoints/boringssl/ech_key_converter.py new file mode 100644 index 0000000..f1bea35 --- /dev/null +++ b/impl-endpoints/boringssl/ech_key_converter.py @@ -0,0 +1,35 @@ +#!/usr/bin/python3 + +# SPDX-FileCopyrightText: 2022 The tls-interop-runner Authors +# SPDX-License-Identifier: MIT + +""" +This script takes an ECHKey and outputs the base64-formatted key in a file +called |ech_key_only|, and the corresponding configuration in a file called +|ech_config|, with the length prefixes stripped. +""" + +import struct +import base64 + +with open("/test-inputs/ech_key", "rb") as f: + ech_key_raw = base64.b64decode(f.read(), None, True) + offset = 2 + + # Parse out the private key. + private_key_length = struct.unpack("!H", ech_key_raw[:offset])[0] + private_key = ech_key_raw[offset : offset + private_key_length] + offset += private_key_length + + # Parse out the config. + config_length = struct.unpack("!H", ech_key_raw[offset:offset + 2])[0] + offset += 2 + config = ech_key_raw[offset : offset + config_length] + + out = open("/ech_key_only", "wb") + out.write(base64.b64encode(bytearray(private_key))) + out.close() + + out = open("/ech_config", "wb") + out.write(base64.b64encode(bytearray(config))) + out.close() diff --git a/impl-endpoints/boringssl/run_endpoint.sh b/impl-endpoints/boringssl/run_endpoint.sh index 739d09c..3f43586 100644 --- a/impl-endpoints/boringssl/run_endpoint.sh +++ b/impl-endpoints/boringssl/run_endpoint.sh @@ -7,6 +7,10 @@ set -e sh /setup-routes.sh +if [ "$TESTCASE" = "ech-accept" ]; then + python3 /ech_key_converter.py +fi + if [ "$ROLE" = "client" ]; then runner -as-client -testcase "${TESTCASE}" else diff --git a/impl-endpoints/boringssl/runner-src/client.cc b/impl-endpoints/boringssl/runner-src/client.cc index b4d7582..00f4f67 100644 --- a/impl-endpoints/boringssl/runner-src/client.cc +++ b/impl-endpoints/boringssl/runner-src/client.cc @@ -50,41 +50,6 @@ static bool DoConnection(SSL *ssl) { } unsigned int DoClient(std::string testcase) { - bssl::UniquePtr ctx(SSL_CTX_new(TLS_method())); - - g_keylog_file = fopen(g_keylog_filename, "a"); - if (g_keylog_file == nullptr) { - perror("fopen"); - return 1; - } - SSL_CTX_set_keylog_callback(ctx.get(), KeyLogCallback); - - if (testcase == "dc") { - if (!SSL_CTX_load_verify_locations(ctx.get(), "/test-inputs/root.crt", - nullptr)) { - fprintf(stderr, "Failed to load root certificates.\n"); - ERR_print_errors_fp(stderr); - return 1; - } - SSL_CTX_set_verify( - ctx.get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); - - bssl::UniquePtr ssl(SSL_new(ctx.get())); - SSL_set_tlsext_host_name(ssl.get(), "example.com"); - SSL_enable_delegated_credentials(ssl.get(), true); - - if (!DoConnection(ssl.get())) { - return 1; - } - - if (!SSL_delegated_credential_used_for_certificate_verify(ssl.get())) { - fprintf(stderr, "Delegated credential not used.\n"); - return 1; - } - - return 0; - } else { - fprintf(stderr, "Testcase unsupported.\n"); - return 64; - } + fprintf(stderr, "Testcase unsupported.\n"); + return 64; } diff --git a/impl-endpoints/boringssl/runner-src/file.cc b/impl-endpoints/boringssl/runner-src/file.cc index 1cc6fcf..48d9a4c 100644 --- a/impl-endpoints/boringssl/runner-src/file.cc +++ b/impl-endpoints/boringssl/runner-src/file.cc @@ -2,13 +2,15 @@ // SPDX-License-Identifier: ISC #include +#include + +#include #include #include #include "internal.h" - bool ReadAll(std::vector *out, FILE *file) { out->clear(); @@ -38,70 +40,18 @@ bool ReadAll(std::vector *out, FILE *file) { } } -static bool FromHexDigit(uint8_t *out, char c) { - if ('0' <= c && c <= '9') { - *out = c - '0'; - return true; - } - if ('a' <= c && c <= 'f') { - *out = c - 'a' + 10; - return true; - } - if ('A' <= c && c <= 'F') { - *out = c - 'A' + 10; - return true; - } - return false; -} - -static bool HexDecode(std::string *out, const std::string &in) { - if ((in.size() & 1) != 0) { +bool DecodeBase64(std::vector *out, const std::vector *in) { + size_t len; + if (!EVP_DecodedLength(&len, in->size())) { + fprintf(stderr, "EVP_DecodedLength failed\n"); return false; } - std::unique_ptr buf(new uint8_t[in.size() / 2]); - for (size_t i = 0; i < in.size() / 2; i++) { - uint8_t high, low; - if (!FromHexDigit(&high, in[i * 2]) || !FromHexDigit(&low, in[i * 2 + 1])) { - return false; - } - buf[i] = (high << 4) | low; - } - - out->assign(reinterpret_cast(buf.get()), in.size() / 2); - return true; -} - -bool ReadDelegatedCredential(std::vector *dc_out, - std::vector *priv_out, - const char *filename) { - ScopedFILE f(fopen(filename, "rb")); - std::vector data; - if (f == nullptr || !ReadAll(&data, f.get())) { - fprintf(stderr, "Error reading %s.\n", filename); + out->resize(len); + if (!EVP_DecodeBase64(out->data(), &len, len, in->data(), in->size())) { + fprintf(stderr, "EVP_DecodeBase64 failed\n"); return false; } - - if (!data.empty()) { - std::string dc_str = std::string(data.begin(), data.end()); - std::string::size_type comma = dc_str.find(','); - if (comma == std::string::npos) { - fprintf(stderr, - "failed to find comma in delegated credential argument.\n"); - return false; - } - - const std::string dc_hex = dc_str.substr(0, comma); - const std::string pkcs8_hex = dc_str.substr(comma + 1); - std::string dc, pkcs8; - if (!HexDecode(&dc, dc_hex) || !HexDecode(&pkcs8, pkcs8_hex)) { - fprintf(stderr, "failed to hex decode delegated credential.\n"); - return false; - } - dc_out->assign(dc.begin(), dc.end()); - priv_out->assign(pkcs8.begin(), pkcs8.end()); - - return true; - } - return false; + out->resize(len); + return true; } diff --git a/impl-endpoints/boringssl/runner-src/internal.h b/impl-endpoints/boringssl/runner-src/internal.h index 368250c..bebf963 100644 --- a/impl-endpoints/boringssl/runner-src/internal.h +++ b/impl-endpoints/boringssl/runner-src/internal.h @@ -32,9 +32,7 @@ bool ParseKeyValueArguments(std::map *out_args, const struct argument *templates); bool ReadAll(std::vector *out, FILE *in); -bool ReadDelegatedCredential(std::vector *dc_out, - std::vector *priv_out, - const char *filename); +bool DecodeBase64(std::vector *out, const std::vector *in); unsigned int DoClient(std::string testcase); unsigned int DoServer(std::string testcase); diff --git a/impl-endpoints/boringssl/runner-src/server.cc b/impl-endpoints/boringssl/runner-src/server.cc index c90a07d..6b7350d 100644 --- a/impl-endpoints/boringssl/runner-src/server.cc +++ b/impl-endpoints/boringssl/runner-src/server.cc @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -15,7 +16,7 @@ static const uint16_t g_listen_on_port = 4433; static FILE *g_keylog_file = nullptr; -static const char *g_keylog_filename = "/test-outputs/server_keylog"; +static const char *g_keylog_filename = "server_keylog"; static void KeyLogCallback(const SSL *ssl, const char *line) { (void)ssl; @@ -61,9 +62,9 @@ unsigned int DoServer(std::string testcase) { } SSL_CTX_set_keylog_callback(ctx.get(), KeyLogCallback); - if (testcase == "dc") { - if (!SSL_CTX_use_PrivateKey_file(ctx.get(), "/test-inputs/example.key", - SSL_FILETYPE_PEM)) { + if (testcase == "ech-accept") { + if (!SSL_CTX_use_PrivateKey_file( + ctx.get(), "/test-inputs/example.key", SSL_FILETYPE_PEM)) { fprintf(stderr, "Failed to load private key.\n"); return 1; } @@ -73,32 +74,54 @@ unsigned int DoServer(std::string testcase) { return 1; } bssl::UniquePtr ssl(SSL_new(ctx.get())); - SSL_set_tlsext_host_name(ssl.get(), "example.com"); - - SSL_enable_delegated_credentials(ssl.get(), true); - std::vector dc, dc_priv_raw; - if (!ReadDelegatedCredential(&dc, &dc_priv_raw, "/test-inputs/dc.txt")) { - return false; + SSL_set_tlsext_host_name(ssl.get(), "client-facing.com"); + + // Load the ECH private key + std::string ech_key_path = "/ech_key_only"; + ScopedFILE ech_key_file(fopen(ech_key_path.c_str(), "rb")); + std::vector ech_key_b64; + std::vector ech_key; + if (ech_key_file == nullptr || !ReadAll(&ech_key_b64, ech_key_file.get()) || + !DecodeBase64(&ech_key, &ech_key_b64)) { + fprintf(stderr, "Error reading %s\n", ech_key_path.c_str()); + return 1; } - CBS dc_cbs(bssl::Span(dc.data(), dc.size())); - CBS pkcs8_cbs( - bssl::Span(dc_priv_raw.data(), dc_priv_raw.size())); - - bssl::UniquePtr dc_priv(EVP_parse_private_key(&pkcs8_cbs)); - if (!dc_priv) { - fprintf(stderr, "failed to parse delegated credential private key.\n"); - return false; + + // Load the ECHConfig. + std::string ech_config_path = "/ech_config"; + ScopedFILE ech_config_file(fopen(ech_config_path.c_str(), "rb")); + std::vector ech_config_b64; + std::vector ech_config; + if (ech_config_file == nullptr || + !ReadAll(&ech_config_b64, ech_config_file.get()) || + !DecodeBase64(&ech_config, &ech_config_b64)) { + fprintf(stderr, "Error reading %s\n", ech_config_path.c_str()); + return 1; } - bssl::UniquePtr dc_buf( - CRYPTO_BUFFER_new_from_CBS(&dc_cbs, nullptr)); - if (!SSL_set1_delegated_credential(ssl.get(), dc_buf.get(), dc_priv.get(), - nullptr)) { - fprintf(stderr, "SSL_set1_delegated_credential failed.\n"); - return false; + bssl::UniquePtr keys(SSL_ECH_KEYS_new()); + bssl::ScopedEVP_HPKE_KEY key; + if (!keys || !EVP_HPKE_KEY_init(key.get(), EVP_hpke_x25519_hkdf_sha256(), + ech_key.data(), ech_key.size())) { + fprintf(stderr, "EVP_HPKE_KEY_init failed.\n"); + ERR_print_errors_fp(stderr); + return 1; + } + if (!SSL_ECH_KEYS_add(keys.get(), + /*is_retry_config=*/1, ech_config.data(), + ech_config.size(), key.get())) { + fprintf(stderr, "SSL_ECH_KEYS_add failed.\n"); + ERR_print_errors_fp(stderr); + return 1; + } + if (!SSL_CTX_set1_ech_keys(ctx.get(), keys.get())) { + fprintf(stderr, "SSL_CTX_set1_ech_keys failed.\n"); + ERR_print_errors_fp(stderr); + return 1; } if (!DoListen(ssl.get())) { + ERR_print_errors_fp(stderr); return 1; } return 0; diff --git a/impl-endpoints/cloudflare-go/Dockerfile b/impl-endpoints/cloudflare-go/Dockerfile index d72166a..838f39d 100644 --- a/impl-endpoints/cloudflare-go/Dockerfile +++ b/impl-endpoints/cloudflare-go/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && \ RUN git clone https://github.com/cloudflare/go /cf WORKDIR /cf/src -RUN git checkout 5ef1b90573f8742b763b7e65a43ce7fa20e37bb4 +RUN git checkout 7d4ce7c5377c6bf6ff5977d4734d6f6f2a10ccdb RUN ./make.bash FROM ubuntu:20.04 diff --git a/impl-endpoints/nss/Dockerfile b/impl-endpoints/nss/Dockerfile index 5660c55..55296f2 100644 --- a/impl-endpoints/nss/Dockerfile +++ b/impl-endpoints/nss/Dockerfile @@ -11,6 +11,7 @@ RUN apt-get update \ mercurial \ ninja-build \ zlib1g-dev \ + python \ && rm -rf /var/lib/apt/lists/* \ && apt-get autoremove -y && apt-get clean -y @@ -22,7 +23,7 @@ RUN cd /build \ && hg clone https://hg.mozilla.org/projects/nspr \ --rev b09175587dad2bfb923ec87250ac80461f620577 \ && hg clone https://hg.mozilla.org/projects/nss \ - --rev 38a91427d65fffd0d7f7d2b6d0bcee7dc8b77a37 \ + --rev 6796ae14d413405b09d0e90f8fc206eb9ff14864 \ && cd nss \ && ./build.sh -Denable_draft_hpke=1 \ && cd / diff --git a/impl-endpoints/nss/ech_key_converter.py b/impl-endpoints/nss/ech_key_converter.py index c168be7..a20172e 100644 --- a/impl-endpoints/nss/ech_key_converter.py +++ b/impl-endpoints/nss/ech_key_converter.py @@ -9,7 +9,7 @@ * struct { * opaque pkcs8_ech_keypair<0..2^16-1>; - * ECHConfigs configs<0..2^16>; // draft-ietf-tls-esni-09 + * ECHConfigList configs<0..2^16>; // draft-ietf-tls-esni-09 * } ECHKey; """ @@ -17,7 +17,7 @@ import struct import base64 -ECH_VERSION = 0xFE09 +ECH_VERSION = 0xFE0A DHKEM_X25519_SHA256 = 0x0020 # Hardcoded ASN.1 for ECPrivateKey, curve25519. See section 2 of rfc5958. @@ -30,35 +30,42 @@ def convert_ech_key(in_file, out_file): ech_keypair = base64.b64decode(f.read(), None, True) offset = 0 + + # Parse the private key. length = struct.unpack("!H", ech_keypair[:2])[0] offset += 2 private_key = ech_keypair[offset : offset + length] offset += length - ech_configs = ech_keypair[offset:] + # Encode the ECHConfigList that will be output. + ech_config_list = ech_keypair[offset:] - # Parse the public key out of the ECHConfig. + # Parse the length of the ECHConfigList. length = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] offset += 2 + + # Parse ECHConfig.version, where ECHConfig is the first configuration in + # ECHConfigList. version = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] offset += 2 + # Verify that the version number is as expected. if version != ECH_VERSION: - print("ECHConfig.version is not 0xFE09: %x", hex(version)) + print("ECHConfig.version is not 0xfe0a: got", hex(version)) exit(1) + # Parse ECHConfig.Length, which indicates the length of + # ECHConfig.contents. length = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] offset += 2 - # Public name - length = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] - offset += 2 + length + # Parse ECHConfig.contents.key_config.config_id. + config_id = struct.unpack("!B", ech_keypair[offset : offset + 1])[0] + offset += 1 - # Public key - length = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] + # Parse ECHConfig.contents.key_config.kem_id. + kem_id = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] offset += 2 - public_key = ech_keypair[offset : offset + length] - offset += length # Verify that the KEM is X25519. We don't support anything else. kem_id = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] @@ -66,6 +73,12 @@ def convert_ech_key(in_file, out_file): print("Unsupported KEM ID: %x", hex(kem_id)) exit(1) + # Parse ECHConfig.contents.key_config.public_key. + length = struct.unpack("!H", ech_keypair[offset : offset + 2])[0] + offset += 2 + public_key = ech_keypair[offset : offset + length] + offset += length + pkcs8 = bytearray() pkcs8 += ( bytearray(pkcs8_start) @@ -75,7 +88,7 @@ def convert_ech_key(in_file, out_file): ) out_bytes = bytearray() - out_bytes += struct.pack("!H", len(pkcs8)) + pkcs8 + ech_configs + out_bytes += struct.pack("!H", len(pkcs8)) + pkcs8 + ech_config_list out = open(out_file, "wb") out.write(base64.b64encode(out_bytes)) diff --git a/internal/pcap/validate.go b/internal/pcap/validate.go index 0d7807a..3ce4195 100644 --- a/internal/pcap/validate.go +++ b/internal/pcap/validate.go @@ -28,6 +28,16 @@ func Validate(transcript TLSTranscript, testCase string) error { } } return errors.New("ClientHello: supported_versions does not include TLS 1.3") + case "ech-accept": + if transcript.ClientHello.version != 0x0303 { + return errors.New("ClientHello: legacy_version is not TLS 1.2") + } + 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/internal/utils/ech.go b/internal/utils/ech.go index ad60c61..722258c 100644 --- a/internal/utils/ech.go +++ b/internal/utils/ech.go @@ -13,12 +13,16 @@ import ( ) const ( - ECHVersionDraft09 uint16 = 0xfe09 // draft-ietf-tls-esni-09 + ECHVersionDraft13 uint16 = 0xfe0d // draft-ietf-tls-esni-13 ) // ECHConfigTemplate defines the parameters for generating an ECH config and // corresponding key. type ECHConfigTemplate struct { + // The 1-byte configuration identifier (chosen at random by the + // client-facing server). + Id uint8 + // The version of ECH to use for this configuration. Version uint16 @@ -39,7 +43,7 @@ type ECHConfigTemplate struct { // extension, the ClientHelloInner is padded to this length in order to // protect the server name. This value may be 0, in which case the default // padding is used. - MaximumNameLength uint16 + MaximumNameLength uint8 // Extensions to add to the end of the configuration. This implementation // currently doesn't handle extensions, but this field is useful for testing @@ -53,7 +57,7 @@ type ECHConfigTemplate struct { // // struct { // opaque sk<0..2^16-1>; -// ECHConfig config<0..2^16>; // draft-ietf-tls-esni-09 +// ECHConfig config<0..2^16>; // draft-ietf-tls-esni-13 // } ECHKey; type ECHKey struct { sk []byte @@ -65,7 +69,7 @@ type ECHKey struct { // GenerateECHKey generates an ECH config and corresponding key using the // parameters specified by template. func GenerateECHKey(template ECHConfigTemplate) (*ECHKey, error) { - if template.Version != ECHVersionDraft09 { + if template.Version != ECHVersionDraft13 { return nil, errors.New("template version not supported") } @@ -92,13 +96,11 @@ func GenerateECHKey(template ECHConfigTemplate) (*ECHKey, error) { var c cryptobyte.Builder c.AddUint16(template.Version) c.AddUint16LengthPrefixed(func(c *cryptobyte.Builder) { // contents - c.AddUint16LengthPrefixed(func(c *cryptobyte.Builder) { - c.AddBytes([]byte(template.PublicName)) - }) + c.AddUint8(template.Id) + c.AddUint16(template.KemId) c.AddUint16LengthPrefixed(func(c *cryptobyte.Builder) { c.AddBytes(publicKey) }) - c.AddUint16(template.KemId) c.AddUint16LengthPrefixed(func(c *cryptobyte.Builder) { for _, kdfId := range template.KdfIds { for _, aeadId := range template.AeadIds { @@ -107,7 +109,10 @@ func GenerateECHKey(template ECHConfigTemplate) (*ECHKey, error) { } } }) - c.AddUint16(template.MaximumNameLength) + c.AddUint8(template.MaximumNameLength) + c.AddUint8LengthPrefixed(func(c *cryptobyte.Builder) { + c.AddBytes([]byte(template.PublicName)) + }) c.AddUint16LengthPrefixed(func(c *cryptobyte.Builder) { c.AddBytes(template.Extensions) }) diff --git a/internal/utils/make.go b/internal/utils/make.go index 5c2a265..fad8997 100644 --- a/internal/utils/make.go +++ b/internal/utils/make.go @@ -339,7 +339,7 @@ func MakeDelegatedCredential(config *Config, inCertPath string, inKeyPath string // MakeECHKey generates an ECH config and corresponding key, writing the key to // outKeyPath and the config to outPath. The config is encoded as an ECHConfigs -// structure as specified by draft-ietf-tls-esni-09 (i.e., it is prefixed by +// structure as specified by draft-ietf-tls-esni-13 (i.e., it is prefixed by // 16-bit integer that encodes its length). This is the format as it is consumed // by the client. func MakeECHKey(template ECHConfigTemplate, outPath, outKeyPath string) error {