diff --git a/README.md b/README.md index 0644053..f72d0ae 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,6 @@ 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 @@ -122,31 +118,6 @@ 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 @@ -175,13 +146,68 @@ Features: * 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 +## Reproduce + +### Record + +Authorize rpxy and netreplay to bind to ports 80 and 443: +```bash +sudo setcap CAP_NET_BIND_SERVICE=+eip netreplay +``` + +Open Firefox with a dedicated profile: (create the profile using the GUI if it doesn't exist) + +```bash +firefox -P tlsbench +``` + +In settings, disable DNS security. + +In `about:config`, set `devtools.chrome.enabled` to `true`. + +In the `about:config` tab, open the console, execute this script to override DNS for the selected names, and redirect them to localhost: + +```js +const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(Ci.nsINativeDNSResolverOverride); +gOverride.clearOverrides(); +var names = [ + "apple.com", "www.apple.com", + "yt3.ggpht.com", + "accounts.google.com", "www.google.com", + "fonts.gstatic.com", "www.gstatic.com", + "mzstatic.com", + "wikimedia.org", "intake-analytics.wikimedia.org", "meta.wikimedia.org", "upload.wikimedia.org", + "wikipedia.org", "fr.wikipedia.org", + "youtube.com", "www.youtube.com", + "i.ytimg.com" +]; +for(var i in names) { + gOverride.addIPOverride(names[i], "127.0.0.1"); +} +``` + +Stop anything running on ports 80 or 443. + +Start the record proxy: + +```bash +./netreplay records/mynewrecord record +``` + +Just browse. Any traffic to and from the selected names will be recorded. Terminate netplayer with CTRL+C when finished. + +### Measure + +Add p2 the `/etc/hosts`: + +``` +192.168.3.14 p2 +``` Install sa on p2: @@ -190,35 +216,11 @@ 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 netreplay -``` - ```bash python exp.py make -c python exp.py send python exp.py update-certs # also do this command on p2 -python exp.py run +python exp.py run --idle ``` On Debian, update-certs says 0 certs added even if it has actually updated some certs. This step is still needed. diff --git a/exp.py b/exp.py index 767dbc1..84b7a98 100644 --- a/exp.py +++ b/exp.py @@ -6,6 +6,7 @@ P2_SSH = "exp@p2" P2_PSW = "exp" P2_REPODIR = "/home/exp/exp" EXPDIR = "/dev/shm/exp" +LOG_BACKUP_DIR = "/home/tuxmain" P2_ADDR = "192.168.3.14" DOMAINS_ = [ # Apple @@ -33,11 +34,11 @@ DOMAINS_ = [ WATTMETER = True RECORDS = [ - #{ "filename": "youtube", "repeat": 1 }, + #{ "filename": "youtube", "repeat": 1000 }, #{ "filename": "peertube", "repeat": 10 }, - #{ "filename": "wikipedia", "repeat": 10 }, - { "filename": "apple", "repeat": 100 }, - #{ "filename": "google", "repeat": 10 }, + { "filename": "wikipedia", "repeat": 100 }, + #{ "filename": "apple", "repeat": 1000 }, + #{ "filename": "google", "repeat": 1000 }, ] CERT_SIGN_ALGS = [ "prime256v1", # widely used @@ -46,8 +47,8 @@ CERT_SIGN_ALGS = [ "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 + "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 @@ -72,6 +73,7 @@ KEXES = [ "SECP256R1", "SECP384R1", ] +IDLE = "idle - - - - - - 600.000081539154 0.0 896 4792 0.5399999999999991" # Testing all combinations would be too much. Instead we isolate independent parts. EXPERIMENTS = { @@ -83,36 +85,36 @@ EXPERIMENTS = { "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", + #"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 + ## 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", + #"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", ], "kexes": ["X25519"], "cert": [ "prime256v1", #"secp384r1", "rsa2048", - #"rsa3072", "rsa4096" + "rsa3072", "rsa4096" ], }, - # # Compare key exchange groups among implementations and TLS versions + ## 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", + #"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256", ], "kexes": ["X25519", "SECP256R1", "SECP384R1"], "cert": ["prime256v1"], @@ -122,8 +124,9 @@ EXPERIMENTS = { # "records": RECORDS, # "ciphers": [ # "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": ["SECP384R1"], + # "kexes": ["X25519"], # "cert": ["prime256v1"], #}, } @@ -286,24 +289,28 @@ SETUPS = { "netreplay_tls_mode": "none", "p2_port": 80, "listen_port": 80, + "tls_invariant": True, }, "client": { "rpxy_config": "tls", "netreplay_tls_mode": "server", "p2_port": 80, "listen_port": 443, + "tls_invariant": False, }, "server": { "rpxy_config": "plain", "netreplay_tls_mode": "client", "p2_port": 443, "listen_port": 80, + "tls_invariant": False, }, #"both": { # "rpxy_config": "tls", # "netreplay_tls_mode": "both", # "p2_port": 443, # "listen_port": 443, + # "tls_invariant": False, #}, } @@ -357,22 +364,15 @@ def choose_impl(expdir, p, impl): 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): +def run_netreplay(expdir, repodir, record, p2_addr, p2_port, listen_port, tls_mode, only_record=None, ciphers=None, kexes=None): if expdir[-1] != "/": expdir += "/" repodir = repodir.removesuffix("/") env = {"RUST_LOG": "debug"} if ciphers: - env["CIPHERS"] = ",".join(ciphers) + env["CIPHERS"] = 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"} + env["KEXES"] = kexes 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] @@ -401,7 +401,7 @@ def get_net_stat(ssh): bytes_out = int(items[8]) return (bytes_in, bytes_out) -def run_exp(ssh, expdir, p2_path, exps, only_record=None): +def run_exp(ssh, expdir, p2_path, exps, only_record=None, idle=False): wattmeter = None if WATTMETER: errmsg = YRefParam() @@ -420,13 +420,58 @@ def run_exp(ssh, expdir, p2_path, exps, only_record=None): ssh.run(f"killall rpxy_rustls_{impl}") except invoke.exceptions.UnexpectedExit as e: pass - rpxy_cpu = get_cpu_stat(ssh) - logfile_name = expdir+"/log-"+str(int(time.time())) - logfile = open(logfile_name, "w") + logfile_name = "log-"+str(int(time.time())) + logfile_path = expdir+"/"+logfile_name + logfile = open(logfile_path, "w") logfile.write("exp impl alg kex cipher setup record time cpu bytes_in bytes_out Wh\n") logfile.close() + + if idle: + print("Measuring idle...") + rpxy_cpu = get_cpu_stat(ssh) + p2_bytes_in, p2_bytes_out = get_net_stat(ssh) + energy = 0 + if WATTMETER: + energy = wattmeter.get_meter() + start = time.time() + + time.sleep(600) + + 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) + 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 + time_diff = end - start + while True: + try: + with open(logfile_path, "a") as logfile: + logfile.write(f"idle - - - - - - {time_diff} {rpxy_cpu_diff} {p2_bytes_in_diff} {p2_bytes_out_diff} {energy_diff}\n") + logfile.close() + break + except Exception as e: + print("Can't open log file:", e) + time.sleep(1) + else: + while True: + try: + with open(logfile_path, "a") as logfile: + logfile.write(IDLE+"\n") + logfile.close() + break + except Exception as e: + print("Can't open log file:", e) + time.sleep(1) + sh(f"cp {logfile_path} {LOG_BACKUP_DIR}/{logfile_name}") + for expname in exps: exp = exps[expname] + first_set = True for impl in exp["impls"]: for alg in exp["cert"]: for kex in exp["kexes"]: @@ -434,6 +479,8 @@ def run_exp(ssh, expdir, p2_path, exps, only_record=None): choose_cert_alg(expdir, alg) ssh.run(f"python {p2_path}/exp.py cert {alg}") for setup in SETUPS: + if SETUPS[setup]["tls_invariant"] and not first_set: + continue setupdir = expdir+"setups/"+setup for record in exp["records"]: print(f"EXPERIMENT {expname}: {impl} {alg} {kex} {cipher} {setup}") @@ -443,13 +490,14 @@ def run_exp(ssh, expdir, p2_path, exps, only_record=None): runbg(ssh, f"{p2_path}/rpxy_rustls_{impl} --config {expdir}/configs/{p2_rpxy_config}.toml --log-dir /dev/shm", vars) time.sleep(1) + rpxy_cpu = get_cpu_stat(ssh) 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) + 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, ciphers=cipher, kexes=kex) # TODO detect when netreplay has finished try: @@ -468,6 +516,10 @@ def run_exp(ssh, expdir, p2_path, exps, only_record=None): #time.sleep(30) #sh("killall netreplay") + try: + ssh.run(f"rm /dev/shm/access.log /dev/shm/rpxy.log") + except invoke.exceptions.UnexpectedExit as e: + pass try: ssh.run(f"killall rpxy_rustls_{impl}") except invoke.exceptions.UnexpectedExit as e: @@ -488,17 +540,18 @@ def run_exp(ssh, expdir, p2_path, exps, only_record=None): 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 time_diff = end - start while True: try: - with open(logfile_name, "a") as logfile: + with open(logfile_path, "a") as logfile: logfile.write(f"{expname} {impl} {alg} {kex} {cipher} {setup} {record_filename} {time_diff} {rpxy_cpu_diff} {p2_bytes_in_diff} {p2_bytes_out_diff} {energy_diff}\n") logfile.close() break except Exception as e: print("Can't open log file:", e) time.sleep(1) + sh(f"cp {logfile_path} {LOG_BACKUP_DIR}/{logfile_name}") + first_set = False if WATTMETER: YAPI.FreeAPI() @@ -564,6 +617,7 @@ 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 + --idle Also measure when idle """.format( sig_algs=" ".join(CERT_SIGN_ALGS), impls=" ".join(IMPLS), @@ -607,7 +661,7 @@ Run options: from yoctopuce.yocto_power import * ssh = connect_ssh() - run_exp(ssh, EXPDIR, P2_REPODIR, EXPERIMENTS, only_record=getargv("--record", None)) + run_exp(ssh, EXPDIR, P2_REPODIR, EXPERIMENTS, only_record=getargv("--record", None), idle="--idle" in sys.argv) elif opt == "script": print(SCRIPT_FIREFOX_HOSTS) else: diff --git a/plots.py b/plots.py new file mode 100644 index 0000000..44ac542 --- /dev/null +++ b/plots.py @@ -0,0 +1,148 @@ +import os, sys + +# Nice labels for algorithms of all kinds +ALG_LABEL = { + "AES_128_GCM_SHA256": "AES128", + "AES_256_GCM_SHA384": "AES256", + "CHACHA20_POLY1305_SHA256": "CHACHA20", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256": "AES128", + "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384": "AES256", + "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": "CHACHA20", + "prime256v1": "prime256v1", + "rsa2048": "rsa2048", + "rsa3072": "rsa3072", + "rsa4096": "rsa4096", + "X25519": "X25519", + "SECP256R1": "SECP256R1", + "SECP384R1": "SECP384R1" +} + +# Nice labels for TLS versions using ciphers +VER_LABEL = { + "AES_128_GCM_SHA256": "1.3", + "AES_256_GCM_SHA384": "1.3", + "CHACHA20_POLY1305_SHA256": "1.3", + "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256": "1.2", + "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384": "1.2", + "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": "1.2" +} + +# Titles for measured quantities +OBJ_TITLE = { + "cpu": "CPU time", + "energy": "energy consumption" +} +# Logfile column names +COL = { + "cpu": "cpu", + "energy": "Wh", + "cipher": "cipher", + "cert": "alg", + "kex": "kex" +} +# Physical units by object +UNIT = { + "cpu": "s", + "energy": "W" +} +# Titles for criteria +CRITERION_TITLE = { + "cipher": "cipher", + "cert": "signature algorithm", + "kex": "key exchange" +} + +# Where gnuplot files, data files and images are output +PLOTS_DIR = "/dev/shm/plots" + +def gnuplot_histogram(**kwargs): + cluster = "" + for i in range(kwargs["nb_impls"]-1): + cluster += """, "" using {}:xticlabels(1) title col""".format(i+4) + f = open("{plots_dir}/{object}_by_{criterion}_{side}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs), "w") + f.write("""\ +set terminal pngcairo enhanced font "CMU Sans Serif,11" fontscale 1.0 size 800, 600 +set output "{plots_dir}/{object}_by_{criterion}_{side}.png" +set boxwidth 0.9 absolute +set style fill solid 1.0 border lt -1 +set style histogram clustered gap 1 title textcolor lt -1 +set style data histograms +set title font "CMU Sans Serif,12" "{object_title} by {criterion_title} ({side} side) ({unit})" +set xtics border in scale 0,0 nomirror rotate by -45 autojustify +set key fixed right top vertical Right noreverse noenhanced autotitle nobox +set colorbox vertical origin screen 0.9, 0.2 size screen 0.05, 0.6 front noinvert bdefault +set xrange [ * : * ] noreverse writeback +plot \ + newhistogram "", "{plots_dir}/{object}_by_{criterion}_{side}.dat" using 2:xticlabels(1) notitle col, \ + newhistogram "", "{plots_dir}/{object}_by_{criterion}_{side}.dat" using 3:xticlabels(1) title col{cluster} +""".format(plots_dir=PLOTS_DIR, cluster=cluster, **kwargs)) + f.close() + os.system("gnuplot {plots_dir}/{object}_by_{criterion}_{side}.gnuplot".format(plots_dir=PLOTS_DIR, **kwargs)) + +def make_plot(records, exp, criterion, side, obj): + f = open(f"/dev/shm/plots/{obj}_by_{criterion}_{side}.dat", "w") + ciphers = {} + impls = [] + plain_line = None + idle_val = None + + for record in records: + if record["exp"] == "idle": + idle_val = float(record[COL[obj]]) / float(record["time"]) + if record["exp"] != exp: + continue + if record["setup"] == "none": + plain_line = "plain {}".format(float(record[COL[obj]]) - idle_val * float(record["time"])) + + if plain_line == None: + return + + for record in records: + if record["exp"] != exp: + continue + elif record["setup"] == side: + if record[COL[criterion]] not in ciphers: + ciphers[record[COL[criterion]]] = {} + ciphers[record[COL[criterion]]][record["impl"]] = float(record[COL[obj]]) - idle_val * float(record["time"]) + if record["impl"] not in impls: + impls.append(record["impl"]) + impls.sort() + f.write("{} none {}\n".format(criterion, " ".join(impls))) + f.write(plain_line+" -"*len(impls)+"\n") + for cipher in ciphers: + f.write("{}({}) - {}\n".format( + ALG_LABEL[cipher], + VER_LABEL[record["cipher"]], + " ".join([ + str(ciphers[cipher][impl]) + for impl in impls + ]), + )) + f.close() + gnuplot_histogram(object=obj, criterion=criterion, side=side, object_title=OBJ_TITLE[obj], criterion_title=CRITERION_TITLE[criterion], unit=UNIT[obj], nb_impls=len(impls)) + +if __name__ == "__main__": + logfile_name = sys.argv[1] + logfile = open(logfile_name, "r") + lines = logfile.readlines() + logfile.close() + + colnames = lines[0].removesuffix("\n").split(" ") + + records = [] + for line in lines[1:]: + cols = line.removesuffix("\n").split(" ") + record = {} + for col in range(len(cols)): + record[colnames[col]] = cols[col] + records.append(record) + + os.makedirs("/dev/shm/plots", exist_ok=True) + + for side in ["client", "server"]: + make_plot(records, "impl-cipher-ver", "cipher", side, "cpu") + make_plot(records, "impl-cipher-ver", "cipher", side, "energy") + make_plot(records, "impl-cert-ver", "cert", side, "cpu") + make_plot(records, "impl-cert-ver", "cert", side, "energy") + make_plot(records, "impl-kex-ver", "kex", side, "cpu") + make_plot(records, "impl-kex-ver", "kex", side, "energy") diff --git a/records/wikipedia b/records/wikipedia new file mode 100644 index 0000000..3d03970 Binary files /dev/null and b/records/wikipedia differ