commit a71491c068b2d7a61bdf409c3ea569c8d4762da3 Author: Pascal Engélibert Date: Wed Nov 5 14:28:26 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2079428 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +netreplay +rpxy_* diff --git a/README.md b/README.md new file mode 100644 index 0000000..ccc6a86 --- /dev/null +++ b/README.md @@ -0,0 +1,270 @@ +# TLS power measure benchmark + +Goal: measure the power overhead of adding TLS to a client or a server, on realistic loads. + +Realistic load implies using a real-world client (such as a web browser on a low-end device) and server (such as a video streaming platform on a typical server). + +Problem: realistic clients have complex behaviors that go beyond a simple OpenSSL example code, and modern browsers and website won't work without HTTPS. + +Assumption: most of the load added by the security layers is decoupled from the application (in both web browsers and web servers). + +``` +Experiments: +1: (Client) <=[TLS]=> (Proxy 1) <-[plain]-> (Proxy 2) <-[plain]-> (Proxy 3) <=[TLS]=> (Server) +2: (Client) <=[TLS]=> (Proxy 1) <==[TLS]==> (Proxy 2) <-[plain]-> (Proxy 3) <=[TLS]=> (Server) +3: (Client) <=[TLS]=> (Proxy 1) <-[plain]-> (Proxy 2) <==[TLS]==> (Proxy 3) <=[TLS]=> (Server) + ^measure^ +``` + +Client and server are identical in all experiments. The only modification made to the client is to add a trusted certificate owned by Proxy 1. + +Only Proxy 2 must be a dedicated machine. The other parties may be placed on the same machine. + +Call E1, E2, E3 the energy measured for Proxy 2 in experiments 1, 2, 3. + +E2-E1 is the energy used to operate the server's half of a TLS connection. + +E3-E1 is the energy used to operate the client's half of a TLS connection. + +Client: +* Web browser trusting a certificate owned by Proxy 1 + +## Things to experiment + +* Implementations + * OpenSSL 1.0.2 + * OpenSSL 1.1.1 + * OpenSSL 3.0 + * OpenSSL 3.2 + * OpenSSL 3.3 + * OpenSSL 3.4 + * OpenSSL 3.5 + * OpenSSL 3.6 + * [WolfSSL](https://github.com/wolfSSL/wolfssl) + * GnuTLS + * [NSS](https://github.com/nss-dev/nss) (used by Firefox) (?) + * opencryptoki (?) + * AWS-LC + * [BoringSSL](https://github.com/google/boringssl) (Chrome, Android) + * LibreSSL + * [SymCrypt](https://github.com/microsoft/SymCrypt) (used by Windows) ([install manually](https://github.com/microsoft/SymCrypt/releases)) +* Versions + * TLS 1.2 + * TLS 1.3 +* Features + * TLS + * Ciphers + * TLS_AES_128_GCM_SHA256 + * TLS_AES_256_GCM_SHA384 + * TLS_CHACHA20_POLY1305_SHA256 + * TLS_AES_128_CCM_SHA256 + * TLS_AES_128_CCM_8_SHA256 + * Key exchange groups + * secp256r1 + * secp384r1 + * secp521r1 + * x25519 + * x448 + * ffdhe2048 + * ffdhe3072 + * ffdhe4096 + * ffdhe6144 + * ffdhe8192 + * Signatures + * rsa_pkcs1_sha256 + * rsa_pkcs1_sha384 + * rsa_pkcs1_sha512 + * ecdsa_secp256r1_sha256 + * ecdsa_secp384r1_sha384 + * ecdsa_secp521r1_sha512 + * rsa_pss_rsae_sha256 + * rsa_pss_rsae_sha384 + * rsa_pss_rsae_sha512 + * ed25519 + * ed448 + * rsa_pss_pss_sha256 + * rsa_pss_pss_sha384 + * rsa_pss_pss_sha512 + * rsa_pkcs1_sha1 + * ecdsa_sha1 + * X.509 + * Signature algorithm + * RSA2048 + * RSA3072 + * RSA4096 + * EC 256 + * EC 384 + * SCT + * TODO !!! +* Usages + * Video streaming + * Full-speed download + * Real-time video call + * Mostly text browsing + * With or without ads + +Most of the implementations can be used through RusTLS. + +However RusTLS clients won't enable to force TLS1.2 if 1.3 is available. + +### rpxy + +Reverse-proxy utilisant RusTLS. + +### WolfSSL + +```bash +git clone https://github.com/wolfSSL/wolfssl --depth 1 +cd wolfssl +sh autogen.sh +./configure --enable-all --enable-all-crypto --disable-shared --prefix=/opt/wolfssl-rs/ +make +sudo make install +``` + +### self-signed cert + +```bash +openssl req -x509 -newkey rsa:2048 -keyout /tmp/foo.home.key -subj "/CN=foo.home/C=AT/ST=Lyon/L=Lyon/O=MyOrg" -out /tmp/foo.home.crt -nodes -sha256 -addext "subjectAltName=DNS:foo.home" +``` + +### Client + +Automatize experiments using [Selenium](https://www.selenium.dev/documentation/webdriver/getting_started/) + +#### Experiment management + +* Manager tells P2 what shared libs and rpxy binary to load. +* Tell P1, P2, P3 what rpxy config to load. +* Start measures. + * Start Yocto (USB). + * Start + +#### Ad-hoc proxy? + +Features: +* Use RusTLS and any backend easily +* Listen to plain HTTP or TLS (1.2 or 1.3) +* + +## State of the art + +* https://pub.h-brs.de/frontdoor/deliver/index/docId/4771/file/2019-ESP32-TLS-Power.pdf + * 2019 + * TLS 1.2, 1.3 + * ESP32 + * WolfSSL +* https://www.semanticscholar.org/paper/Energy-Consumption-Framework-and-Analysis-of-on-Patterson-Buchanan/706736a29cef777e5dc50ba22b4788b2bfb4c6ef + * 2025 + * RaspberryPi + * OpenSSL + * ML-KEM +* https://www.semanticscholar.org/paper/Energy-Profiling-and-Comparison-of-TLS-Protocols-Gatram-Reddy/9c061fe57a0008574b85919bc70fc803c6e66f06 + * 2024 + * RaspberryPi + * TLS PQ, TLS KEM, TLS +* https://www.semanticscholar.org/paper/Energy-Consumption-Evaluation-of-Post-Quantum-TLS-Tasopoulos-Dimopoulos/2ffc6d13349e2fa5f89aaf18e69ce2044ecef4fe + * 2023 + * STM Nucleo + * WolfSSL + * TLS 1.3 PQ +* https://arxiv.org/pdf/2508.04583v2 + * 2025 + * TLS 1.3 + * Nginx + Python requests + * https://github.com/MarcT0K/privacy-carbon-experiments +* https://davidtnaylor.com/CostOfTheS.pdf + * 2014 + * + +## Sources + +* [RFC8446 (TLS 1.3)](https://datatracker.ietf.org/doc/html/rfc8446) + +## Notes + +Install sa on p2: + +```bash +sudo apt install acct +sudo chmod +s /sbin/sa +``` + +Override DNS in browser: + +```bash +firefox -P tlsbench +``` + +In `about:config`, set `devtools.chrome.enabled` to `true`. +Set default DNS in settings. +In console: + +```js +const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(Ci.nsINativeDNSResolverOverride); +gOverride.clearOverrides(); +var names = ["apple.com", "www.apple.com", "mzstatic.com", "youtube.com", "www.youtube.com", "i.ytimg.com", "fonts.gstatic.com", "www.google.com", "accounts.google.com", "yt3.ggpht.com", "www.gstatic.com"]; +for(var i in names) { + gOverride.addIPOverride(names[i], "127.0.0.1"); +} +``` + +Authorize rpxy to bind to ports 80 and 443: +```bash +sudo setcap CAP_NET_BIND_SERVICE=+eip rpxy_rustls_ring +``` + +Add CA certificate on ArchLinux: +```bash +sudo cp /dev/shm/exp/certs/rsa2048/ca.crt /etc/ca-certificates/trust-source/anchors/ca-rsa2048.crt +sudo cp /dev/shm/exp/certs/rsa3072/ca.crt /etc/ca-certificates/trust-source/anchors/ca-rsa3072.crt +sudo cp /dev/shm/exp/certs/rsa4096/ca.crt /etc/ca-certificates/trust-source/anchors/ca-rsa4096.crt +sudo cp /dev/shm/exp/certs/secp384r1/ca.crt /etc/ca-certificates/trust-source/anchors/ca-secp384r1.crt +sudo chown root:root /etc/ca-certificates/trust-source/anchors/ca-*.crt +sudo update-ca-trust extract +``` + +```bash +python exp.py make +python exp.py send-setups +python exp.py send-certs +python exp.py run +``` + +On Debian, update-certs says 0 certs added even if it has actually updated some certs. This step is still needed. + +## Problems + +### Youtube gives 502 bad gateway. + +* Works with bare curl. (not hiding we're a bot) +* We have same JA3 fingerprint as Firefox. +* HTTP request is intact. +* JA4 fingerprint different from Firefox but existing for some browsers. + +### Modèle d'expérience à revoir + +* Le relai d'une vidéo streaming avec TLS prend max 3% d'un cœur sur le Pi3, soit pas beaucoup plus que le bruit. +* Il faudrait spammer avec plusieurs connexions pour voir un effet significatif. +* On peut spammer le streaming vidéo, mais pas le reste (antibots). +* On ne devrait pas spammer sur Renater... +* Il faut donc tout faire en local. + +Solutions : + +* Copier les sites en statique et les servir avec Apache. => OK pour des sites propres genre Wikipedia, WordPress (à voir pour la pub) +* Installer des instances => Peertube, WordPress +* Copier le trafic et le rejouer => risque de demander beaucoup de dev + +### Youtube + +Youtube utilise des trucs aléatoires en `RANDOM.googlevideo.com` pour la vidéo. Cependant il y a quelques domaines utilisés qui ne changent pas, du moins sur un même navigateur avec la même vidéo et sur une courte période. +Avant d'enregistrer le trafic, il faut observer les domaines utilisés puis générer les certificats et les redirections en fonction. + +## TODO + +* Partie serveur sans TLS de netreplay -> sans SNI, il faut parser le HTTP >.< +* exp.py: détecter la fin du replay +* yoctowatt +* mesures CPU, mémoire, bande passante sur p2 + * CPU: paquet acct, commande `sa -m` diff --git a/exp.py b/exp.py new file mode 100644 index 0000000..7b5cd7a --- /dev/null +++ b/exp.py @@ -0,0 +1,604 @@ +#!/usr/bin/python3 +import os, sys, subprocess + +REPODIR = "/home/tuxmain/Documents/doc/tlsbench" +P2_SSH = "exp@p2" +P2_PSW = "exp" +P2_REPODIR = "/home/exp/exp" +EXPDIR = "/dev/shm/exp" +P2_ADDR = "192.168.3.14" +DOMAINS_ = [ + # Apple + "apple.com", "www.apple.com", "graffiti-tags.apple.com", "securemetrics.apple.com", + "store.storeimages.cdn-apple.com", + "mzstatic.com", "is1-ssl.mzstatic.com", + # Youtube video + "youtube.com", "www.youtube.com", + "i.ytimg.com", + "fonts.gstatic.com", "www.gstatic.com", + "www.google.com", "accounts.google.com", + "yt3.ggpht.com", + "rr1---sn-gxo5uxg-jqbl.googlevideo.com", + "rr2---sn-gxo5uxg-jqbl.googlevideo.com", + "rr4---sn-q4fl6nds.googlevideo.com", + # Amazon + "amazon.com", "www.amazon.com", + # Wikipedia article + "fr.wikipedia.org", "upload.wikimedia.org", + # Google search + "www.google.com", "www.googleadservices.com", "www.gstatic.com", "csp.withgoogle.com", "ogads-pa.clients6.google.com", "play.google.com", "ssl.gstatic.com", "fonts.gstatic.com", "ogs.google.com", + # Peertube video + "videos.domainepublic.net", +] +WATTMETER = True + +RECORDS = [ + { "filename": "youtube", "repeat": 10 }, + #{ "filename": "peertube", "repeat": 10 }, + #{ "filename": "wikipedia", "repeat": 10 }, + #{ "filename": "apple", "repeat": 10 }, + #{ "filename": "google", "repeat": 10 }, +] +CERT_SIGN_ALGS = [ + "prime256v1", # widely used + "secp384r1", # rarely used but supported by browsers because it's NIST standard + #"secp521r1", # not supported by browsers because NIST said it was not needed + "rsa2048", "rsa3072", "rsa4096", # widely used +] +IMPLS = [ + #"aws_lc_rs", # Amazon's Rust crypto widely used in Rust stuff + #"boring", # Google's fork of OpenSSL used in Chrome and Android + "openssl", # widely used + #"ring", # used in most Rust stuff + #"symcrypt", # Microsoft's crypto + #"wolfcrypt" # used in embedded (won't build with rpxy for now) +] +# Symmetric ciphers +# They also allow to choose the TLS version. +CIPHERS = [ + # TLS 1.3 + "AES_256_GCM_SHA384", + "AES_128_GCM_SHA256", + "CHACHA20_POLY1305_SHA256", + # TLS 1.2 + # ECDSA vs RSA refers to the certificate signature algorithm. + # DH is EC in either case, using the group specified below. + "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", +] +KEXES = [ + "X25519", + "SECP256R1", + "SECP384R1", +] + +# Testing all combinations would be too much. Instead we isolate independent parts. +EXPERIMENTS = { + # Compare ciphers among implementations and TLS versions + # "impl-cipher-ver": { + # "impls": IMPLS, + # "records": RECORDS, + # "ciphers": [ + # "AES_128_GCM_SHA256", + # "AES_256_GCM_SHA384", + # "CHACHA20_POLY1305_SHA256", + # "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384", + # "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + # ], + # "kexes": ["X25519"], + # "cert": ["prime256v1"], + # }, + # # Compare signatures among implementations and TLS versions + # "impl-cert-ver": { + # "impls": IMPLS, + # "records": RECORDS, + # "ciphers": [ + # "AES_128_GCM_SHA256", + # "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # ], + # "kexes": ["X25519"], + # "cert": [ + # "prime256v1", + # #"secp384r1", + # "rsa2048", + # #"rsa3072", "rsa4096" + # ], + # }, + # # Compare key exchange groups among implementations and TLS versions + # "impl-kex-ver": { + # "impls": IMPLS, + # "records": RECORDS, + # "ciphers": [ + # "AES_128_GCM_SHA256", + # "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + # ], + # "kexes": ["X25519", "SECP256R1", "SECP384R1"], + # "cert": ["prime256v1"], + # }, + "debug": { + "impls": IMPLS, + "records": RECORDS, + "ciphers": [ + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + ], + "kexes": ["SECP384R1"], + "cert": ["prime256v1"], + }, +} + +DOMAINS = [] +for domain in DOMAINS_: + if not domain in DOMAINS: + DOMAINS.append(domain) + +# JS to redirect the target domains to local (bypass DNS without altering system's config or webpages or packets) +SCRIPT_FIREFOX_HOSTS = """const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(Ci.nsINativeDNSResolverOverride); +gOverride.clearOverrides(); +var names = """+str(DOMAINS)+"""; +for(var i in names) { + gOverride.addIPOverride(names[i], "127.0.0.1"); +} +""" + +def getargv(arg:str, default="", n:int=1, args:list=sys.argv): + if arg in args and len(args) > args.index(arg)+n: + return args[args.index(arg)+n] + else: + return default + +def sh(cmds): + if type(cmds) == list: + for cmd in cmds: + print(cmd) + os.system(cmd) + elif type(cmds) == str: + print(cmds) + os.system(cmds) + else: + raise TypeError + +def make_sk(outpath, alg): + sh({ + "ed25519": [ + f"openssl genpkey -out {outpath}.sec1 -algorithm ed25519", + f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM" + ], + "prime256v1": [ + f"openssl ecparam -genkey -name prime256v1 -noout -out {outpath}.sec1", + f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM" + ], + "rsa2048": [ + f"openssl genrsa -out {outpath} 2048" + ], + "rsa3072": [ + f"openssl genrsa -out {outpath} 3072" + ], + "rsa4096": [ + f"openssl genrsa -out {outpath} 4096" + ], + "secp384r1": [ + f"openssl ecparam -genkey -name secp384r1 -noout -out {outpath}.sec1", + f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM" + ], + }[alg]) + +def make_ca_cert(outpath, skpath): + sh(f"openssl req -x509 -new -key {skpath} -sha256 -days 730 -out {outpath} -subj '/CN=Foobar Root CA/C=AT/ST=Lyon/L=Lyon/O=Foobar'") + +def make_cert(outpath, skpath, capath, caskpath, name, domains): + sh(f"openssl req -new -key {skpath} -sha256 -out /tmp/tmp.csr -subj '/CN={name}/C=AT/ST=Lyon/L=Lyon/O={name}'") + ext = open("/tmp/tmp.v3.ext", "w") + ext.write("""authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +""") + i = 1 + for domain in domains: + ext.write(f"DNS.{i} = {domain}\n") + i += 1 + ext.write(f"DNS.{i} = {domain}.localhost\n") + i += 1 + ext.write(f"DNS.{i} = {domain}.p2\n") + i += 1 + ext.write(f"DNS.{i} = {domain}.p3\n") + i += 1 + ext.close() + + sh(f"openssl x509 -req -in /tmp/tmp.csr -CA {capath} -CAkey {caskpath} -CAcreateserial -out {outpath} -days 365 -sha256 -extfile /tmp/tmp.v3.ext") + +def get_domain_root(domain): + last_dot = domain.rfind(".") + penultimate_dot = domain.rfind(".", 0, last_dot) + return domain[penultimate_dot+1:] + +# Issue secret keys, CA cert and signed certs for given domains +# All using the same algorithm +def make_certs(outdir, domains, alg, make_ca): + if outdir[-1] != "/": + outdir += "/" + + if make_ca: + make_sk(outdir+"ca.key", alg) + make_ca_cert(outdir+"ca.crt", outdir+"ca.key") + + # Only make certs for root domains, and include subdomains + roots = {} + for domain in domains: + root = domain + if domain.count(".") > 1: + root = get_domain_root(domain) + if root in roots: + roots[root].append(domain) + else: + roots[root] = [domain] + for root in roots: + make_sk(outdir+root+".key", alg) + make_cert(outdir+root+".crt", outdir+root+".key", outdir+"ca.crt", outdir+"ca.key", root, roots[root]) + + # Make a cert for all domains because choosing a certificate as a proxy is a real pain + make_sk(outdir+"all.key", alg) + make_cert(outdir+"all.crt", outdir+"all.key", outdir+"ca.crt", outdir+"ca.key", "wikipedia.org", domains) + +RPXY_CONFIGS = { + "plain": { + "listen_http": 80, + "listen_https": 443, + "app": """[apps.{app}] + server_name = "{domain}" + tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} + reverse_proxy = [{{ upstream = [{{ location = "{domain}" }}], set_host = "{domain}"}}] + [apps.{app}_localhost] + server_name = "{domain}.localhost" + tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} + reverse_proxy = [{{ upstream = [{{ location = "{domain}" }}], set_host = "{domain}"}}] + [apps.{app}_p2] + server_name = "{domain}.p2" + tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} + reverse_proxy = [{{ upstream = [{{ location = "{domain}" }}], set_host = "{domain}"}}] + """ + }, + "tls": { + "listen_http": 80, + "listen_https": 443, + "app": """[apps.{app}] + server_name = "{domain}" + tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} + reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}] + [apps.{app}_localhost] + server_name = "{domain}.localhost" + tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} + reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}] + [apps.{app}_p2] + server_name = "{domain}.p2" + tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} + reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}] + """ + }, +} + +SETUPS = { + "none": { + "rpxy_config": "plain", + "netreplay_tls_mode": "none", + "p2_port": 80, + "listen_port": 80, + }, + "client": { + "rpxy_config": "tls", + "netreplay_tls_mode": "server", + "p2_port": 80, + "listen_port": 443, + }, + "server": { + "rpxy_config": "plain", + "netreplay_tls_mode": "client", + "p2_port": 443, + "listen_port": 80, + }, + #"both": { + # "rpxy_config": "tls", + # "netreplay_tls_mode": "both", + # "p2_port": 443, + # "listen_port": 443, + #}, +} + +def make_rpxy_config(outdir, domains, cryptodir, config_name): + if outdir[-1] != "/": + outdir += "/" + if cryptodir[-1] != "/": + cryptodir += "/" + + config = RPXY_CONFIGS[config_name] + f = open(outdir+config_name+".toml", "w") + f.write("listen_port = {}\nlisten_port_tls = {}\n".format(config["listen_http"], config["listen_https"])) + for domain in domains: + app = domain.replace(".", "_") + root = get_domain_root(domain) + f.write(config["app"].format( + app=app, + domain=domain, + cert=cryptodir+root+".crt", + key=cryptodir+root+".key", + )) + f.close() + +def make_everything(expdir, domains, make_ca): + os.makedirs(expdir, exist_ok=True) + if expdir[-1] != "/": + expdir += "/" + for alg in CERT_SIGN_ALGS: + algdir = expdir+"certs/"+alg + os.makedirs(algdir, exist_ok=True) + make_certs(algdir, domains, alg, make_ca) + # this will be a symbolic link to the chosen certs directory + cryptodir = expdir+"current_certs" + configdir = expdir+"configs/" + os.makedirs(configdir, exist_ok=True) + for config_name in RPXY_CONFIGS: + make_rpxy_config(configdir, domains, cryptodir, config_name) + +def choose_cert_alg(expdir, alg): + if expdir[-1] != "/": + expdir += "/" + try: + os.unlink(expdir+"current_certs") + except FileNotFoundError as e: + pass + # WHY is dst pointing to src????? + os.symlink(expdir+"certs/"+alg, expdir+"current_certs", True) + +def choose_impl(expdir, p, impl): + if expdir[-1] != "/": + expdir += "/" + os.symlink(os.getcwd()+"/rpxy_rustls_"+impl, expdir+str(p)+"_rpxy", False) + +def run_rpxy(expdir, repodir, config_name, impl, ciphers=None, kexes=None): + if expdir[-1] != "/": + expdir += "/" + repodir = repodir.removesuffix("/") + env = {"RUST_LOG": "debug"} + if ciphers: + env["CIPHERS"] = ",".join(ciphers) + if kexes: + env["KEXES"] = ",".join(KEXES) + return subprocess.Popen([repodir+"/rpxy_rustls_"+impl, "--config", expdir+"configs/"+config_name+".toml"], env=env) + +def run_netreplay(expdir, repodir, record, p2_addr, p2_port, listen_port, tls_mode, only_record=None): + if expdir[-1] != "/": + expdir += "/" + repodir = repodir.removesuffix("/") + env = {"RUST_LOG": "debug"} + cmd = [repodir+"/netreplay", repodir+"/records/"+record["filename"], "play", p2_addr, str(p2_port), str(listen_port), expdir+"current_certs", tls_mode, "-r", str(record["repeat"])] + if only_record != None: + cmd += ["--record", only_record] + print(" ".join(cmd)) + return subprocess.Popen(cmd, env=env) + +# https://stackoverflow.com/questions/8775598/how-to-start-a-background-process-with-nohup-using-fabric +def runbg(ssh, cmd, vars={}): + print("[SSH]", cmd) + strvars = "" + for var in vars: + strvars += f"export {var}="+vars[var]+" && " + return ssh.run(f"{strvars}dtach -n `mktemp -u /tmp/dtach.XXXX` {cmd}") + +def get_cpu_stat(ssh): + res = ssh.run("/sbin/sa --list-all-names", hide=True) + for line in res.stdout.split("\n"): + if "rpxy" in line: + return float(re.finditer("\\s(\\d+\\.\\d+)cp\\s", line).__next__().group(1)) + return 0.0 + +def get_net_stat(ssh): + res = ssh.run("cat /proc/net/netstat", hide=True) + items = res.stdout.split("\n")[3].split(" ") + bytes_in = int(items[7]) + bytes_out = int(items[8]) + return (bytes_in, bytes_out) + +def run_exp(ssh, expdir, p2_path, exps, only_record=None): + wattmeter = None + if WATTMETER: + errmsg = YRefParam() + if YAPI.RegisterHub("usb", errmsg) != YAPI.SUCCESS: + sys.exit("init error" + errmsg.value) + wattmeter = YPower.FirstPower() + if wattmeter is None or not wattmeter.isOnline(): + print("No YoctoWatt connected") + exit(1) + + sh("killall netreplay") + for expname in exps: + exp = exps[expname] + for impl in exp["impls"]: + try: + ssh.run(f"killall rpxy_rustls_{impl}") + except invoke.exceptions.UnexpectedExit as e: + pass + rpxy_cpu = get_cpu_stat(ssh) + logfile = open(expdir+"/log-"+str(int(time.time())), "w") + logfile.write("exp impl alg kex cipher setup record t_start t_end cpu bytes_in bytes_out Wh\n") + for expname in exps: + exp = exps[expname] + for impl in exp["impls"]: + for alg in exp["cert"]: + for kex in exp["kexes"]: + for cipher in exp["ciphers"]: + choose_cert_alg(expdir, alg) + ssh.run(f"python {p2_path}/exp.py cert {alg}") + for setup in SETUPS: + setupdir = expdir+"setups/"+setup + for record in exp["records"]: + print(f"EXPERIMENT {expname}: {impl} {alg} {kex} {cipher} {setup}") + p2_rpxy_config = SETUPS[setup]["rpxy_config"] + vars = {"CIPHERS": cipher, "KEXES": kex} + + runbg(ssh, f"{p2_path}/rpxy_rustls_{impl} --config {expdir}/configs/{p2_rpxy_config}.toml --log-dir /dev/shm", vars) + time.sleep(1) + + p2_bytes_in, p2_bytes_out = get_net_stat(ssh) + energy = 0 + if WATTMETER: + energy = wattmeter.get_meter() + + start = time.time() + netreplay = run_netreplay(expdir, REPODIR, record, P2_ADDR, SETUPS[setup]["p2_port"], SETUPS[setup]["listen_port"], SETUPS[setup]["netreplay_tls_mode"], only_record=only_record) + + # TODO detect when netreplay has finished + try: + netreplay.wait() + except KeyboardInterrupt: + netreplay.kill() + try: + ssh.run(f"killall rpxy_rustls_{impl}") + except invoke.exceptions.UnexpectedExit as e: + pass + try: + ssh.run(f"killall dtach") + except invoke.exceptions.UnexpectedExit as e: + pass + exit(0) + + #time.sleep(30) + #sh("killall netreplay") + try: + ssh.run(f"killall rpxy_rustls_{impl}") + except invoke.exceptions.UnexpectedExit as e: + pass + try: + ssh.run(f"killall dtach") + except invoke.exceptions.UnexpectedExit as e: + pass + end = time.time() + + new_energy = 0 + if WATTMETER: + new_energy = wattmeter.get_meter() + new_p2_bytes_in, new_p2_bytes_out = get_net_stat(ssh) + new_rpxy_cpu = get_cpu_stat(ssh) + record_filename = record["filename"] + rpxy_cpu_diff = new_rpxy_cpu - rpxy_cpu + p2_bytes_in_diff = new_p2_bytes_in - p2_bytes_in + p2_bytes_out_diff = new_p2_bytes_out - p2_bytes_out + energy_diff = new_energy - energy + rpxy_cpu = new_rpxy_cpu + logfile.write(f"{expname} {impl} {alg} {kex} {cipher} {setup} {record_filename} {start} {end} {rpxy_cpu_diff} {p2_bytes_in_diff} {p2_bytes_out_diff} {energy}\n") + + logfile.flush() + if WATTMETER: + YAPI.FreeAPI() + +def update_certs(): + dist = platform.freedesktop_os_release()["ID"] + if dist == "debian": + for alg in CERT_SIGN_ALGS: + sh([ + f"sudo cp {EXPDIR}/certs/{alg}/ca.crt /usr/local/share/ca-certificates/ca-{alg}.crt", + f"sudo chown root:root /usr/local/share/ca-certificates/ca-{alg}.crt" + ]) + sh("sudo update-ca-certificates") + elif dist == "arch": + for alg in CERT_SIGN_ALGS: + sh([ + f"sudo cp {EXPDIR}/certs/{alg}/ca.crt /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt", + f"sudo chown root:root /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt" + ]) + sh("sudo update-ca-trust extract") + +# copy local dir src to remote parent dir dst (src's copy will be a subdir of dst) +def upload_dir(ssh, src, dst): + src = src.removesuffix("/") + src_name = src.split("/")[-1] + os.chdir(f"{src}/..") + os.system(f"tar -czf {src}/../tmp.tar.gz {src_name}") + print(ssh.put(src+"/../tmp.tar.gz", dst)) + print(ssh.run(f"cd {dst} && tar -xf tmp.tar.gz")) + +def connect_ssh(): + ssh_passphrase = "--passphrase" in sys.argv + connect_kwargs = {} + if ssh_passphrase: + import getpass + connect_kwargs["passphrase"] = getpass.getpass("Enter passphrase for SSH key: ") + if P2_PSW != None: + connect_kwargs["password"] = P2_PSW + ssh = fabric.Connection(P2_SSH, connect_kwargs=connect_kwargs) + return ssh + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] in ["h", "help", "?", "-h", "-help", "--help", "/?"]: + print("""Options: + make [-c] Create everything + cert Select cert signature algorithm + send Send configs and certs to p2 + update-certs Update system's certs + run Run experiment + script Print Firefox script to override DNS + +Signature algorithms: + {sig_algs} + +Implementations: + {impls} + +Make options: + -c Make CA cert (otherwise use already existing one) + +Run options: + --passphrase Prompt SSH key decryption passphrase (when using pubkey login) + --count Do not run experiments but display number of experiments + --record Only play this record +""".format( + sig_algs=" ".join(CERT_SIGN_ALGS), + impls=" ".join(IMPLS), +)) + exit() + + opt = sys.argv[1] + if opt == "make": + make_ca = "-c" in sys.argv + make_everything(EXPDIR, DOMAINS, make_ca) + elif opt == "cert": + alg = sys.argv[2] + if not alg in CERT_SIGN_ALGS: + print("Error: alg must be in", CERT_SIGN_ALGS) + exit(1) + choose_cert_alg(EXPDIR, alg) + elif opt == "send": + import fabric + ssh = connect_ssh() + upload_dir(ssh, EXPDIR, "/dev/shm") + elif opt == "update-certs": + import platform + update_certs() + elif opt == "run": + if "--count" in sys.argv: + exps = 0 + for expname in EXPERIMENTS: + exp = EXPERIMENTS[expname] + exps += len(exp["impls"]) * len(exp["cert"]) * len(exp["kexes"]) * len(exp["ciphers"]) * len(exp["records"]) + print("Experiments to make:", exps * len(SETUPS)) + exit(0) + + import time + import invoke + import re + import fabric + + if WATTMETER: + import yoctopuce + from yoctopuce.yocto_api import * + from yoctopuce.yocto_power import * + + ssh = connect_ssh() + run_exp(ssh, EXPDIR, P2_REPODIR, EXPERIMENTS, only_record=getargv("--record", None)) + elif opt == "script": + print(SCRIPT_FIREFOX_HOSTS) + else: + print("Unknown command, use help for help") + exit(1) diff --git a/exp_proxies.py b/exp_proxies.py new file mode 100644 index 0000000..8b5b57b --- /dev/null +++ b/exp_proxies.py @@ -0,0 +1,535 @@ +#!/usr/bin/python3 +# OLD VERSION +# using 3 proxies between actual browser and actual server + +import os, sys, subprocess + +REPODIR = "/home/tuxmain/Documents/doc/tlsbench" +P2_SSH = "exp@p2" +P2_PSW = "exp" +P2_REPODIR = "/home/exp/exp" +EXPDIR = "/dev/shm/exp" +DOMAINS_ = [ + # Apple + "apple.com", "www.apple.com", "graffiti-tags.apple.com", + "store.storeimages.cdn-apple.com", + "mzstatic.com", "is1-ssl.mzstatic.com" + # Youtube video + "youtube.com", "www.youtube.com", "i.ytimg.com", "fonts.gstatic.com", "www.google.com", "accounts.google.com", "yt3.ggpht.com", "www.gstatic.com", "fonts.googleapis.com", "www.googleadservices.com", + # W3C + "w3.org", "www.w3.org", + # Amazon + "amazon.com", "www.amazon.com", + # JA3 fingerprint test + "www.edgecomputing.live", + # JA4 fingerprint test + "ja4db.com", + # Local test + "foo.home", + # Peertube + "flim.txmn.tk", +] + +#URL = "https://www.youtube.com/watch?v=IBWezPN4Ep8" +#URL = "https://www.edgecompute.live/tls/ja3" +#URL = "https://ja4db.com/id/ja4/" +#URL = "https://www.w3.org/" +#URL = "http://foo.home:8080/?hello=hi" +#URL = "https://www.apple.com/" +#URL = "https://www.amazon.com/" +URL = "https://flim.txmn.tk/w/4zpfvGB72oTL4hcSqdAwx8" + +URLS = [ + # Heavy showcase website + "https://www.apple.com/", + # Light showcase website + "https://librezo.fr/", + # Long Wikipedia article + "https://fr.wikipedia.org/wiki/Sp%C3%A9cial:Recherche?search=Victor+Hugo&sourceid=Mozilla-search", + # Youtube video + "https://www.youtube.com/watch?v=IBWezPN4Ep8", + # Peertube video + "https://videos.domainepublic.net/videos/watch/eaee7866-d209-4e5c-b7b0-443395b79c82", + # Google search + "https://www.google.com/search?q=where+do+birds+go+when+it+rains" +] + +CERT_SIGN_ALGS = [ + "prime256v1", # widely used + "secp384r1", # rarely used but supported by browsers because it's NIST standard + #"secp521r1", # not supported by browsers because NIST said it was not needed + "rsa2048", "rsa3072", "rsa4096", # widely used +] +IMPLS = [ + #"aws_lc_rs", # Amazon's Rust crypto widely used in Rust stuff + #"boring", # Google's fork of OpenSSL used in Chrome and Android + #"openssl", # widely used + "ring", # used in most Rust stuff + #"symcrypt", # Microsoft's crypto + #"wolfcrypt" # used in embedded (won't build with rpxy for now) +] +# Symmetric ciphers +# They also allow to choose the TLS version. +CIPHERS = [ + # TLS 1.3 + "AES_256_GCM_SHA384", + "AES_128_GCM_SHA256", + "CHACHA20_POLY1305_SHA256", + # TLS 1.2 + # ECDSA vs RSA refers to the certificate signature algorithm. + # DH is EC is aither case, using the group specified below. + "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", +] +KEXES = [ + "X25519", + "SECP256R1", + "SECP384R1", +] + +# Testing all combinations would be too much. Instead we isolate independent parts. +EXPERIMENTS = { + # Compare ciphers among implementations and TLS versions + "impl-cipher-ver": { + "impls": IMPLS, + "urls": [URL], + "ciphers": [ + "AES_128_GCM_SHA256", + "AES_256_GCM_SHA384", + "CHACHA20_POLY1305_SHA256", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + ], + "kexes": ["X25519"], + "cert": ["prime256v1"], + }, + # Compare signatures among implementations and TLS versions + "impl-cert-ver": { + "impls": IMPLS, + "urls": [URL], + "ciphers": [ + "AES_128_GCM_SHA256", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + ], + "kexes": ["X25519"], + "cert": ["prime256v1", "secp384r1", "rsa2048", "rsa3072", "rsa4096"], + }, + # Compare key exchange groups among implementations and TLS versions + "impl-kex-ver": { + "impls": IMPLS, + "urls": [URL], + "ciphers": [ + "AES_128_GCM_SHA256", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", + ], + "kexes": ["X25519", "SECP256R1", "SECP384R1"], + "cert": ["prime256v1"], + }, +} + +DOMAINS = [] +for domain in DOMAINS_: + if not domain in DOMAINS: + DOMAINS.append(domain) + +# JS to redirect the target domains to local (bypass DNS without altering system's config or webpages or packets) +SCRIPT_FIREFOX_HOSTS = """const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(Ci.nsINativeDNSResolverOverride); +gOverride.clearOverrides(); +var names = """+str(DOMAINS)+"""; +for(var i in names) { + gOverride.addIPOverride(names[i], "127.0.0.1"); +} +""" + +def getargv(arg:str, default:str="", n:int=1, args:list=sys.argv) -> str: + if arg in args and len(args) > args.index(arg)+n: + return args[args.index(arg)+n] + else: + return default + +def sh(cmds): + if type(cmds) == list: + for cmd in cmds: + os.system(cmd) + elif type(cmds) == str: + os.system(cmds) + else: + raise TypeError + +def make_sk(outpath, alg): + sh({ + "ed25519": [ + f"openssl genpkey -out {outpath}.sec1 -algorithm ed25519", + f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM" + ], + "prime256v1": [ + f"openssl ecparam -genkey -name prime256v1 -noout -out {outpath}.sec1", + f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM" + ], + "rsa2048": [ + f"openssl genrsa -out {outpath} 2048" + ], + "rsa3072": [ + f"openssl genrsa -out {outpath} 3072" + ], + "rsa4096": [ + f"openssl genrsa -out {outpath} 4096" + ], + "secp384r1": [ + f"openssl ecparam -genkey -name secp384r1 -noout -out {outpath}.sec1", + f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM" + ], + }[alg]) + +def make_ca_cert(outpath, skpath): + sh(f"openssl req -x509 -new -key {skpath} -sha256 -days 730 -out {outpath} -subj '/CN=Foobar Root CA/C=AT/ST=Lyon/L=Lyon/O=Foobar'") + +def make_cert(outpath, skpath, capath, caskpath, name, domains): + sh(f"openssl req -new -key {skpath} -sha256 -out /tmp/tmp.csr -subj '/CN={name}/C=AT/ST=Lyon/L=Lyon/O={name}'") + ext = open("/tmp/tmp.v3.ext", "w") + ext.write("""authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +""") + i = 1 + for domain in domains: + ext.write(f"DNS.{i} = {domain}\n") + i += 1 + ext.write(f"DNS.{i} = {domain}.localhost\n") + i += 1 + ext.write(f"DNS.{i} = {domain}.p2\n") + i += 1 + ext.write(f"DNS.{i} = {domain}.p3\n") + i += 1 + ext.close() + + sh(f"openssl x509 -req -in /tmp/tmp.csr -CA {capath} -CAkey {caskpath} -CAcreateserial -out {outpath} -days 365 -sha256 -extfile /tmp/tmp.v3.ext") + +def get_domain_root(domain): + last_dot = domain.rfind(".") + penultimate_dot = domain.rfind(".", 0, last_dot) + return domain[penultimate_dot+1:] + +# Issue secret keys, CA cert and signed certs for given domains +# All using the same algorithm +def make_certs(outdir, domains, alg): + if outdir[-1] != "/": + outdir += "/" + + make_sk(outdir+"ca.key", alg) + make_ca_cert(outdir+"ca.crt", outdir+"ca.key") + + # Only make certs for root domains, and include subdomains + roots = {} + for domain in domains: + root = domain + if domain.count(".") > 1: + root = get_domain_root(domain) + if root in roots: + roots[root].append(domain) + else: + roots[root] = [domain] + for root in roots: + make_sk(outdir+root+".key", alg) + make_cert(outdir+root+".crt", outdir+root+".key", outdir+"ca.crt", outdir+"ca.key", root, roots[root]) + +RPXY_CONFIGS = { + "p1_plain": { + "listen_http": 80, + "listen_https": 443, + "app": """[apps.{app}] +server_name = "{domain}" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p2:42002" }}]}}] +[apps.{app}_localhost] +server_name = "{domain}.localhost" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p2:42002" }}]}}] +""" + }, + "p1_tls": { + "listen_http": 80, + "listen_https": 443, + "app": """[apps.{app}] +server_name = "{domain}" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p2:43002", tls = true }}], set_host = "{domain}.p2"}}] +[apps.{app}_localhost] +server_name = "{domain}.localhost" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p2:43002", tls = true }}], set_host = "{domain}.p2"}}] +""" + }, + "p2_plain": { + "listen_http": 42002, + "listen_https": 43002, + "app": """[apps.{app}] +server_name = "{domain}" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p3:42003" }}], set_host = "{domain}.p3"}}] +[apps.{app}_localhost] +server_name = "{domain}.localhost" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p3:42003" }}], set_host = "{domain}.p3"}}] +[apps.{app}_p2] +server_name = "{domain}.p2" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p3:42003" }}], set_host = "{domain}.p3"}}] +""" + }, + "p2_tls": { + "listen_http": 42002, + "listen_https": 43002, + "app": """[apps.{app}] +server_name = "{domain}" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p3:43003", tls = true }}], set_host = "{domain}.p3"}}] +[apps.{app}_localhost] +server_name = "{domain}.localhost" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p3:43003", tls = true }}], set_host = "{domain}.p3"}}] +[apps.{app}_p2] +server_name = "{domain}.p2" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}.p3:43003", tls = true }}], set_host = "{domain}.p3"}}] +""" + }, + "p3": { + "listen_http": 42003, + "listen_https": 43003, + "app": """[apps.{app}] +server_name = "{domain}" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}] +[apps.{app}_localhost] +server_name = "{domain}.localhost" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}] +[apps.{app}_p3] +server_name = "{domain}.p3" +tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }} +reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}] +""" + }, +} + +SETUPS = { + #"none": { + # "rpxy_configs": ["p1_plain", "p2_plain", "p3"], + #}, + #"client": { + # "rpxy_configs": ["p1_plain", "p2_tls", "p3"], + #}, + "server": { + "rpxy_configs": ["p1_tls", "p2_plain", "p3"], + }, +} + +def make_rpxy_config(outdir, domains, cryptodir): + if outdir[-1] != "/": + outdir += "/" + if cryptodir[-1] != "/": + cryptodir += "/" + + for config_name in RPXY_CONFIGS: + config = RPXY_CONFIGS[config_name] + f = open(outdir+config_name+".toml", "w") + f.write("listen_port = {}\nlisten_port_tls = {}\n".format(config["listen_http"], config["listen_https"])) + for domain in domains: + app = domain.replace(".", "_") + root = get_domain_root(domain) + f.write(config["app"].format( + app=app, + domain=domain, + cert=cryptodir+root+".crt", + key=cryptodir+root+".key", + )) + f.close() + +def make_everything(expdir, domains): + os.makedirs(expdir, exist_ok=True) + if expdir[-1] != "/": + expdir += "/" + for alg in CERT_SIGN_ALGS: + algdir = expdir+"certs/"+alg + os.makedirs(algdir, exist_ok=True) + make_certs(algdir, domains, alg) + # this will be a symbolic link to the chosen certs directory + cryptodir = expdir+"current_certs" + for setup in SETUPS: + setupdir = expdir+"setups/"+setup + os.makedirs(setupdir, exist_ok=True) + make_rpxy_config(setupdir, domains, cryptodir) + +def choose_cert_alg(expdir, alg): + if expdir[-1] != "/": + expdir += "/" + try: + os.unlink(expdir+"current_certs") + except FileNotFoundError as e: + pass + # WHY is dst pointing to src????? + os.symlink(expdir+"certs/"+alg, expdir+"current_certs", True) + +def choose_impl(expdir, p, impl): + if expdir[-1] != "/": + expdir += "/" + os.symlink(os.getcwd()+"/rpxy_rustls_"+impl, expdir+str(p)+"_rpxy", False) + +def run_rpxy(expdir, repodir, setup, p, impl, ciphers=None, kexes=None): + if expdir[-1] != "/": + expdir += "/" + repodir = repodir.removesuffix("/") + env = {"RUST_LOG": "debug"} + if ciphers: + env["CIPHERS"] = ",".join(ciphers) + if kexes: + env["KEXES"] = ",".join(KEXES) + print("RUN", [repodir+"/rpxy_rustls_"+impl, "--config", expdir+"setups/"+setup+"/"+p+".toml"]) + return subprocess.Popen([repodir+"/rpxy_rustls_"+impl, "--config", expdir+"setups/"+setup+"/"+p+".toml"], env=env) + +# https://stackoverflow.com/questions/8775598/how-to-start-a-background-process-with-nohup-using-fabric +def runbg(ssh, cmd, vars={}): + strvars = "" + for var in vars: + strvars += f"export {var}="+vars[var]+" && " + return ssh.run(f"{strvars}dtach -n `mktemp -u /tmp/dtach.XXXX` {cmd}") + +def run_exp(ssh, expdir, p2_path, exps): + from selenium import webdriver + import time + import invoke + + logfile = open(expdir+"/log-"+str(int(time.time())), "w") + + driver = webdriver.Firefox() + driver.get("about:config") + driver.execute_script(SCRIPT_FIREFOX_HOSTS) + time.sleep(1) + for expname in exps: + exp = exps[expname] + for impl in exp["impls"]: + sh(f"killall rpxy_rustls_{impl}") + try: + ssh.run(f"killall rpxy_rustls_{impl}") + except invoke.exceptions.UnexpectedExit as e: + pass + for impl in exp["impls"]: + for alg in exp["cert"]: + for kex in exp["kexes"]: + for cipher in exp["ciphers"]: + choose_cert_alg(expdir, alg) + ssh.run(f"python {p2_path}/exp.py cert {alg}") + for setup in SETUPS: + print(f"EXPERIMENT {expname}: {impl} {alg} {kex} {cipher} {setup}") + setupdir = expdir+"setups/"+setup + p1 = run_rpxy(expdir, REPODIR, setup, SETUPS[setup]["rpxy_configs"][0], impl) + p3 = run_rpxy(expdir, REPODIR, setup, SETUPS[setup]["rpxy_configs"][2], impl) + p2_rpxy_config = SETUPS[setup]["rpxy_configs"][1] + vars = {"CIPHERS": cipher, "KEXES": kex} + + runbg(ssh, f"{p2_path}/rpxy_rustls_{impl} --config {expdir}/setups/{setup}/{p2_rpxy_config}.toml --log-dir /dev/shm", vars) + + start = time.time() + for url in exp["urls"]: + driver.get(url) + #time.sleep(20) + input("???") + end = time.time() + logfile.write(f"{expname} {impl} {alg} {kex} {cipher} {setup} {start} {end}\n") + + # DO the experiment + + p1.kill() + p3.kill() + ssh.run(f"killall rpxy_rustls_{impl}") + ssh.run(f"killall dtach") + logfile.flush() + driver.quit() + +def update_certs(): + import platform + dist = platform.freedesktop_os_release()["ID"] + if dist == "debian": + for alg in CERT_SIGN_ALGS: + sh([ + f"sudo cp {EXPDIR}/certs/{alg}/ca.crt /usr/local/share/ca-certificates/ca-{alg}.crt", + f"sudo chown root:root /usr/local/share/ca-certificates/ca-{alg}.crt" + ]) + sh("sudo update-ca-certificates") + elif dist == "arch": + for alg in CERT_SIGN_ALGS: + sh([ + f"sudo cp {EXPDIR}/certs/{alg}/ca.crt /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt", + f"sudo chown root:root /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt" + ]) + sh("sudo update-ca-trust extract") + +# copy local dir src to remote parent dir dst (src's copy will be a subdir of dst) +def upload_dir(ssh, src, dst): + src = src.removesuffix("/") + src_name = src.split("/")[-1] + os.chdir(f"{src}/..") + os.system(f"tar -czf {src}/../tmp.tar.gz {src_name}") + print(ssh.put(src+"/../tmp.tar.gz", dst)) + print(ssh.run(f"cd {dst} && tar -xf tmp.tar.gz")) + +def connect_ssh(): + import fabric + ssh_passphrase = "--passphrase" in sys.argv + connect_kwargs = {} + if ssh_passphrase: + import getpass + connect_kwargs["passphrase"] = getpass.getpass("Enter passphrase for SSH key: ") + if P2_PSW != None: + connect_kwargs["password"] = P2_PSW + ssh = fabric.Connection(P2_SSH, connect_kwargs=connect_kwargs) + return ssh + +if __name__ == "__main__": + if len(sys.argv) < 2 or sys.argv[1] in ["h", "help", "?", "-h", "-help", "--help", "/?"]: + print("""Options: + make Create everything + cert Select cert signature algorithm + send + update-certs + run Run experiment + +Signature algorithms: + {sig_algs} + +Implementations: + {impls} + +Run options: + --passphrase Prompt SSH key decryption passphrase (when using pubkey login) +""".format( + sig_algs=" ".join(CERT_SIGN_ALGS), + impls=" ".join(IMPLS), +)) + exit() + + opt = sys.argv[1] + if opt == "make": + make_everything(EXPDIR, DOMAINS) + elif opt == "cert": + alg = sys.argv[2] + if not alg in CERT_SIGN_ALGS: + print("Error: alg must be in", CERT_SIGN_ALGS) + exit(1) + choose_cert_alg(EXPDIR, alg) + elif opt == "send": + ssh = connect_ssh() + upload_dir(ssh, EXPDIR, "/dev/shm") + elif opt == "update-certs": + update_certs() + elif opt == "run": + ssh = connect_ssh() + run_exp(ssh, EXPDIR, P2_REPODIR, EXPERIMENTS) + else: + print("Unknown command, use help for help") + exit(1) diff --git a/install-certs-arch.sh b/install-certs-arch.sh new file mode 100644 index 0000000..2bf5ba2 --- /dev/null +++ b/install-certs-arch.sh @@ -0,0 +1,6 @@ +sudo cp /dev/shm/exp/certs/rsa2048/ca.crt /etc/ca-certificates/trust-source/anchors/ca-rsa2048.crt +sudo cp /dev/shm/exp/certs/rsa3072/ca.crt /etc/ca-certificates/trust-source/anchors/ca-rsa3072.crt +sudo cp /dev/shm/exp/certs/rsa4096/ca.crt /etc/ca-certificates/trust-source/anchors/ca-rsa4096.crt +sudo cp /dev/shm/exp/certs/secp384r1/ca.crt /etc/ca-certificates/trust-source/anchors/ca-secp384r1.crt +sudo chown root:root /etc/ca-certificates/trust-source/anchors/ca-*.crt +sudo update-ca-trust extract diff --git a/install-certs-debian.sh b/install-certs-debian.sh new file mode 100644 index 0000000..9554cf2 --- /dev/null +++ b/install-certs-debian.sh @@ -0,0 +1,6 @@ +sudo cp /dev/shm/exp/certs/rsa2048/ca.crt /usr/local/share/ca-certificates/ca-rsa2048.crt +sudo cp /dev/shm/exp/certs/rsa3072/ca.crt /usr/local/share/ca-certificates/ca-rsa3072.crt +sudo cp /dev/shm/exp/certs/rsa4096/ca.crt /usr/local/share/ca-certificates/ca-rsa4096.crt +sudo cp /dev/shm/exp/certs/secp384r1/ca.crt /usr/local/share/ca-certificates/ca-secp384r1.crt +sudo chown root:root /usr/local/share/ca-certificates/ca-*.crt +sudo update-ca-certificates diff --git a/mkcacert.sh b/mkcacert.sh new file mode 100644 index 0000000..9aa56db --- /dev/null +++ b/mkcacert.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Generate CA secret key +openssl genrsa -aes256 -out ca.key -passout pass:foo 4096 + +# Generate CA certificate +openssl req -x509 -new -nodes -key certs/ca.key -sha256 -days 1826 -out certs/ca.crt -passin pass:foo -subj '/CN=MyOrg Root CA/C=AT/ST=Vienna/L=Vienna/O=MyOrg' diff --git a/mkcert.sh b/mkcert.sh new file mode 100644 index 0000000..dc8343f --- /dev/null +++ b/mkcert.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# $1 is the first script argument, that will be the certificate's name. +# Other arguments are other domain names to be added to the certificate. + +# Generate secret key +openssl req -new -nodes -out certs/$1.csr -newkey rsa:2048 -keyout certs/$1.key -subj "/CN=$1/C=AT/ST=Vienna/L=Vienna/O=MyOrg" + +# Write certificate information +cat > certs/$1.v3.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +EOF + +# Iterate script arguments +let i=1 +for name in "$@" +do + echo "DNS.$i = $name" >> certs/$1.v3.ext + let i++ + echo "DNS.$i = $name.localhost" >> certs/$1.v3.ext + let i++ +done + +# Sign certificate with CA +openssl x509 -req -in certs/$1.csr -CA certs/ca.crt -CAkey certs/ca.key -CAcreateserial -out certs/$1.crt -days 730 -sha256 -extfile certs/$1.v3.ext -passin pass:foo diff --git a/mkcerts.sh b/mkcerts.sh new file mode 100644 index 0000000..08c2738 --- /dev/null +++ b/mkcerts.sh @@ -0,0 +1,3 @@ +#!/bin/bash +sh mkcert.sh apple.com www.apple.com securemetrics.apple.com +sh mkcert.sh mzstatic.com www.mzstatic.com is1-ssl.mzstatic.com diff --git a/records/apple b/records/apple new file mode 100644 index 0000000..3ed9121 Binary files /dev/null and b/records/apple differ diff --git a/records/google b/records/google new file mode 100644 index 0000000..1d4cb5d Binary files /dev/null and b/records/google differ diff --git a/records/youtube b/records/youtube new file mode 100644 index 0000000..1b1d8d0 Binary files /dev/null and b/records/youtube differ diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..76d883f --- /dev/null +++ b/run.sh @@ -0,0 +1,8 @@ +#kissdns kissdns.json 5532 +RUST_LOG=debug ./rpxy_rustls_ring --config experiments/p1_tls.toml & +RUST_LOG=debug ./rpxy_rustls_ring --config experiments/p2_tls.toml & +RUST_LOG=debug ./rpxy_rustls_ring --config experiments/p3.toml & + +# By default, ^C does not kill subprocesses. This fixes it. +trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT +wait