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..a0efa8cd5c8 --- /dev/null +++ b/test/pyhttpd/ech/README.md @@ -0,0 +1,24 @@ +### Step 1: Local DNS Configuration +Map the test domain to your local loopback interface: +```bash +echo "127.0.0.1 ech-test.fyp.local" | sudo tee -a /etc/hosts + +### Step 2: Environment Initialization +Set the path to your ECH-enabled OpenSSL build and run the setup script: +export OPENSSL_ECH_PATH=/path/to/your/openssl + +### for me right now +export OPENSSL_ECH_PATH=/home/yag/final_year/build/openssl +export LD_LIBRARY_PATH=$OPENSSL_ECH_PATH/lib64:$LD_LIBRARY_PATH + + +./setup_test_env.sh + +### Step 3: Start the Server +docker-compose up -d --build + +### Step 4: execute test +pytest -s test_ech.py + + +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. 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/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/setup_test_env.sh b/test/pyhttpd/ech/setup_test_env.sh new file mode 100755 index 00000000000..c4aa8863300 --- /dev/null +++ b/test/pyhttpd/ech/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/test_ech.py b/test/pyhttpd/ech/test_ech.py new file mode 100755 index 00000000000..a78396be2ae --- /dev/null +++ b/test/pyhttpd/ech/test_ech.py @@ -0,0 +1,73 @@ +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): + """Pre-test setup: Locates keys and extracts the ECHConfig string.""" + 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) + + # Set a shorter timeout so you don't wait 3 minutes for a failure + 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) + # CHANGED: Match the actual output from your terminal error + 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) + # CHANGED: Match the actual output + assert "ECH Decryption Successful" in page_source + print("✅ SUCCESS: GREASE handled correctly.") \ No newline at end of file diff --git a/test/pyhttpd/ech/test_ech_negative.py b/test/pyhttpd/ech/test_ech_negative.py new file mode 100644 index 00000000000..391b92fca06 --- /dev/null +++ b/test/pyhttpd/ech/test_ech_negative.py @@ -0,0 +1,61 @@ +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): + """Negative Test: Verifies that without ECH, the 'Inner' site is invisible.""" + print("\n[AUDIT] Testing Handshake without ECH Extension...") + page_source = self.run_negative_browser_test(use_ech=False) + + # We expect the PUBLIC gateway, NOT the private content + 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): + """Negative Test: Verifies that a corrupted ECH key triggers fallback.""" + 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.") \ No newline at end of file diff --git a/test/pyhttpd/ech/test_ech_performance.py b/test/pyhttpd/ech/test_ech_performance.py new file mode 100644 index 00000000000..548b7331949 --- /dev/null +++ b/test/pyhttpd/ech/test_ech_performance.py @@ -0,0 +1,82 @@ +import time +import statistics +import os +import subprocess +import re + +# Reuse your existing config and runner +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/test_negative_configs.sh b/test/pyhttpd/ech/test_negative_configs.sh new file mode 100755 index 00000000000..ed768431232 --- /dev/null +++ b/test/pyhttpd/ech/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/verify_ech.sh b/test/pyhttpd/ech/verify_ech.sh new file mode 100755 index 00000000000..8fe1551c9af --- /dev/null +++ b/test/pyhttpd/ech/verify_ech.sh @@ -0,0 +1,25 @@ +#!/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) + +if [ "$MATCH_COUNT" -gt 0 ]; + echo " SNI LEAK DETECTED" + exit 1 +else + echo " SUCCESS: Handshake Encrypted." + echo " No cleartext SNI found on the wire." + exit 0 +fi \ No newline at end of file