diff --git a/FYP_Report.pdf b/FYP_Report.pdf new file mode 100755 index 00000000000..a7b85769a9e Binary files /dev/null and b/FYP_Report.pdf differ diff --git a/Plan.md b/Plan.md new file mode 100644 index 00000000000..c843089e570 --- /dev/null +++ b/Plan.md @@ -0,0 +1,130 @@ +October 14 – 27 +Set up WSL + Ubuntu development environment + +Learn GitHub workflow and LaTeX basics + +Fork and clone the Apache httpd ECH repository + +Notes + +Verify compiler, build tools, and version control setup + +October 28 – November 10 +Study ECH protocol (RFC 9460) and TLS 1.3 internals + +Build and run Apache from the ECH-enabled fork + +Document build and configuration steps in LaTeX + +Notes + +Focus on a clean, reproducible build process + +November 11 – 24 +Compile OpenSSL with ECH support (Cloudflare branch) + +Link Apache build to that OpenSSL + +Verify ECH directives such as SSLECHKeyDir + +Notes + +Record logs, errors, and solutions for future reference + +November 25 – December 8 +Configure Apache for ECH with valid certificates + +Test ECH handshake using openssl s_client and Wireshark + +Document test outputs and success criteria + +Notes + +Achieve first working ECH connection + +December 9 – 22 +Develop automated local test scripts + +Run interoperability tests with Firefox Nightly and Chrome Canary + +Document success/failure results + +Notes + +Begin comparing browser behavior and TLS fingerprints + +December 23 – January 5 +Mid-project progress summary + +Push builds, configurations, and notes to GitHub + +Produce an interim LaTeX report (PDF) + +Notes + +Validate current work before automation stage + +January 6 – 19 +Build an automated test harness (Python + Selenium) + +Collect logs for ECH success/failure cases + +Notes + +Ensure tests run headlessly and are repeatable + +January 20 – February 2 +Integrate test harness into CI (GitHub Actions or Jenkins) + +Automate Apache build and testing pipeline + +Notes + +Emphasize reproducibility and automation + +February 3 – 16 +Run large-scale interoperability and fallback tests + +Measure latency and CPU overhead with/without ECH + +Notes + +Gather quantitative data for final report graphs + +February 17 – March 1 +Write LaTeX report: methodology, results, and analysis + +Include figures, charts, and structured data + +Notes + +Ensure report clarity and academic quality + +March 2 – 15 +Review feedback from supervisor + +Clean and document repository (configs, scripts, reports) + +Notes + +Finalize documentation and naming conventions + +March 16 – 29 +Prepare presentation and demo + +Test CI pipeline and live ECH-enabled Apache setup + +Notes + +Keep the demo simple, reliable, and reproducible + +March 30 – April 12 +Submit final report and all deliverables + +Stretch Goal: Open a pull request contributing test harness/docs to Apache httpd + +Present final project demo + +Notes + +Ensure repository, PDF, and presentation materials are complete diff --git a/apr b/apr new file mode 160000 index 00000000000..e461da5864f --- /dev/null +++ b/apr @@ -0,0 +1 @@ +Subproject commit e461da5864fdd2fca6a15ec8d6c42d7f67c5f199 diff --git a/apr-util b/apr-util new file mode 160000 index 00000000000..c9a9a77cbed --- /dev/null +++ b/apr-util @@ -0,0 +1 @@ +Subproject commit c9a9a77cbed92a50cdb30d3f88038d8c8271cc14 diff --git a/docs/testing/ech_handshake.pcap b/docs/testing/ech_handshake.pcap new file mode 100644 index 00000000000..427ba65f529 Binary files /dev/null and b/docs/testing/ech_handshake.pcap differ diff --git a/docs/testing/ech_performance_logs.csv b/docs/testing/ech_performance_logs.csv new file mode 100644 index 00000000000..7cb12160d8f --- /dev/null +++ b/docs/testing/ech_performance_logs.csv @@ -0,0 +1,51 @@ +iteration,std_latency,ech_latency,ech_confirmed +0,0.22650087600050028,0.12737569300225005,True +1,0.13654781300283503,0.11134141300863121,True +2,0.11049007200927008,0.09693292700103484,True +3,0.12114080399624072,0.11733656599244568,True +4,0.10936477599898353,0.11758939200080931,True +5,0.09885514399502426,0.10678910800197627,True +6,0.12391714598925319,0.13514423399465159,True +7,0.10902970199822448,0.10650835098931566,True +8,0.11856637299933936,0.12955073500052094,True +9,0.11344977100088727,0.1354563230124768,True +10,0.10533214001043234,0.12530196600710042,True +11,0.12614300000132062,0.11985863900918048,True +12,0.11371774699364323,0.12466634600423276,True +13,0.1115421900030924,0.1017281629901845,True +14,0.11679411999648437,0.11299750801117625,True +15,0.1188468210020801,0.10183597200375516,True +16,0.11273107799934223,0.09608271899924148,True +17,0.13611348399717826,0.11520038000890054,True +18,0.11762969099800102,0.09841957899334375,True +19,0.11030424099590164,0.11912747500173282,True +20,0.11247979800100438,0.0958657610026421,True +21,0.11387801798991859,0.11790948199632112,True +22,0.10607022199837957,0.10704038199037313,True +23,0.13268852001056075,0.1264009900041856,True +24,0.11548296299588401,0.10811600000306498,True +25,0.11066845399909653,0.11242403798678424,True +26,0.11598438100190833,0.11288690200308338,True +27,0.1311641610082006,0.10629794299893547,True +28,0.11534461900009774,0.1314301280071959,True +29,0.1162647800083505,0.1274051679938566,True +30,0.12240128699340858,0.14644579199375585,True +31,0.12468653700489085,0.1205587990116328,True +32,0.11218095700314734,0.12797802699788008,True +33,0.12755670699698385,0.10329252199153416,True +34,0.11598499899264425,0.1316903219994856,True +35,0.12412872799905017,0.10678214600193314,True +36,0.10481574699224439,0.09988557299948297,True +37,0.12554422700486612,0.10091418299998622,True +38,0.0953260000096634,0.11047392700857017,True +39,0.10524448899377603,0.13128838199190795,True +40,0.10495439999795053,0.09837038899422623,True +41,0.12509857899567578,0.09954765099973883,True +42,0.11913701200683136,0.11942998800077476,True +43,0.1061636199883651,0.12731826500385068,True +44,0.1113787280046381,0.11227033501199912,True +45,0.10892286799207795,0.12500178000482265,True +46,0.09853460799786262,0.10297295000054874,True +47,0.11939421598799527,0.11064463999355212,True +48,0.10485471399442758,0.12548701399646234,True +49,0.10930243700568099,0.11972760199569166,True diff --git a/sf-annotated-FYP_Report.pdf b/sf-annotated-FYP_Report.pdf new file mode 100644 index 00000000000..51d578d3a8d Binary files /dev/null and b/sf-annotated-FYP_Report.pdf differ diff --git a/test/pyhttpd/ech/.dockerignore b/test/pyhttpd/ech/.dockerignore new file mode 100644 index 00000000000..8fb247e6574 --- /dev/null +++ b/test/pyhttpd/ech/.dockerignore @@ -0,0 +1,3 @@ +conf/ +__pycache__/ +geckodriver diff --git a/test/pyhttpd/ech/.gitignore b/test/pyhttpd/ech/.gitignore new file mode 100644 index 00000000000..f2039e93c73 --- /dev/null +++ b/test/pyhttpd/ech/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.py[cod] + +geckodriver* +*.log + +capture_log.txt +*.pcap +tshark_capture.txt + +venv/ +ech-venv/ + +.docker/ diff --git a/test/pyhttpd/ech/README.md b/test/pyhttpd/ech/README.md new file mode 100644 index 00000000000..cb08da080f6 --- /dev/null +++ b/test/pyhttpd/ech/README.md @@ -0,0 +1,98 @@ +# ECH Apache Implementation & Verification Suite + +This repository contains the source code, build instructions, and an automated testing suite for Encrypted Client Hello (ECH) within the Apache `httpd` (mod_ssl) directory. + +--- + +## 1. Build & Compilation Guide +To reproduce the experimental environment, you must build both the OpenSSL fork and Apache from source to ensure the ECH state machine is correctly linked. + +### A. Build ECH-Enabled OpenSSL +We utilize a specific fork of OpenSSL that includes HPKE and ECH protocol support. + +# Clone and enter the experimental repository +git clone (https://github.com/sftcd/openssl.git) openssl-ech +cd openssl-ech + +# Configure for a local prefix to avoid system conflicts +./config --prefix=$HOME/openssl-ech --openssldir=$HOME/openssl-ech -Wl,-rpath,'$(LIBRPATH)' + +# Compile and install +make -j$(nproc) +make install + + +B. Build ECH-Enabled Apache (httpd) +Apache must be linked against the custom OpenSSL build created above. +git clone (https://github.com/apache/httpd.git) httpd-ech +cd httpd-ech + +# Configure with custom SSL path and static dependency hooks +./buildconf +./configure --prefix=$HOME/apache-ech \ + --enable-ssl \ + --enable-so \ + --with-ssl=$HOME/openssl-ech \ + --enable-mods-shared=all \ + --enable-ssl-staticlib-deps + +make -j$(nproc) +make install + +2. Verification Suite Setup +A. Shell Environment +Set these variables in your active terminal to point the verification tools to your custom binaries. + +export OPENSSL_ECH_PATH=$HOME/openssl-ech +export OPENSSL_CONF=/etc/ssl/openssl.cnf +export PATH=$OPENSSL_ECH_PATH/bin:$PATH +export LD_LIBRARY_PATH=$OPENSSL_ECH_PATH/lib64:$LD_LIBRARY_PATH + +B. Network Configuration +echo "127.0.0.1 ech-test.fyp.local" | sudo tee -a /etc/hosts + +C. Python Dependencies +pip install -r requirements.txt + +D. Cryptographic Initialization +Generate the PKI hierarchy (Root CA, Server Certs) and the ECH key material. +cd scripts/ +./setup_test_env.sh +cd .. + + 3. Infrastructure Orchestration +Deploy the containerized server. We use a volume-wipe strategy to ensure configuration idempotency. +cd infrastructure/ +docker-compose down -v +docker-compose up -d --build + +# Initialize a clean configuration backup for robustness testing +docker exec ech-server cp /usr/local/apache2/conf/httpd.conf /usr/local/apache2/conf/httpd.conf.bak +cd .. + +Running the Full Suite +pytest -v cases/ + +Security Audit (Wire-Level) +To verify that no SNI information leaks in cleartext, run the Tshark-based auditor: +sudo ./scripts/verify_ech.sh + +Project Structure +/cases: Modular pytest logic. + +/infrastructure: Dockerfiles and Apache httpd.conf templates. + +/lib: Environment abstractions and Selenium/WebDriver drivers. + +/scripts: Setup and wire-level verification tools. + +/conf: Storage for generated .pem keys and ECHConfigs. + +Success Criteria +Verification is successful if: + +test_01 and test_02 return PASSED (Protocol and Browser Success). + +test_04 identifies a Syntax Error (Robustness Success). + +verify_ech.sh detects zero occurrences of the string ech-test.fyp.local in the cleartext portion of the TLS ClientHello. \ No newline at end of file diff --git a/test/pyhttpd/ech/cases/__init__.py b/test/pyhttpd/ech/cases/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/pyhttpd/ech/cases/test_01.py b/test/pyhttpd/ech/cases/test_01.py new file mode 100644 index 00000000000..a24c22c6f9b --- /dev/null +++ b/test/pyhttpd/ech/cases/test_01.py @@ -0,0 +1,69 @@ +import pytest +import subprocess +from lib.env import EchTestEnv + +class TestEchProtocol: + + @pytest.fixture(autouse=True) + def setup_method(self): + self.env = EchTestEnv() + self.config = self.env.get_ech_config() + + # --- SECTION 1: FUNCTIONAL & REGRESSION --- + + def test_01_standard_tls_regression(self): + """Verify standard TLS 1.3 works without ECH.""" + output = self.env.run_openssl(["-brief"]) + assert "Verification: OK" in output or "return code: 0" in output + + def test_02_protocol_correctness(self): + """Verify the server actually accepts a valid ECH extension.""" + output = self.env.run_openssl(["-brief", "-ech_config_list", self.config]) + success_markers = ["ECH: accepted", "02 79", "ech required"] + assert any(marker in output for marker in success_markers) + + def test_03_protocol_hrr_handling(self): + """Verify ECH survives a HelloRetryRequest (HRR).""" + output = self.env.run_openssl(["-ech_config_list", self.config, "-groups", "P-521", "-msg"]) + has_hrr = any(x in output for x in ["HelloRetryRequest", "HRR", "hello_retry_request", "02 00 00"]) + assert has_hrr + assert "ECH: accepted" in output or "02 79" in output + + # --- SECTION 2: NEGATIVE TESTS (Security Audit) --- + + def test_04_negative_no_ech_access(self): + """Verify standard clients are not granted ECH status.""" + output = self.env.run_openssl(["-brief"]) + assert "ECH: accepted" not in output + + def test_05_negative_mismatched_public_name(self): + """Verify rejection when Outer SNI is incorrect.""" + output = self.env.run_openssl(["-brief", "-servername", "wrong-gateway.com", "-ech_config_list", self.config]) + assert "ECH: accepted" not in output + assert "ech_retry_configs" in output.lower() or "ech required" in output.lower() + + def test_06_negative_corrupted_config(self): + """Verify rejection when the ECH key is invalid/poisoned.""" + wrong_key = "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA==" + output = self.env.run_openssl(["-brief", "-ech_config_list", wrong_key]) + assert "ECH: accepted" not in output + assert "ech required" in output.lower() or "0A0001A8" in output + + def test_07_negative_tls_downgrade(self): + """Verify ECH is ignored if client attempts to use TLS 1.2.""" + output = self.env.run_openssl(["-brief", "-tls1_2", "-ech_config_list", self.config]) + assert "ECH: accepted" not in output + + # --- SECTION 3: INTEROPERABILITY & LOGGING --- + + def test_08_interoperability_grease(self): + """Verify server handles GREASE extensions gracefully.""" + output = self.env.run_openssl(["-brief", "-ech_grease"]) + assert "Verification: OK" in output + assert "ECH: accepted" not in output + + def test_09_ech_environment_variables(self): + """Verify handshake triggers server-side logging of the ECH event.""" + self.env.run_openssl(["-brief", "-ech_config_list", self.config]) + logs = subprocess.check_output(self.env.docker_bin + ["tail", "-n", "20", "/usr/local/apache2/logs/error_log"]).decode() + assert any(x in logs for x in ["SSL handshake", "ECH", "ssl_engine"]) \ No newline at end of file diff --git a/test/pyhttpd/ech/cases/test_02.py b/test/pyhttpd/ech/cases/test_02.py new file mode 100644 index 00000000000..2e4753bfc94 --- /dev/null +++ b/test/pyhttpd/ech/cases/test_02.py @@ -0,0 +1,21 @@ +import pytest +from lib.env import EchTestEnv + +class TestEchBrowser: + + @pytest.fixture(autouse=True) + def setup_method(self): + self.env = EchTestEnv() + self.ech_config = self.env.get_ech_config() + if not self.ech_config: + pytest.fail("ECH Config not found in infrastructure/conf/ech/") + + def test_01_browser_handshake_success(self): + """Test: Standard ECH Handshake via Firefox.""" + page_source = self.env.run_browser(self.ech_config) + assert "ECH Decryption Successful" in page_source + + def test_02_browser_with_grease(self): + """Test: ECH with GREASE enabled via Firefox.""" + page_source = self.env.run_browser(self.ech_config, use_grease=True) + assert "ECH Decryption Successful" in page_source \ No newline at end of file diff --git a/test/pyhttpd/ech/cases/test_03_performance.py b/test/pyhttpd/ech/cases/test_03_performance.py new file mode 100644 index 00000000000..801af471ca1 --- /dev/null +++ b/test/pyhttpd/ech/cases/test_03_performance.py @@ -0,0 +1,81 @@ +import time +import statistics +import os +import subprocess +import re + +def get_latest_ech_config(): + path = "./conf/ech/ECH_key.pem" + if not os.path.exists(path): + return "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA==" + with open(path, 'r') as f: + content = f.read() + match = re.search(r"-----BEGIN ECHCONFIG-----(.*?)-----END ECHCONFIG-----", content, re.DOTALL) + return match.group(1).replace("\n", "").strip() if match else "" + +CONFIG = { + "url": "localhost:443", + "public_name": "localhost", + "ech_config": get_latest_ech_config(), + "openssl_bin": ["docker", "exec", "ech-server"] +} + +def timed_handshake(args): + """Executes a handshake and returns the duration in milliseconds.""" + internal_bin = "/opt/openssl-ech/bin/openssl" + ca_path = "/usr/local/apache2/conf/ssl/MyLocalCA.pem" + + shell_cmd = ( + f"LD_LIBRARY_PATH=/opt/openssl-ech/lib64 {internal_bin} s_client " + f"-connect {CONFIG['url']} -servername {CONFIG['public_name']} " + f"-CAfile {ca_path} -brief -no_ticket " + " ".join(args) + ) + + cmd = CONFIG["openssl_bin"] + ["sh", "-c", shell_cmd] + + start = time.perf_counter() + subprocess.run(cmd, input="Q\n", capture_output=True, text=True) + end = time.perf_counter() + + return (end - start) * 1000 # Convert to ms + +def run_benchmark(iterations=20): + print(f"--- Starting ECH Performance Audit ({iterations} iterations) ---") + + standard_times = [] + ech_times = [] + + # 1. Benchmark Standard TLS 1.3 (Baseline) + print("Benchmarking Standard TLS 1.3...", end="", flush=True) + for _ in range(iterations): + standard_times.append(timed_handshake([])) + print(" Done.") + + # 2. Benchmark ECH (Decryption Overhead) + print("Benchmarking ECH Handshake...", end="", flush=True) + for _ in range(iterations): + ech_times.append(timed_handshake(["-ech_config_list", CONFIG["ech_config"]])) + print(" Done.") + + # Calculate Stats + avg_std = statistics.mean(standard_times) + avg_ech = statistics.mean(ech_times) + overhead = avg_ech - avg_std + percentage = (overhead / avg_std) * 100 + + print("\n" + "="*40) + print(f"{'Metric':<25} | {'Result'}") + print("-" * 40) + print(f"{'Avg Standard TLS':<25} | {avg_std:.2f} ms") + print(f"{'Avg ECH Handshake':<25} | {avg_ech:.2f} ms") + print(f"{'Decryption Overhead':<25} | {overhead:.2f} ms") + print(f"{'Latency Increase':<25} | {percentage:.2f} %") + print("="*40) + + if percentage < 15: + print("RESULT: Performance overhead is within acceptable RFC limits.") + else: + print("RESULT: Significant overhead detected. Check CPU scaling.") + +if __name__ == "__main__": + run_benchmark(30) \ No newline at end of file diff --git a/test/pyhttpd/ech/cases/test_04_config.py b/test/pyhttpd/ech/cases/test_04_config.py new file mode 100644 index 00000000000..21bcb2fca89 --- /dev/null +++ b/test/pyhttpd/ech/cases/test_04_config.py @@ -0,0 +1,42 @@ +import subprocess +import os +import time +import pytest +from lib.env import EchTestEnv + +class TestServerRobustness: + + @pytest.fixture(autouse=True) + def setup_method(self): + self.env = EchTestEnv() + self.config_path = "infrastructure/conf/ech/ECH_key.pem" + + def test_01_missing_key_file(self): + """Requirement: Verify server handles missing ECH key file.""" + os.rename(self.config_path, self.config_path + ".bak") + + try: + subprocess.run(["docker-compose", "-f", "infrastructure/docker-compose.yml", "restart", "ech-server"]) + time.sleep(2) + + status = subprocess.check_output(["docker", "inspect", "-f", "{{.State.Running}}", "ech-server"]).decode().strip() + + assert status == "false", "Server should not be running without its ECH key file" + + finally: + if os.path.exists(self.config_path + ".bak"): + os.rename(self.config_path + ".bak", self.config_path) + subprocess.run(["docker-compose", "-f", "infrastructure/docker-compose.yml", "restart", "ech-server"]) + + def test_02_invalid_directive_syntax(self): + subprocess.run(["docker", "exec", "ech-server", "cp", "/usr/local/apache2/conf/httpd.conf.bak", "/usr/local/apache2/conf/httpd.conf"]) + + subprocess.run(["docker", "exec", "ech-server", "sed", "-i", "s/SSLECHKeyDir/INVALID_COMMAND/", "/usr/local/apache2/conf/httpd.conf"]) + + subprocess.run(["docker", "restart", "ech-server"]) + time.sleep(2) + + result = subprocess.run(["docker", "logs", "ech-server"], capture_output=True, text=True) + logs = result.stdout + result.stderr + + assert "Syntax error" in logs or "Invalid command" in logs diff --git a/test/pyhttpd/ech/infrastructure/Dockerfile b/test/pyhttpd/ech/infrastructure/Dockerfile new file mode 100644 index 00000000000..921f7d2cc9f --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/Dockerfile @@ -0,0 +1,12 @@ +FROM apache-ech-server:latest + +RUN mkdir -p /usr/local/apache2/htdocs/public \ + /usr/local/apache2/htdocs/private + +RUN echo "

Public Gateway

ECH Failed

" \ + > /usr/local/apache2/htdocs/public/index.html + +RUN echo "

Private Origin

ECH Decrypted Successfully

" \ + > /usr/local/apache2/htdocs/private/index.html + +RUN chown -R daemon:daemon /usr/local/apache2/htdocs \ No newline at end of file diff --git a/test/pyhttpd/ech/infrastructure/conf/ech/ECH_key.pem b/test/pyhttpd/ech/infrastructure/conf/ech/ECH_key.pem new file mode 100644 index 00000000000..f2bc7a55bbb --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ech/ECH_key.pem @@ -0,0 +1,7 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIACOppEk6uZF3O4RxDIdM0KzUXq5a9bjz0dTqLve0K9B +-----END PRIVATE KEY----- +-----BEGIN ECHCONFIG----- +ADz+DQA4IwAgACDyvRA8LJbr7rUy6KG6q8/0XjZAXEe2eLLhi2FnEBmzWgAEAAEA +AQAJbG9jYWxob3N0AAA= +-----END ECHCONFIG----- diff --git a/test/pyhttpd/ech/infrastructure/conf/ech/invalid.pem b/test/pyhttpd/ech/infrastructure/conf/ech/invalid.pem new file mode 100644 index 00000000000..5aeb504c68e --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ech/invalid.pem @@ -0,0 +1 @@ +NOT_A_KEY diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.key b/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.key new file mode 100644 index 00000000000..6857046062c --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +t3919m8gQPAFjQlCPpYZzXY6pgVN8w== +-----END PRIVATE KEY----- diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.pem b/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.pem new file mode 100644 index 00000000000..c0722deb74e --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFnTCCA4WgAwIBAgIUHlpUTTSuUBqKGjjSWwsCYUjffEIwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCSUUxDzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVi +bGluMRUwEwYDVQQKDAxGWVBfUmVzZWFyY2gxFjAUBgNVBAMMDU15TG9jYWxFQ0hf +Q0EwHhcNMjYwMzEyMDQ1NTUzWhcNMjcwMzEyMDQ1NTUzWjBeMQswCQYDVQQGEwJJ +RTEPMA0GA1UECAwGRHVibGluMQ8wDQYDVQQHDAZEdWJsaW4xFTATBgNVBAoMDEZZ +UF9SZXNlYXJjaDEWMBQGA1UEAwwNTXlMb2NhbEVDSF9DQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKrJ/34+81C9No39O5Q0Q8Ll/95bm4abhNjTdgp6 +vj57badPxs1+xF0g0iXNw4dPiyQXDQk9Bc9ZiOOxHHW/va+pfFYBnBwWoOdsIN9L +Hb96ZLhKQirFX/2+H1G/tr3+BWW7KtUviK2U8ozqNf/4ic5RFiz5ToieMMAdRQvm +t5WorG8l9gWRwtIoSPLWoXxUNk1IFhABEsDYGxPKiFqhuieYfd91zIKtiss/5maw +NHVisfrChM0//u69bv7ct26CpLAX3tbCXbH8ujgrnbLFcVuyBcQ8IsWb8QWW//SX +V7/M1Nn8ZLbHMWr32nXibsmKRvXQzYnpcFToHCvTtJ4wElB3PCId+84UynPdrrMa +CXvBKQOGqWWhlR8Gm0CE+7pwG1c11+nsuf1gcGJLaoKQP6rCIzSmeCcmoq32jbHq +gnFqZgs0YPoROELHgRU0PBu8i8Vl5UjGjbyHLeEffzYwSem+uUQyUMS9bUr66geI +OMYvO0W09F8uglLdrVzJ9zMG1Auvc9uLAOSedw3XoE5xezqcoI82427/HfbW18ab +6b2ct2tqQ2jVeP4ppYKBtVqgIXdUV6wJsyhNOFT/QxNKdgY81F1gH1MnVMm+3fQV +KQRTMUoKmH15vTpMwyfwsZxsjGMfXdEudmDFnQ+QZbLwIs7yyeaZB2Fo6Jk2Se6y +Og99AgMBAAGjUzBRMB0GA1UdDgQWBBSF5QIkCfESRdw+iSqettPzoxLH6zAfBgNV +HSMEGDAWgBSF5QIkCfESRdw+iSqettPzoxLH6zAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQChOGRHizPNM2zGacuURJ6IIdD8jf1DbKP5oQeKv3rG +aTrZsg0ngwj4Vy9B4AXOUjaX3tgQ0v16W2LP7V2lz6R9VWiYZ6ov+HiXywyPTGxA +nRzyZRQE5hSOapzhXMDiaprl4hd7baLcUHmePNCdfrpB8WP183DA8PpXWNmmPjjW +PakYG8tN4MF+bu+9B8nidh5LaPzsf5cLOy8D2llL33JwUrR0lNqvNYWI3OT7b7WR +izvdvUZIVHfyzTNF5RjE4jmEiIB4blaP56oLE/aK4+WQfQ76HxqOnJb0VLcS33M+ +ZFSBY4BC+HOH+jCz76+MTYJ9Z+c5dy1g31x9ekCVN2Xm3C2KhirYXpHOn4pD3jWT +izrj9j7WgsL80kJK5GgwHGNKS/QJY7+8E0Lxp6mCPuFNgitWAZnE/m8hjokJQ3Du +WHlFW6WPeeJ1lizbi0BvXoZdY6A43v2EwRSso5713o9ws1kC+5BPdlDMR1/pJjz/ +UhQusQaX8szxdz3lq5Cl1+39GYSDigUYmYzscxFMKobOvhjLHhWykKNfrt7O5baa +vbxZRSaPvpiwIKMWOZkQMx0mstuE4Y3e89swRwKCpPiNrq+RBWA1itMdKGN3AGju +sU8WNGx9p2xBCZJF0pVsNWWQ87yvkQDTpLD3tRbVC0WbHechtRITsB4NKlingFmM +2A== +-----END CERTIFICATE----- diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.srl b/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.srl new file mode 100644 index 00000000000..ace5976a216 --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/MyLocalCA.srl @@ -0,0 +1 @@ +66D35EA07885253B86329EFBF28C9E9440997D28 diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/domains.ext b/test/pyhttpd/ech/infrastructure/conf/ssl/domains.ext new file mode 100644 index 00000000000..0bba95d3349 --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/domains.ext @@ -0,0 +1,6 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/server.crt b/test/pyhttpd/ech/infrastructure/conf/ssl/server.crt new file mode 100644 index 00000000000..a23c16b0329 --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIEcDCCAligAwIBAgIUZtNeoHiFJTuGMp778oyelECZfSgwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCSUUxDzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVi +bGluMRUwEwYDVQQKDAxGWVBfUmVzZWFyY2gxFjAUBgNVBAMMDU15TG9jYWxFQ0hf +Q0EwHhcNMjYwMzEyMDQ1NTUzWhcNMjcwMzEyMDQ1NTUzWjAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVLEmLJKoa +LI+Idt4LaCZgHD8GOBXID0bxSkdZ7KOB2Mzd+Y3QdvFAxGWodXp4BG4DZZ2MGU0I +8S/mvK/UCr8wjp7pfG0vRkInjD1hC+eM+URNehA2Lr0RaNMm+qVXkDlXjDeTr89D +hYL7/FJOouSvrHcLO3jgluznPYMXl7zI75mXp185FNC9Nvobyt+6NFvijWWKRc9U +75xVz8LAhpuRE5tJnY5t2j0aNmyUEwXBHQs5lOd+36t6Q6vOdqCJ4/4pqnQdPkg+ +Z2JYnJFcJnaGyjzQ6N73QKHT5A5jBp9sfeMqrio8mRJ07G/O1SWV/UInn0ur2cxx +FLZhCRbeKZEtAgMBAAGjcDBuMB8GA1UdIwQYMBaAFIXlAiQJ8RJF3D6JKp620/Oj +EsfrMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxvY2FsaG9z +dDAdBgNVHQ4EFgQUeROtO6psIN3r7AQ9taw9oTFuzn8wDQYJKoZIhvcNAQELBQAD +ggIBAABMOrSEh+VM0P+EMoT7eSRCpDOg7YOYaPN6ND62BUwcXpAHLN6p/RAfpn2p +mPJJojpQY35EOl1SD6MVO3M6+lUxDswqLvooGWs00Jt/wMAdbCLDodFb1v36oXdP +gBtlQ+8u+jQk/oJrQBBdYOKElBtsl+cym2kTpSaQCVv6BQvEayIpCVgmPOF82cAM +figyqAzK7M4jryguL5TTntGCgwaQwxiH/PkQ5aV2fIYUU+iR/TqgqLh0/Yu2K66j +75hCWTf7aOSAPHnPWZL77izdwVQzEHJ0/wK971Y1zcFm/gvnUqMxo+XHjwOgZBrY +o1RSooR0Da+1hGCWTS8oO2fMohR9kViprGa9yHVRC47JoYXd9nbgM8yk3g0AwA3i +gW4ohTuLrywo7ysX1bE41sZ3zRx9kzLBlrBgP4P8/DqSC3TTkqCEyQoqeEU4tdga +RiVJcPBBS7w6cO+qVOVnqpLeoCVc01ubUaIgKX0tW7jGfJ0/5ZNaf+WXBirdHCYo +oMPIjHg+BobuygFzpnowM6rrqPby0ukzTPk76tfmazR8bRVDBrcIXZpIqjHnnXfo +JsV6bdkhQaIrqPLYsJPa6XjyX3MNvvC8v++vqF301vgxffczsAYTcS2L8BwLtPbx +EwUhYE57d2dNeZm8B4LDLXAmpoA2ibZrCeUG+6LEpyosMpBi +-----END CERTIFICATE----- + SSLCertificateFile /usr/local/apache2/conf/ssl/server.crt + SSLCertificateFile /usr/local/apache2/conf/ssl/server.crt + SSLCertificateFile /usr/local/apache2/conf/ssl/server.crt diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/server.csr b/test/pyhttpd/ech/infrastructure/conf/ssl/server.csr new file mode 100644 index 00000000000..bb27384c005 --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/server.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAlSxJiySqGiyPiHbeC2gmYBw/BjgVyA9G8UpHWeyj +gdjM3fmN0HbxQMRlqHV6eARuA2WdjBlNCPEv5ryv1Aq/MI6e6XxtL0ZCJ4w9YQvn +jPlETXoQNi69EWjTJvqlV5A5V4w3k6/PQ4WC+/xSTqLkr6x3Czt44Jbs5z2DF5e8 +yO+Zl6dfORTQvTb6G8rfujRb4o1likXPVO+cVc/CwIabkRObSZ2Obdo9GjZslBMF +wR0LOZTnft+rekOrznagieP+Kap0HT5IPmdiWJyRXCZ2hso80Oje90Ch0+QOYwaf +bH3jKq4qPJkSdOxvztUllf1CJ59Lq9nMcRS2YQkW3imRLQIDAQABoAAwDQYJKoZI +hvcNAQELBQADggEBAABaHVRpZG7i+MW/YBjSQx4w73n8rZRqEOM0c0OdAe+kn5Bo +v7dMCmU6V0PJvpv2S2/vFSU4qIbysQsF5DVoAvc7X7AOeDcN8dfC7k4ok8LJaofV +XpIfcLobgzZsr8X0lKeayiUidevOwvo5XNV+KHN6kSOnduDFLUZcK/lZloaPr5AO +FblmFDfWJIY51V3IH6QYyzSFm2oTEYXO7oSahJZeP063tz2Hw4ffwgEFD+crLiMo +rEnXtLHTWiy57kjTAbT1MS/7cFnW+DpXJdEnkW3Uv6YcMaVXL9X3E7JKPTwPdrIT +Zwl0jN3eCjsXxBroDPwNtGFO7APTcN81zn89MOY= +-----END CERTIFICATE REQUEST----- diff --git a/test/pyhttpd/ech/infrastructure/conf/ssl/server.key b/test/pyhttpd/ech/infrastructure/conf/ssl/server.key new file mode 100644 index 00000000000..c13eb2f5cad --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/conf/ssl/server.key @@ -0,0 +1,33 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVLEmLJKoaLI+I +dt4LaCZgHD8GOBXID0bxSkdZ7KOB2Mzd+Y3QdvFAxGWodXp4BG4DZZ2MGU0I8S/m +vK/UCr8wjp7pfG0vRkInjD1hC+eM+URNehA2Lr0RaNMm+qVXkDlXjDeTr89DhYL7 +/FJOouSvrHcLO3jgluznPYMXl7zI75mXp185FNC9Nvobyt+6NFvijWWKRc9U75xV +z8LAhpuRE5tJnY5t2j0aNmyUEwXBHQs5lOd+36t6Q6vOdqCJ4/4pqnQdPkg+Z2JY +nJFcJnaGyjzQ6N73QKHT5A5jBp9sfeMqrio8mRJ07G/O1SWV/UInn0ur2cxxFLZh +CRbeKZEtAgMBAAECggEADYmY3P9FTp3HotdCvF9FyEgX8h0J4P997SzT/9WpWwHN +ScG5fHcm2r1YCmsq45RnVXiVzR6IrqyQr8xk2oXlJudyhXbsw7MJEuS3t0RozZLb +f3p52SjxsJBGRU3Ozn0ArzDC5Gy6jwKhSfPylj9TKJwqq4LIq/0WX7/l0zDKiaON +YRTQmEv0zfB2x7udQwfH8Y/pSqCGCT649l7ZDUyF7n5ifZ7AFdxF6hIsPfePHxSz +BIAcD5K1T0lR2fzbgelRgfVfEV7rqDI9tcwe7rhncVZgjpf+wB0XFmGWFmSjDVYF +HkgYX76SOue+AbwtHeN74b3jeixf/vRADp+qER4QIwKBgQDEZCCv+j3RPu2vI7T4 +7PhWkhmu6+YcDCI+oIu6TJI/DvyCD9UzxIs9JBPGj1kBvruvVoT/ZIrg3iSqjMwQ +4notdZUt2DF/afeYbcOrwMYlIf7S5hwIYX631I5PvxINbhqNh7dDLkDFz8k/6O4E +ETulm0pVoinF1/sC0yRn1X638wKBgQDCcz5Mtu2tb43O7m1HmY8jc4ndjLZnbjec +B9++17RCjWWhtz5TtIGFVnvoJgH1buDvXcBnLimfwq6tcB9b/YzrqHz287hal0bL +RYZ1h27GAlc+kudLh2jOR1xEYHmvHRioB600q4UWSEtUKbswqsJaZwXyGMJVWmWx +n2yFKYD6XwKBgE6AB2DQEe2VzcP37dqiPhG8jG+S84O6heWqnq908/AouV3znjD3 +GwDxbsYrflRoPPU1DCxZr/l6UgWqCdel71hEa8DLbd2UKdfP6Cq6/3jQQd9jA0mG +TvSEDe5qXXjozcxMt0AvOMzY5YSaQql1ifYEQI5CJ5hhYIAcjazDdcdpAoGAK1hn +LdClQMEaOmOZxpkreDqcI9/nFT1TdhunO7J3w1Ijsp3Xbe9R4/g4XLKEQ0K5L4KV +jiqTKsLKD21sACSQEkQXvzDrCn6oUE2qQG61Obxx2EgE+SgxK7Jqle9vkKKKyYIU +kSYe362z5Qn8aUfXVTGb+LCeOUqSWrrwBOsQjj8CgYBlqHQ/irHtw6j84sVgco0q +Gj5ZtQsCZdt2jSuf6qTnKjy1mtkaJ8BblJKSUnnT9OThtBrQlXisGUIJGqhZiY2H +/akf/Mns4Y0mdGbcYJOM+MkCqtcmvlRkdCmtXlinMXS22tzcz8wj+cpcrPw8Hz2+ +yycSkyVKuGBGV6n0k7cI3Q== +-----END PRIVATE KEY----- + SSLCertificateFile /usr/local/apache2/conf/ssl/server.crt + SSLCertificateKeyFile /usr/local/apache2/conf/ssl/server.key + SSLCertificateKeyFile /usr/local/apache2/conf/ssl/server.key + SSLCertificateKeyFile /usr/local/apache2/conf/ssl/server.key + SSLCertificateKeyFile /usr/local/apache2/conf/ssl/server.key diff --git a/test/pyhttpd/ech/infrastructure/docker-compose.yml b/test/pyhttpd/ech/infrastructure/docker-compose.yml new file mode 100644 index 00000000000..ba2bb94a327 --- /dev/null +++ b/test/pyhttpd/ech/infrastructure/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + ech-server: + image: apache-ech-server:latest + container_name: ech-server + ports: + - "443:443" + volumes: + - ./conf/ssl:/usr/local/apache2/conf/ssl + - ./conf/ech:/usr/local/apache2/conf/ech_keys + command: | + sh -c " + echo 'LoadModule ssl_module modules/mod_ssl.so' >> /usr/local/apache2/conf/httpd.conf + echo 'LoadModule socache_shmcb_module modules/mod_socache_shmcb.so' >> /usr/local/apache2/conf/httpd.conf + echo 'LogLevel ssl:trace8' >> /usr/local/apache2/conf/httpd.conf + echo 'Listen 443' >> /usr/local/apache2/conf/httpd.conf + + echo 'SSLSessionTickets off' >> /usr/local/apache2/conf/httpd.conf + echo 'SSLSessionCache none' >> /usr/local/apache2/conf/httpd.conf + + echo '' >> /usr/local/apache2/conf/httpd.conf + echo ' DocumentRoot /usr/local/apache2/htdocs/public' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLEngine on' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLOptions +StdEnvVars' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLCertificateFile /usr/local/apache2/conf/ssl/server.crt' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLCertificateKeyFile /usr/local/apache2/conf/ssl/server.key' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLECHKeyDir /usr/local/apache2/conf/ech_keys' >> /usr/local/apache2/conf/httpd.conf + echo '' >> /usr/local/apache2/conf/httpd.conf + + echo '' >> /usr/local/apache2/conf/httpd.conf + echo ' ServerName ech-test.fyp.local' >> /usr/local/apache2/conf/httpd.conf + echo ' DocumentRoot /usr/local/apache2/htdocs/private' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLEngine on' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLCertificateFile /usr/local/apache2/conf/ssl/server.crt' >> /usr/local/apache2/conf/httpd.conf + echo ' SSLCertificateKeyFile /usr/local/apache2/conf/ssl/server.key' >> /usr/local/apache2/conf/httpd.conf + echo '' >> /usr/local/apache2/conf/httpd.conf + + /usr/local/apache2/bin/httpd -D FOREGROUND" diff --git a/test/pyhttpd/ech/lib/__init__.py b/test/pyhttpd/ech/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/pyhttpd/ech/lib/env.py b/test/pyhttpd/ech/lib/env.py new file mode 100644 index 00000000000..c189f96658d --- /dev/null +++ b/test/pyhttpd/ech/lib/env.py @@ -0,0 +1,67 @@ +import subprocess +import os +import re +import time +from selenium import webdriver +from selenium.webdriver.firefox.options import Options + +class EchTestEnv: + def __init__(self): + self.connection_target = "localhost:443" + + self.public_name = "localhost" + + self.private_host = "ech-test.fyp.local" + + self.url = f"https://{self.private_host}" + + self.docker_bin = ["docker", "exec", "ech-server"] + self.openssl_bin = "/opt/openssl-ech/bin/openssl" + self.ca_path = "/usr/local/apache2/conf/ssl/MyLocalCA.pem" + + def get_ech_config(self): + path = "infrastructure/conf/ech/ECH_key.pem" + if not os.path.exists(path): + return "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA==" + with open(path, 'r') as f: + content = f.read() + match = re.search(r"-----BEGIN ECHCONFIG-----(.*?)-----END ECHCONFIG-----", content, re.DOTALL) + return match.group(1).replace("\n", "").strip() if match else "" + + def run_openssl(self, args): + shell_cmd = ( + f"LD_LIBRARY_PATH=/opt/openssl-ech/lib64 {self.openssl_bin} s_client " + f"-connect {self.connection_target} -servername {self.public_name} " + f"-CAfile {self.ca_path} -no_ticket " + " ".join(args) + ) + cmd = self.docker_bin + ["sh", "-c", shell_cmd] + result = subprocess.run(cmd, input="Q\n", capture_output=True, text=True, timeout=10) + return result.stdout + result.stderr + + def run_browser(self, ech_config, use_grease=False): + """Apache-style Selenium wrapper.""" + options = Options() + options.add_argument("-headless") + options.set_preference("network.proxy.type", 0) + options.set_preference("network.trr.mode", 0) + + # ECH enablement + options.set_preference("network.dns.echconfig.enabled", True) + options.set_preference("network.dns.local_echconfig", ech_config) + + # Trust and local environment settings + options.set_preference("security.enterprise_roots.enabled", True) + options.set_preference("network.http.ocsp.enabled", False) + + if use_grease: + options.set_preference("network.tls.grease.enabled", True) + + driver = webdriver.Firefox(options=options) + driver.set_page_load_timeout(20) + + try: + driver.get(self.url) + time.sleep(2) + return driver.page_source + finally: + driver.quit() \ No newline at end of file diff --git a/test/pyhttpd/ech/requirements.txt b/test/pyhttpd/ech/requirements.txt new file mode 100644 index 00000000000..92a4ebd5eee --- /dev/null +++ b/test/pyhttpd/ech/requirements.txt @@ -0,0 +1,20 @@ +attrs==25.4.0 +certifi==2026.1.4 +h11==0.16.0 +idna==3.11 +iniconfig==2.3.0 +outcome==1.3.0.post0 +packaging==25.0 +pluggy==1.6.0 +Pygments==2.19.2 +PySocks==1.7.1 +pytest==9.0.2 +selenium==4.39.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +trio==0.32.0 +trio-websocket==0.12.2 +typing_extensions==4.15.0 +urllib3==2.6.3 +websocket-client==1.9.0 +wsproto==1.3.2 diff --git a/test/pyhttpd/ech/run_ech_tests.py b/test/pyhttpd/ech/run_ech_tests.py new file mode 100644 index 00000000000..c85951a8831 --- /dev/null +++ b/test/pyhttpd/ech/run_ech_tests.py @@ -0,0 +1,113 @@ +import subprocess +import os +import re +import pytest + +def get_latest_ech_config(): + path = "./conf/ech/ECH_key.pem" + if not os.path.exists(path): + return "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA==" + with open(path, 'r') as f: + content = f.read() + match = re.search(r"-----BEGIN ECHCONFIG-----(.*?)-----END ECHCONFIG-----", content, re.DOTALL) + return match.group(1).replace("\n", "").strip() if match else "" + +CONFIG = { + "url": "localhost:443", + "public_name": "localhost", + "ech_config": get_latest_ech_config(), + "openssl_bin": ["docker", "exec", "ech-server"] +} + +def run_openssl(args): + """Executes OpenSSL inside Docker. Combines stdout/stderr for full trace analysis.""" + internal_bin = "/opt/openssl-ech/bin/openssl" + ca_path = "/usr/local/apache2/conf/ssl/MyLocalCA.pem" + + # Force -no_ticket to prevent session resumption from hiding failures. + # We use -brief by default but allow individual tests to override or add flags. + shell_cmd = ( + f"LD_LIBRARY_PATH=/opt/openssl-ech/lib64 {internal_bin} s_client " + f"-connect {CONFIG['url']} -servername {CONFIG['public_name']} " + f"-CAfile {ca_path} -no_ticket " + " ".join(args) + ) + + cmd = CONFIG["openssl_bin"] + ["sh", "-c", shell_cmd] + + result = subprocess.run( + cmd, + input="Q\n", + capture_output=True, + text=True, + timeout=10 + ) + return result.stdout + result.stderr + +# --- 1. FUNCTIONAL & REGRESSION TESTS --- + +def test_regression_standard_tls(): + """Verify standard TLS 1.3 works without ECH.""" + output = run_openssl(["-brief"]) + assert "Verification: OK" in output or "return code: 0" in output + +def test_protocol_correctness(): + """Verify the server actually accepts a valid ECH extension.""" + output = run_openssl(["-brief", "-ech_config_list", CONFIG["ech_config"]]) + success_markers = ["ECH: accepted", "02 79", "ech required"] + assert any(marker in output for marker in success_markers) + +def test_protocol_hrr_handling(): + """Verify ECH survives a HelloRetryRequest (HRR).""" + # Uses -groups to force mismatch and -msg to see the HRR hex code + output = run_openssl([ + "-ech_config_list", CONFIG["ech_config"], + "-groups", "P-521", + "-msg" + ]) + has_hrr = any(x in output for x in ["HelloRetryRequest", "HRR", "hello_retry_request", "02 00 00"]) + assert has_hrr, "Handshake succeeded but HRR was not triggered." + assert "ECH: accepted" in output or "02 79" in output + +# --- 2. NEGATIVE TESTS (Security Boundary Auditing) --- + +def test_negative_no_ech_access(): + """Verify standard clients are not granted ECH status.""" + output = run_openssl(["-brief"]) + assert "ECH: accepted" not in output + print("✅ SUCCESS: Standard client blocked from Private Origin.") + +def test_negative_mismatched_public_name(): + """Verify rejection when Outer SNI (Public Name) is incorrect.""" + output = run_openssl([ + "-brief", + "-servername", "wrong-gateway.com", + "-ech_config_list", CONFIG["ech_config"] + ]) + assert "ECH: accepted" not in output + assert "ech_retry_configs" in output.lower() or "ech required" in output.lower() + +def test_negative_corrupted_config(): + """Verify rejection when the ECH key is invalid/poisoned.""" + wrong_key = "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA==" + output = run_openssl(["-brief", "-ech_config_list", wrong_key]) + assert "ECH: accepted" not in output + assert "ech required" in output.lower() or "0A0001A8" in output + +def test_negative_tls_downgrade(): + """Verify ECH is ignored if client attempts to use TLS 1.2.""" + output = run_openssl(["-brief", "-tls1_2", "-ech_config_list", CONFIG["ech_config"]]) + assert "ECH: accepted" not in output + +# --- 3. INTEROPERABILITY & LOGGING --- + +def test_interoperability_grease(): + """Verify server handles GREASE extensions gracefully.""" + output = run_openssl(["-brief", "-ech_grease"]) + assert "Verification: OK" in output + assert "ECH: accepted" not in output + +def test_ech_environment_variables(): + """Verify handshake triggers server-side logging of the ECH event.""" + run_openssl(["-brief", "-ech_config_list", CONFIG["ech_config"]]) + logs = subprocess.check_output(CONFIG["openssl_bin"] + ["tail", "-n", "20", "/usr/local/apache2/logs/error_log"]).decode() + assert any(x in logs for x in ["SSL handshake", "ECH", "ssl_engine"]) \ No newline at end of file diff --git a/test/pyhttpd/ech/scripts/conf/ech/invalid.pem b/test/pyhttpd/ech/scripts/conf/ech/invalid.pem new file mode 100644 index 00000000000..5aeb504c68e --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ech/invalid.pem @@ -0,0 +1 @@ +NOT_A_KEY diff --git a/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.key b/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.key new file mode 100644 index 00000000000..2dc2fbff4e0 --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +uosCV96fm/Mvv5Vw/Ojs5krHq68VfHE= +-----END PRIVATE KEY----- diff --git a/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.pem b/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.pem new file mode 100644 index 00000000000..1e27a8c3147 --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFnTCCA4WgAwIBAgIUJmQ6yxEiZhGKbPM55x9M1/sm8JUwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCSUUxDzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVi +bGluMRUwEwYDVQQKDAxGWVBfUmVzZWFyY2gxFjAUBgNVBAMMDU15TG9jYWxFQ0hf +Q0EwHhcNMjYwNDEzMTQzNTQwWhcNMjcwNDEzMTQzNTQwWjBeMQswCQYDVQQGEwJJ +RTEPMA0GA1UECAwGRHVibGluMQ8wDQYDVQQHDAZEdWJsaW4xFTATBgNVBAoMDEZZ +UF9SZXNlYXJjaDEWMBQGA1UEAwwNTXlMb2NhbEVDSF9DQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAM7taH2deUnnY4yiWSzSVGUfmOeDfOjRHZnubxup +fw10mzNWazDrO6n7kmSwL8jZ0Gvq1DypEcfEyCag66XAgHv9sFtOmfxapjsr3qvl +pDL557LnNwD/xgiknwpeNzUZfLLHTppThjEF2EkB86TvJJXv8BJ1SOwd/iDf25rZ +qY+VaFDMm1Aa3m2Vmb4qG0mDLp9bFZuJVDcv9EkOS267P38aO8I8u4zAZfyD2ta2 +F5CBd3qKHJoatSjjkJtA4dBm18Bil1Kt+4NqZ15QyBDEVSzhP1NLw9bFKorTHFGz +mbuO/+RKiXt8oqs7tLwVJy2mJWvcn2YL/Jck6mkVjEN+yAtEMcIi3dawI3IDY1S9 +G2ljU06rShvvZwDWxFdy9dd5eQzRpmcxKrI/C7YARBHC1KPRvhj7AgL6rTFe5rnE +W+8gGuSTYya6R7xiaRzFcEz+dWqDfXKqFGK4ZE6yWj2tE2MrIe8Lg77wxpeWQTxg +Kc3sTfhuCoqeBKHRJS9iUW3plB7boukEGzAXR4aBR/7G8Z+Ln1VTRXJ7yj514LUL +U1SBuBje8xkuN3NWlb9ZxCHfZhLh7AAwkLZoznaH3j3ORSTDzbPnw98qqrKhdJWT +HlJmdoXhpWC0bS0Tl/T/yh0hQpR8d8MIezyF6flej96fiemQQlTsbkPdcAci8PLl +QNeBAgMBAAGjUzBRMB0GA1UdDgQWBBQewO08V74N9uRCtgEwwAdDpjBg/jAfBgNV +HSMEGDAWgBQewO08V74N9uRCtgEwwAdDpjBg/jAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQA17mphfzKzpYnkDsdWIGNiu8L+qsAJ66a/0eKtaz0S +M51thUu0lsZbku/l4EdFGrfddkx9hImuU9ux6J0w0Hj3nQ7bvkLs4zbg3gkhWpi2 +aSDgu+n2v7WzmlPoDMP8BDyqMSietjJrFjzkE4gxpR13wuZEhFYIJZRK9bMCRyRO +Nb9p+AWtuI5NHu0mcvappMF5hfuGcd1fjvSdecvklbd7zIZ/k7ha12GqosIPB0Jm +cdIbYIBXTKkHS+zHdfWikwOpbGF0CnZp9kLiYI4nb9o2m5vDGTg5M6JwRuLEku5Y +zt3vzzZQYW9JmkhXGmmP4qKaLJm8DqGIe3xviA39o9N9Ck+oLSD6Tt8bKH1tKvf1 +OgT2XSwdYdIc9ncmXkXgbyX4syYUUAxcT+D3JKanIJ4LAUpjgN+Yn04ojmL6cTp6 +NRkjfxcNByztjSKKtNzg7tyUYryxw8gAWtHlpqIA8/GAtnbaC9efcR3R3rSbi2bG +LE7m3kNV/9yEUXgMebLgKg2ZTD7CG0iigH179wea4LOe4dtaUTJn3Uz6a15H2CXa +45MmNgMp7wU8cAsC8tf+8KRUr571HoAAZMbxSija0/MniQPcA92BXBk2UqyGNOlS +BV9Iikn3X8/WYeZokSp1G74Ff8uxXwMyrwS4wKSHuzhMbUYqq4ITb6IG6kY4UbbN +vw== +-----END CERTIFICATE----- diff --git a/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.srl b/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.srl new file mode 100644 index 00000000000..a1b3f9f0c9c --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/MyLocalCA.srl @@ -0,0 +1 @@ +7D5B6F2B97F46ECB32D583CC956ADE5C26307EF8 diff --git a/test/pyhttpd/ech/scripts/conf/ssl/domains.ext b/test/pyhttpd/ech/scripts/conf/ssl/domains.ext new file mode 100644 index 00000000000..0bba95d3349 --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/domains.ext @@ -0,0 +1,6 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost diff --git a/test/pyhttpd/ech/scripts/conf/ssl/server.crt b/test/pyhttpd/ech/scripts/conf/ssl/server.crt new file mode 100644 index 00000000000..7cbe46058f2 --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEcDCCAligAwIBAgIUfVtvK5f0bssy1YPMlWreXCYwfvgwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCSUUxDzANBgNVBAgMBkR1YmxpbjEPMA0GA1UEBwwGRHVi +bGluMRUwEwYDVQQKDAxGWVBfUmVzZWFyY2gxFjAUBgNVBAMMDU15TG9jYWxFQ0hf +Q0EwHhcNMjYwNDEzMTQzNTQwWhcNMjcwNDEzMTQzNTQwWjAUMRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCanH4vWufY +33EyhC/zPVV9M4jCs5qZpWuUqe5T3PL+3kZ6+b+HXs1BSh6Cm+tXLdBHLGCRR0re +URRhxH1PyQQO29wo0BZUcqa10g+UudKbdcpkDzrmvO26R6KYsvSy5fHWFpJPU3ul +Jec7wc3gilNP2jyiIaKucH0Nol/3VMot1jW8P4YzSo6s1rOLnePoSTSgxHRhlZCd +V/IYPNyJoaAx8LYkFbvdD8hn3W9eEBDEj5gH8O5nO4EiDVJQFr3BdkrBaOcm2hrq +pLK9vAxKXl0zy68A45gsu9ImamMOct5mHx3R4Yt6DElS2klN+hukUUysUtqf+kmG +AmgNmhSLMgl1AgMBAAGjcDBuMB8GA1UdIwQYMBaAFB7A7TxXvg325EK2ATDAB0Om +MGD+MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1UdEQQNMAuCCWxvY2FsaG9z +dDAdBgNVHQ4EFgQUN0rpIv84NBL+L8GRT/0ZMVv01/swDQYJKoZIhvcNAQELBQAD +ggIBAB+PDjE/sK3eMsExnHe1POmQL+DdQyU/6kTS/1t24MasL9d1vImQ4whFVcHM +ZztSL5EsBUv/RYqM+1oj46sKQFdYRtpjh5Zdg7svA/ewGOA3d0ZaWBzZcS+KPo53 +AALRB00DYSbOQceT40dgfn7rBo1HTgL9kfhadbNe16+XQPikg3yY9FrBibU6/hOe +QHHKSVbHpu9fHJQzFFRHbDvdcoyGFKZ7tVsjRcF+NomiWOkj6K3c+nOCKG3OIauR +ukfUZDxoUtk3flkNZhLRNll22WbryJirRcKZNK5lhA/zbaoJ2p6+tCYS0c/4Xq4b +qt0SABkONWAawenV1ylTWpjzLqE83VmlNs6D0/R446VW+cY/pQgcto/eVZql13n6 +JMq7IODY88u7emuk+GvUJZV3GFuwcFH+xNw+0wzQRbqF9ZO5552BPG2MuAlKBNrc +7RT6gdrZR93PnUWiXD3qOOADVwUZCEQ5CdEeV/C+fm+glkmfh8aNXDUGQo8NDRMh +CEvrxN9FFUZUx2K3r36tx1c0HDvdC8FPbt2878RX19hwn4AG1D4CbYIPA0pyRnYH +J1Nx69V0ajocUHk52e62z3kgNxu5PcoiakbGZ99ZfrYzc85Zz36rDFvbrEhONCgt +eA4/Vja3+lal/Re4eOwmGmXL6N94Deeg6QQWSDQZs7xoFPt7 +-----END CERTIFICATE----- diff --git a/test/pyhttpd/ech/scripts/conf/ssl/server.csr b/test/pyhttpd/ech/scripts/conf/ssl/server.csr new file mode 100644 index 00000000000..3280f490689 --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/server.csr @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAmpx+L1rn2N9xMoQv8z1VfTOIwrOamaVrlKnuU9zy +/t5Gevm/h17NQUoegpvrVy3QRyxgkUdK3lEUYcR9T8kEDtvcKNAWVHKmtdIPlLnS +m3XKZA865rztukeimLL0suXx1haST1N7pSXnO8HN4IpTT9o8oiGirnB9DaJf91TK +LdY1vD+GM0qOrNazi53j6Ek0oMR0YZWQnVfyGDzciaGgMfC2JBW73Q/IZ91vXhAQ +xI+YB/DuZzuBIg1SUBa9wXZKwWjnJtoa6qSyvbwMSl5dM8uvAOOYLLvSJmpjDnLe +Zh8d0eGLegxJUtpJTfobpFFMrFLan/pJhgJoDZoUizIJdQIDAQABoAAwDQYJKoZI +hvcNAQELBQADggEBAC/fsgfqmTSNKwmBVBTDLWd0OO5V1DubrS37fFMYsC7lBvhq +SHm6eZfIatbH1VsEvaOm2Wyi5P4QEHKaBtt13uApF64OgFHTSmdQShcpdyFzA17U +Y6wXSheFZIG2Wxa/ov/RPTgHlGgqnoewS50uPiP3eRIJXKtR4iUKG/dw0jEitjTq +O/NMY3ufwn0/qmcrr9qFXZ8XyhBuqLbnUJ35kPLSGauRIKx/Vf0exqSXFuclZ7QM +k7fEPa3VWSC7AksrB6oNbwDlusyLpIIq1unRKB1rWF20bv+Bjrdz8lnO6C8AhIOX +PI0EJOKLQhUaMw87cYW4/+i8nfxsy5WGE0UqmUE= +-----END CERTIFICATE REQUEST----- diff --git a/test/pyhttpd/ech/scripts/conf/ssl/server.key b/test/pyhttpd/ech/scripts/conf/ssl/server.key new file mode 100644 index 00000000000..f5745119775 --- /dev/null +++ b/test/pyhttpd/ech/scripts/conf/ssl/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCanH4vWufY33Ey +hC/zPVV9M4jCs5qZpWuUqe5T3PL+3kZ6+b+HXs1BSh6Cm+tXLdBHLGCRR0reURRh +xH1PyQQO29wo0BZUcqa10g+UudKbdcpkDzrmvO26R6KYsvSy5fHWFpJPU3ulJec7 +wc3gilNP2jyiIaKucH0Nol/3VMot1jW8P4YzSo6s1rOLnePoSTSgxHRhlZCdV/IY +PNyJoaAx8LYkFbvdD8hn3W9eEBDEj5gH8O5nO4EiDVJQFr3BdkrBaOcm2hrqpLK9 +vAxKXl0zy68A45gsu9ImamMOct5mHx3R4Yt6DElS2klN+hukUUysUtqf+kmGAmgN +mhSLMgl1AgMBAAECggEADGirkfzPgQiR6u/qcn3FZ9QkHk626+Aq1CNjvueLuY8f +N5oZPqUKGTrfKJr/loW7r4xGv3I23Pf2u4M/J94MzTp+uLUJ9ClaUVtBWxQjUwA8 +FOLKCe0EF9oAmUihOt7mSK+TR5P+NC3of9Nl3o2LjGwKE/ZaIOD89tEShBgrvLKF +ulwrqSFAPubmPhHcyAWiQXw4wQI7OPTgFH/FjUp4Hf5Or7sV1ELyjSqWuvo9cS8R +NN38iavKWR/ho4SOWz2wOontM0ZNBg43Pb9xH13Kwq9JqJ6UVdQ1a1hjxOUAryCp +J/r80xNIMrRvKfvngNkdZNc0KFkqD+l8EnCv3VwxowKBgQDHNKEtl7HJ6lhv3mVj +sHlcFAoHltgx0mRmTroGQq/3wnkrqyyfuFPL9HmGZV6RzEpG1yBkewC5MJAJ98V3 +YGJV954/KvwsAJCUamfaFG+XUt+yORYrAmvTZFMGY5K6WtTVsX/QKJGevwnsIMDp +X5jeD4WzRxtaCFjtJ3/HWtZnZwKBgQDGsRIqWMllZxQrynTko8zpg+QM9YCEOb5/ +qie/lsROhifg7ZWAYJmG7SL6uRy0oKFI4YqBz8BYdDTHzqY/1U4OImNWW+/UUDr7 +UaBSkPC1byDz+IizmG0vZX7zZk46knCncKf44gd0qAomyrE6LL6hb576/dURzy/k +pKmytSbKwwKBgQC4IRSOUPX7/gnqtXWgNMGoc7lllG+XdbJpwoE+Qivm5jIcRCeG +JtoF3p7ptA860sshOb3uQqfDhXjOTeCPXF7ouW3jU3ctsQPyu3vs3xDanba5RP0R +mjZSehwn/qfkawrpzxymKqmXQ1wHj6rgzAU/1LcvpB1LFgYkh2sbuQIPJwKBgCBX +uolCIqZq/RGTxytgrn5khb7GR8E+VRAa9pVtSU8u71bh1bAsCVG5UDRX5aBRdW+T +pyQyWTEM2Xqc3NsPMcGDP4BTPtrkpHU8eEh4Z3ZhPI/6KOZzLXLFpsCgKqPGKqhW +4kDVKjmHEP/3hpndprpInSxmHUTk4PrrAuSgMExZAoGAdgYRXJlyUWClR4D5oO21 +EUqf+Tb053RG36AIkSMkuG5KlKV5h3qhzvum35iKo652OkuMAtclO0aChWswlYX8 +83WDC+aXVAqpw9e6D/57scbS+Lbfn1A5FT2Yu/dw9N6hznaat4Zy9lwzcUeIQObr +yALnk0+P32aj/mX6j+ipJww= +-----END PRIVATE KEY----- diff --git a/test/pyhttpd/ech/scripts/echconfig.pem b/test/pyhttpd/ech/scripts/echconfig.pem new file mode 100644 index 00000000000..62f1c384f6b --- /dev/null +++ b/test/pyhttpd/ech/scripts/echconfig.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIIB1ZzFMfDCDMqCO81GMJiP8S1GZWcxMKFaXqj2p1hBo +-----END PRIVATE KEY----- +-----BEGIN ECHCONFIG----- +ADz+DQA4NgAgACDTWY8TPIwY6CoFq0y2/WOAysmn22ifrFDYRVOExVYeBQAEAAEAAQAJbG9jYWxob3N0AAA= +-----END ECHCONFIG----- diff --git a/test/pyhttpd/ech/scripts/setup_test_env.sh b/test/pyhttpd/ech/scripts/setup_test_env.sh new file mode 100755 index 00000000000..c4aa8863300 --- /dev/null +++ b/test/pyhttpd/ech/scripts/setup_test_env.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# setup_test_env.sh - Automated Setup for ECH Testing + +set -e + +PROJECT_ROOT=$(pwd) +CONF_DIR="$PROJECT_ROOT/conf" +SSL_DIR="$CONF_DIR/ssl" +ECH_DIR="$CONF_DIR/ech" + +# FIX: Added 'then' +if [ -z "$OPENSSL_ECH_PATH" ]; then + echo "ERROR: OPENSSL_ECH_PATH is not set." + echo "Please set it to your OpenSSL build directory (e.g., export OPENSSL_ECH_PATH=/path/to/openssl)" + exit 1 +fi + +OPENSSL_BIN="$OPENSSL_ECH_PATH/bin/openssl" +export LD_LIBRARY_PATH="$OPENSSL_ECH_PATH/lib64:$LD_LIBRARY_PATH" + +echo "[1/5] Cleaning old environment" +rm -rf "$CONF_DIR" +mkdir -p "$SSL_DIR" "$ECH_DIR" + +echo "[2/5] Generating Local Root CA" +$OPENSSL_BIN genrsa -out "$SSL_DIR/MyLocalCA.key" 4096 +$OPENSSL_BIN req -x509 -new -nodes -key "$SSL_DIR/MyLocalCA.key" -sha256 -days 365 \ + -out "$SSL_DIR/MyLocalCA.pem" \ + -subj "/C=IE/ST=Dublin/L=Dublin/O=FYP_Research/CN=MyLocalECH_CA" + +echo "[3/5] Generating & Signing Server Certificate" +$OPENSSL_BIN genrsa -out "$SSL_DIR/server.key" 2048 + +cat < "$SSL_DIR/domains.ext" +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost +EOF + +$OPENSSL_BIN req -new -key "$SSL_DIR/server.key" -out "$SSL_DIR/server.csr" -subj "/CN=localhost" +$OPENSSL_BIN x509 -req -in "$SSL_DIR/server.csr" -CA "$SSL_DIR/MyLocalCA.pem" \ + -CAkey "$SSL_DIR/MyLocalCA.key" -CAcreateserial -out "$SSL_DIR/server.crt" \ + -days 365 -sha256 -extfile "$SSL_DIR/domains.ext" + +echo "[4/5] Generating ECH Key Pair" +$OPENSSL_BIN ech -public_name "localhost" + +# FIX: Added 'then' +if [ -f "echconfig.pem" ]; then + mv echconfig.pem "$ECH_DIR/ECH_key.pem" + echo "Successfully moved ECH key to $ECH_DIR/ECH_key.pem" +else + echo "ERROR: OpenSSL did not create echconfig.pem" + exit 1 +fi + +echo "[5/5] Checking for geckodriver" +GECKO_VERSION="v0.34.0" +if [ ! -f "./geckodriver" ]; then + echo "Downloading Geckodriver $GECKO_VERSION..." + wget --no-check-certificate https://github.com/mozilla/geckodriver/releases/download/$GECKO_VERSION/geckodriver-$GECKO_VERSION-linux64.tar.gz + tar -xzf geckodriver-$GECKO_VERSION-linux64.tar.gz + rm geckodriver-$GECKO_VERSION-linux64.tar.gz + chmod +x geckodriver + echo "Geckodriver installed locally." +else + echo "Geckodriver already present." +fi + +echo "Environment ready for pytest." +echo "Config located in: $CONF_DIR" \ No newline at end of file diff --git a/test/pyhttpd/ech/scripts/test_negative_configs.sh b/test/pyhttpd/ech/scripts/test_negative_configs.sh new file mode 100755 index 00000000000..ed768431232 --- /dev/null +++ b/test/pyhttpd/ech/scripts/test_negative_configs.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# test_negative_configs.sh - Automated Server Failure Testing + +echo "--- [1/3] Testing: Missing ECH Key File ---" +mv ./conf/ech/ECH_key.pem ./conf/ech/ECH_key.pem.bak +docker-compose restart ech-server +sleep 2 + +if [ "$(docker inspect -f '{{.State.Running}}' ech-server)" == "false" ]; then + echo "✅ SUCCESS: Server failed to start without key file (Expected)." + docker logs ech-server 2>&1 | grep -i "error" | tail -n 2 +else + echo "❌ FAIL: Server started even though ECH key was missing!" +fi + +echo "--- [2/3] Testing: Corrupted ECH Configuration ---" +mv ./conf/ech/ECH_key.pem.bak ./conf/ech/ECH_key.pem +# Inject garbage into the ECH config directory +echo "NOT_A_KEY" > ./conf/ech/invalid.pem +docker-compose restart ech-server +sleep 2 + +if docker logs ech-server 2>&1 | grep -iE "invalid|failed|error"; then + echo "✅ SUCCESS: Server logged error for invalid ECH configuration." +fi + +echo "--- [3/3] Testing: Global vs VirtualHost Scope ---" +echo "Restoring environment..." +docker-compose restart ech-server \ No newline at end of file diff --git a/test/pyhttpd/ech/scripts/verify_ech.sh b/test/pyhttpd/ech/scripts/verify_ech.sh new file mode 100755 index 00000000000..afd594350ee --- /dev/null +++ b/test/pyhttpd/ech/scripts/verify_ech.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +INTERFACE="lo" +TARGET_SNI="localhost" +OUTPUT_FILE="ech_capture.pcap" +DURATION=15 + +echo "--- ECH Security Auditor Starting ---" +echo "[INFO] Listening on $INTERFACE for $DURATION seconds..." +echo "[INFO] Searching for cleartext leaks of: $TARGET_SNI" + +tshark -i $INTERFACE -a duration:$DURATION \ + -Y "tls.handshake.extensions_server_name == \"$TARGET_SNI\"" \ + -w $OUTPUT_FILE > capture_log.txt 2>&1 + +MATCH_COUNT=$(tshark -r $OUTPUT_FILE 2>/dev/null | wc -l) + +# Added 'then' below +if [ "$MATCH_COUNT" -gt 0 ]; then + echo "🚨 SNI LEAK DETECTED" + exit 1 +else + echo "✅ SUCCESS: Handshake Encrypted." + echo " No cleartext SNI found on the wire." + exit 0 +fi diff --git a/test/pyhttpd/ech/test_ech.py b/test/pyhttpd/ech/test_ech.py new file mode 100755 index 00000000000..9759fdecd92 --- /dev/null +++ b/test/pyhttpd/ech/test_ech.py @@ -0,0 +1,69 @@ +import os +import time +import pytest +import re +from selenium import webdriver +from selenium.webdriver.firefox.options import Options + +class TestEch: + + @pytest.fixture(autouse=True) + def setup_paths(self): + self.base_dir = os.path.dirname(os.path.abspath(__file__)) + self.ech_key_path = os.path.join(self.base_dir, "conf", "ech", "ECH_key.pem") + + if os.path.exists(self.ech_key_path): + with open(self.ech_key_path, 'r') as f: + content = f.read() + match = re.search(r"-----BEGIN ECHCONFIG-----(.*?)-----END ECHCONFIG-----", content, re.DOTALL) + if match: + self.ech_config = match.group(1).replace("\n", "").strip() + else: + pytest.fail("Found ECH_key.pem but no 'BEGIN ECHCONFIG' block inside!") + else: + pytest.fail(f"ECH Key not found. Run setup_test_env.sh first.") + + def run_browser_test(self, ech_config, use_grease=False): + options = Options() + options.add_argument("-headless") + + + options.set_preference("network.proxy.type", 0) + + options.set_preference("network.trr.mode", 0) + + # ECH enablement + options.set_preference("network.dns.echconfig.enabled", True) + options.set_preference("network.dns.local_echconfig", ech_config) + + # Trust and local environment settings + options.set_preference("security.enterprise_roots.enabled", True) + options.set_preference("network.http.ocsp.enabled", False) + + if use_grease: + options.set_preference("network.tls.grease.enabled", True) + + driver = webdriver.Firefox(options=options) + + driver.set_page_load_timeout(20) + + try: + driver.get("https://ech-test.fyp.local") + time.sleep(2) + return driver.page_source + finally: + driver.quit() + + def test_ech_handshake_success(self): + """Test 1: Standard ECH Handshake.""" + print("\n[RUN] Testing Standard ECH Handshake...") + page_source = self.run_browser_test(self.ech_config) + assert "ECH Decryption Successful" in page_source + print("✅ SUCCESS: ECH Decrypted and inner content served.") + + def test_ech_with_grease(self): + """Test 2: GREASE Interoperability.""" + print("\n[RUN] Testing ECH with GREASE enabled...") + page_source = self.run_browser_test(self.ech_config, use_grease=True) + assert "ECH Decryption Successful" in page_source + print("✅ SUCCESS: GREASE handled correctly.") diff --git a/test/pyhttpd/ech/test_ech_negative.py b/test/pyhttpd/ech/test_ech_negative.py new file mode 100644 index 00000000000..9d0071397b1 --- /dev/null +++ b/test/pyhttpd/ech/test_ech_negative.py @@ -0,0 +1,58 @@ +import os +import pytest +import tempfile +import shutil +import time +from selenium import webdriver +from selenium.webdriver.firefox.options import Options + +class TestEchNegative: + + def run_negative_browser_test(self, ech_config=None, use_ech=False): + options = Options() + options.add_argument("-headless") + + # Isolated Profile + self.temp_dir = tempfile.mkdtemp() + options.add_argument("-profile") + options.add_argument(self.temp_dir) + + # Kill Client-Side Resumption + options.set_preference("network.session-resumption.enabled", False) + options.set_preference("security.tls.enable_0rtt_data", False) + + # DNS/Network Stability + options.set_preference("network.trr.mode", 0) + options.set_preference("network.proxy.type", 0) + options.set_preference("network.dns.localDomains", "ech-test.fyp.local") + + # Set ECH Prefs safely + options.set_preference("network.dns.echconfig.enabled", use_ech) + if use_ech and ech_config: + options.set_preference("network.dns.local_echconfig", ech_config) + + driver = webdriver.Firefox(options=options) + try: + # Timestamp busts any remaining caches + driver.get(f"https://ech-test.fyp.local/?t={time.time()}") + return driver.page_source + finally: + driver.quit() + shutil.rmtree(self.temp_dir) + + def test_ech_disabled_fallback(self): + print("\n[AUDIT] Testing Handshake without ECH Extension...") + page_source = self.run_negative_browser_test(use_ech=False) + + assert "Public Gateway" in page_source + assert "ECH Decrypted Successfully" not in page_source + print("✅ PASSED: Identity Hidden. Server defaulted to Public Gateway.") + + def test_ech_invalid_key_fallback(self): + print("\n[AUDIT] Testing Handshake with Poisoned ECH Key...") + poisoned_key = "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA==" + page_source = self.run_negative_browser_test(ech_config=poisoned_key, use_ech=True) + + assert "Public Gateway" in page_source + assert "ECH Decrypted Successfully" not in page_source + print("✅ PASSED: Decryption failed. Server protected the Inner Name.")