Skip to content

Commit 6d7ca3b

Browse files
kataokaakataokaa
authored andcommitted
added all changes to tests for final submission of fyp, keeping things up to date
1 parent 33ae90a commit 6d7ca3b

22 files changed

Lines changed: 465 additions & 31 deletions

test/pyhttpd/ech/README.md

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,98 @@
1-
### Step 1: Local DNS Configuration
2-
Map the test domain to your local loopback interface:
3-
```bash
4-
echo "127.0.0.1 ech-test.fyp.local" | sudo tee -a /etc/hosts
1+
# ECH Apache Implementation & Verification Suite
2+
3+
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.
4+
5+
---
6+
7+
## 1. Build & Compilation Guide
8+
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.
9+
10+
### A. Build ECH-Enabled OpenSSL
11+
We utilize a specific fork of OpenSSL that includes HPKE and ECH protocol support.
12+
13+
# Clone and enter the experimental repository
14+
git clone (https://github.com/sftcd/openssl.git) openssl-ech
15+
cd openssl-ech
16+
17+
# Configure for a local prefix to avoid system conflicts
18+
./config --prefix=$HOME/openssl-ech --openssldir=$HOME/openssl-ech -Wl,-rpath,'$(LIBRPATH)'
19+
20+
# Compile and install
21+
make -j$(nproc)
22+
make install
23+
24+
25+
B. Build ECH-Enabled Apache (httpd)
26+
Apache must be linked against the custom OpenSSL build created above.
27+
git clone (https://github.com/apache/httpd.git) httpd-ech
28+
cd httpd-ech
29+
30+
# Configure with custom SSL path and static dependency hooks
31+
./buildconf
32+
./configure --prefix=$HOME/apache-ech \
33+
--enable-ssl \
34+
--enable-so \
35+
--with-ssl=$HOME/openssl-ech \
36+
--enable-mods-shared=all \
37+
--enable-ssl-staticlib-deps
38+
39+
make -j$(nproc)
40+
make install
541

6-
### Step 2: Environment Initialization
7-
Set the path to your ECH-enabled OpenSSL build and run the setup script:
8-
export OPENSSL_ECH_PATH=/path/to/your/openssl
42+
2. Verification Suite Setup
43+
A. Shell Environment
44+
Set these variables in your active terminal to point the verification tools to your custom binaries.
945

10-
### for me right now
11-
export OPENSSL_ECH_PATH=/home/yag/final_year/build/openssl
46+
export OPENSSL_ECH_PATH=$HOME/openssl-ech
47+
export OPENSSL_CONF=/etc/ssl/openssl.cnf
48+
export PATH=$OPENSSL_ECH_PATH/bin:$PATH
1249
export LD_LIBRARY_PATH=$OPENSSL_ECH_PATH/lib64:$LD_LIBRARY_PATH
1350

51+
B. Network Configuration
52+
echo "127.0.0.1 ech-test.fyp.local" | sudo tee -a /etc/hosts
53+
54+
C. Python Dependencies
55+
pip install -r requirements.txt
1456

57+
D. Cryptographic Initialization
58+
Generate the PKI hierarchy (Root CA, Server Certs) and the ECH key material.
59+
cd scripts/
1560
./setup_test_env.sh
61+
cd ..
1662

17-
### Step 3: Start the Server
63+
3. Infrastructure Orchestration
64+
Deploy the containerized server. We use a volume-wipe strategy to ensure configuration idempotency.
65+
cd infrastructure/
66+
docker-compose down -v
1867
docker-compose up -d --build
1968

20-
### Step 4: execute test
21-
pytest -s test_ech.py
69+
# Initialize a clean configuration backup for robustness testing
70+
docker exec ech-server cp /usr/local/apache2/conf/httpd.conf /usr/local/apache2/conf/httpd.conf.bak
71+
cd ..
72+
73+
Running the Full Suite
74+
pytest -v cases/
75+
76+
Security Audit (Wire-Level)
77+
To verify that no SNI information leaks in cleartext, run the Tshark-based auditor:
78+
sudo ./scripts/verify_ech.sh
79+
80+
Project Structure
81+
/cases: Modular pytest logic.
82+
83+
/infrastructure: Dockerfiles and Apache httpd.conf templates.
84+
85+
/lib: Environment abstractions and Selenium/WebDriver drivers.
86+
87+
/scripts: Setup and wire-level verification tools.
88+
89+
/conf: Storage for generated .pem keys and ECHConfigs.
90+
91+
Success Criteria
92+
Verification is successful if:
93+
94+
test_01 and test_02 return PASSED (Protocol and Browser Success).
2295

96+
test_04 identifies a Syntax Error (Robustness Success).
2397

24-
Test will succeed if firefox parses the provided ECHConfig, Apache uses the SSLECHKeyDir to decrypt ClientHello, and then routes the decrypted request to the ech-test.fyp.local VirtualHost, avoiding falling back to the public default.
98+
verify_ech.sh detects zero occurrences of the string ech-test.fyp.local in the cleartext portion of the TLS ClientHello.

test/pyhttpd/ech/cases/__init__.py

Whitespace-only changes.

test/pyhttpd/ech/cases/test_01.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import pytest
2+
import subprocess
3+
from lib.env import EchTestEnv
4+
5+
class TestEchProtocol:
6+
7+
@pytest.fixture(autouse=True)
8+
def setup_method(self):
9+
self.env = EchTestEnv()
10+
self.config = self.env.get_ech_config()
11+
12+
# --- SECTION 1: FUNCTIONAL & REGRESSION ---
13+
14+
def test_01_standard_tls_regression(self):
15+
"""Verify standard TLS 1.3 works without ECH."""
16+
output = self.env.run_openssl(["-brief"])
17+
assert "Verification: OK" in output or "return code: 0" in output
18+
19+
def test_02_protocol_correctness(self):
20+
"""Verify the server actually accepts a valid ECH extension."""
21+
output = self.env.run_openssl(["-brief", "-ech_config_list", self.config])
22+
success_markers = ["ECH: accepted", "02 79", "ech required"]
23+
assert any(marker in output for marker in success_markers)
24+
25+
def test_03_protocol_hrr_handling(self):
26+
"""Verify ECH survives a HelloRetryRequest (HRR)."""
27+
output = self.env.run_openssl(["-ech_config_list", self.config, "-groups", "P-521", "-msg"])
28+
has_hrr = any(x in output for x in ["HelloRetryRequest", "HRR", "hello_retry_request", "02 00 00"])
29+
assert has_hrr
30+
assert "ECH: accepted" in output or "02 79" in output
31+
32+
# --- SECTION 2: NEGATIVE TESTS (Security Audit) ---
33+
34+
def test_04_negative_no_ech_access(self):
35+
"""Verify standard clients are not granted ECH status."""
36+
output = self.env.run_openssl(["-brief"])
37+
assert "ECH: accepted" not in output
38+
39+
def test_05_negative_mismatched_public_name(self):
40+
"""Verify rejection when Outer SNI is incorrect."""
41+
output = self.env.run_openssl(["-brief", "-servername", "wrong-gateway.com", "-ech_config_list", self.config])
42+
assert "ECH: accepted" not in output
43+
assert "ech_retry_configs" in output.lower() or "ech required" in output.lower()
44+
45+
def test_06_negative_corrupted_config(self):
46+
"""Verify rejection when the ECH key is invalid/poisoned."""
47+
wrong_key = "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA=="
48+
output = self.env.run_openssl(["-brief", "-ech_config_list", wrong_key])
49+
assert "ECH: accepted" not in output
50+
assert "ech required" in output.lower() or "0A0001A8" in output
51+
52+
def test_07_negative_tls_downgrade(self):
53+
"""Verify ECH is ignored if client attempts to use TLS 1.2."""
54+
output = self.env.run_openssl(["-brief", "-tls1_2", "-ech_config_list", self.config])
55+
assert "ECH: accepted" not in output
56+
57+
# --- SECTION 3: INTEROPERABILITY & LOGGING ---
58+
59+
def test_08_interoperability_grease(self):
60+
"""Verify server handles GREASE extensions gracefully."""
61+
output = self.env.run_openssl(["-brief", "-ech_grease"])
62+
assert "Verification: OK" in output
63+
assert "ECH: accepted" not in output
64+
65+
def test_09_ech_environment_variables(self):
66+
"""Verify handshake triggers server-side logging of the ECH event."""
67+
self.env.run_openssl(["-brief", "-ech_config_list", self.config])
68+
logs = subprocess.check_output(self.env.docker_bin + ["tail", "-n", "20", "/usr/local/apache2/logs/error_log"]).decode()
69+
assert any(x in logs for x in ["SSL handshake", "ECH", "ssl_engine"])

test/pyhttpd/ech/cases/test_02.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
from lib.env import EchTestEnv
3+
4+
class TestEchBrowser:
5+
6+
@pytest.fixture(autouse=True)
7+
def setup_method(self):
8+
self.env = EchTestEnv()
9+
self.ech_config = self.env.get_ech_config()
10+
if not self.ech_config:
11+
pytest.fail("ECH Config not found in infrastructure/conf/ech/")
12+
13+
def test_01_browser_handshake_success(self):
14+
"""Test: Standard ECH Handshake via Firefox."""
15+
page_source = self.env.run_browser(self.ech_config)
16+
assert "ECH Decryption Successful" in page_source
17+
18+
def test_02_browser_with_grease(self):
19+
"""Test: ECH with GREASE enabled via Firefox."""
20+
page_source = self.env.run_browser(self.ech_config, use_grease=True)
21+
assert "ECH Decryption Successful" in page_source

test/pyhttpd/ech/test_ech_performance.py renamed to test/pyhttpd/ech/cases/test_03_performance.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import subprocess
55
import re
66

7-
# Reuse your existing config and runner
87
def get_latest_ech_config():
98
path = "./conf/ech/ECH_key.pem"
109
if not os.path.exists(path):
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import subprocess
2+
import os
3+
import time
4+
import pytest
5+
from lib.env import EchTestEnv
6+
7+
class TestServerRobustness:
8+
9+
@pytest.fixture(autouse=True)
10+
def setup_method(self):
11+
self.env = EchTestEnv()
12+
self.config_path = "infrastructure/conf/ech/ECH_key.pem"
13+
14+
def test_01_missing_key_file(self):
15+
"""Requirement: Verify server handles missing ECH key file."""
16+
os.rename(self.config_path, self.config_path + ".bak")
17+
18+
try:
19+
subprocess.run(["docker-compose", "-f", "infrastructure/docker-compose.yml", "restart", "ech-server"])
20+
time.sleep(2)
21+
22+
status = subprocess.check_output(["docker", "inspect", "-f", "{{.State.Running}}", "ech-server"]).decode().strip()
23+
24+
assert status == "false", "Server should not be running without its ECH key file"
25+
26+
finally:
27+
if os.path.exists(self.config_path + ".bak"):
28+
os.rename(self.config_path + ".bak", self.config_path)
29+
subprocess.run(["docker-compose", "-f", "infrastructure/docker-compose.yml", "restart", "ech-server"])
30+
31+
def test_02_invalid_directive_syntax(self):
32+
subprocess.run(["docker", "exec", "ech-server", "cp", "/usr/local/apache2/conf/httpd.conf.bak", "/usr/local/apache2/conf/httpd.conf"])
33+
34+
subprocess.run(["docker", "exec", "ech-server", "sed", "-i", "s/SSLECHKeyDir/INVALID_COMMAND/", "/usr/local/apache2/conf/httpd.conf"])
35+
36+
subprocess.run(["docker", "restart", "ech-server"])
37+
time.sleep(2)
38+
39+
result = subprocess.run(["docker", "logs", "ech-server"], capture_output=True, text=True)
40+
logs = result.stdout + result.stderr
41+
42+
assert "Syntax error" in logs or "Invalid command" in logs

test/pyhttpd/ech/lib/__init__.py

Whitespace-only changes.

test/pyhttpd/ech/lib/env.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import subprocess
2+
import os
3+
import re
4+
import time
5+
from selenium import webdriver
6+
from selenium.webdriver.firefox.options import Options
7+
8+
class EchTestEnv:
9+
def __init__(self):
10+
self.connection_target = "localhost:443"
11+
12+
self.public_name = "localhost"
13+
14+
self.private_host = "ech-test.fyp.local"
15+
16+
self.url = f"https://{self.private_host}"
17+
18+
self.docker_bin = ["docker", "exec", "ech-server"]
19+
self.openssl_bin = "/opt/openssl-ech/bin/openssl"
20+
self.ca_path = "/usr/local/apache2/conf/ssl/MyLocalCA.pem"
21+
22+
def get_ech_config(self):
23+
path = "infrastructure/conf/ech/ECH_key.pem"
24+
if not os.path.exists(path):
25+
return "AEH+DQA9tAAgACDG1DRKJzL4jbKU//fdPlSFfASYZgMrpthbvcsc+GbtKQAEAAEAAQAOeW91cmRvbWFpbi5jb20AAA=="
26+
with open(path, 'r') as f:
27+
content = f.read()
28+
match = re.search(r"-----BEGIN ECHCONFIG-----(.*?)-----END ECHCONFIG-----", content, re.DOTALL)
29+
return match.group(1).replace("\n", "").strip() if match else ""
30+
31+
def run_openssl(self, args):
32+
shell_cmd = (
33+
f"LD_LIBRARY_PATH=/opt/openssl-ech/lib64 {self.openssl_bin} s_client "
34+
f"-connect {self.connection_target} -servername {self.public_name} "
35+
f"-CAfile {self.ca_path} -no_ticket " + " ".join(args)
36+
)
37+
cmd = self.docker_bin + ["sh", "-c", shell_cmd]
38+
result = subprocess.run(cmd, input="Q\n", capture_output=True, text=True, timeout=10)
39+
return result.stdout + result.stderr
40+
41+
def run_browser(self, ech_config, use_grease=False):
42+
"""Apache-style Selenium wrapper."""
43+
options = Options()
44+
options.add_argument("-headless")
45+
options.set_preference("network.proxy.type", 0)
46+
options.set_preference("network.trr.mode", 0)
47+
48+
# ECH enablement
49+
options.set_preference("network.dns.echconfig.enabled", True)
50+
options.set_preference("network.dns.local_echconfig", ech_config)
51+
52+
# Trust and local environment settings
53+
options.set_preference("security.enterprise_roots.enabled", True)
54+
options.set_preference("network.http.ocsp.enabled", False)
55+
56+
if use_grease:
57+
options.set_preference("network.tls.grease.enabled", True)
58+
59+
driver = webdriver.Firefox(options=options)
60+
driver.set_page_load_timeout(20)
61+
62+
try:
63+
driver.get(self.url)
64+
time.sleep(2)
65+
return driver.page_source
66+
finally:
67+
driver.quit()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NOT_A_KEY
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
51+
uosCV96fm/Mvv5Vw/Ojs5krHq68VfHE=
52+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)