Harvest Now, Decrypt Later. This tool captures selected TLS 1.2, TLS 1.3, QUIC, and SSH configurations, then reconstructs keys and authenticated plaintext from the retained wire data. It models a future adversary that can solve the relevant asymmetric problem; it does not implement the quantum algorithm itself.
Instrumented OpenSSL and OpenSSH export a private value during the handshake. That value is the simulated output of the future RSA/ECDLP computation. The reconstruction path may consume that value and public wire data only. Separately labelled endpoint state is retained solely for post-reconstruction comparison and is never used to supply an intermediate attack value.
For TLS 1.3 and QUIC, keys/simulated_quantum_output.json contains exactly one
recovered X25519 private value and its public consistency check. Peer and local
key shares are parsed again from the capture. Complete endpoint exports are in
openssl_ephemeral_ground_truth.json; the decoder never reads them. TLS 1.2
uses simulated_quantum_output.pem as the recovered long-term RSA key and
retains key.pem as endpoint state.
sudo apt install openssl wireshark tshark python3 python3-cryptography build-essential autoconf automake zlib1g-dev libssl-dev
# Installs under ./openssl/.local, the default used by hndl.py.
./scripts/build_openssl.sh -c -b -i
./scripts/build_quic_server.sh
./scripts/build_openssh.sh -c -b -iLive capture also requires permission to run dumpcap on the selected
interface. On Debian/Ubuntu, enable non-root packet capture during Wireshark
package configuration and add the user to the wireshark group, then start a
new login session. Verify with dumpcap -D before running the pipeline.
Each invocation captures a session and derives the symmetric keys in one step:
python3 hndl.py --protocol ssh
python3 hndl.py --protocol ssh --ssh-rekey-limit 64K --ssh-payload-bytes 300000
python3 hndl.py --protocol tls13 --mode 1rtt
python3 hndl.py --protocol tls13 --mode 0rtt
python3 hndl.py --protocol tls13 --mode 0rtt --tls13-resumption-kex psk-only
python3 hndl.py --protocol tls13 --mode 0rtt --tls13-grandchild
python3 hndl.py --protocol tls13 --mode external-psk
python3 hndl.py --protocol tls12 --mode rsa
python3 hndl.py --protocol quicThe controlled 0-RTT scenario keeps one s_server process alive across ticket
issuance and use, disables its anti-replay rejection for this first-use test,
and fails unless OpenSSL reports both a resumed TLS 1.3 session and accepted
early data. This setting makes the reconstruction experiment deterministic; it
is not a deployment recommendation.
The optional PSK modes make the resumption dependencies executable. The
psk-only child has no fresh DH contribution; the default psk-dhe child has
one. --tls13-grandchild captures a third connection using a ticket issued by
the child. external-psk generates an independently provisioned PSK and stores
only its explicitly modeled later-compromise copy as a recovery input; the live
command-line value is redacted from the manifest.
The two phases can also run independently:
python3 hndl.py -p tls13 -m 1rtt --capture-only
python3 hndl.py recover data/2025-...-tls13-1rtt-captureRecovery reads the protocol, mode, capture port, archives, simulated-recovery
input, comparison-only state, and expected outputs from manifest.json.
--decrypt-only remains available for compatibility; any supplied protocol,
mode, or port is treated as an assertion and must agree with the manifest.
Build standalone archive-policy experiments from a retained capture with:
python3 hndl.py archive data/2025-...-tls13-1rtt-capture --policy all
python3 hndl.py recover data/policy-archives/2025-...-compactEach generated archive contains the retained wire representation and declared
future-recovery input (simulated asymmetric recovery or external-PSK
compromise), but no SSL key log, SSH ground truth, or other endpoint secret. The
raw policy retains the PCAP, reassembled
retains ordered TCP payloads or UDP datagrams, and compact retains
objective-specific protocol units visible at collection time. Their manifests
record exact byte counts, source and implementation hashes, and the evidence
boundary. The compact result is an achieved sufficient representation for the
controlled application marker, not a proof of a universal minimum.
MinARX is the protocol-aware sufficient archive used for the paper's byte table. It separates a compressible protocol-layout region from verbatim protected/application bytes, reconstructs decoder-compatible PCAPs, and keeps future-recovery material outside the archive and its byte count:
python3 -m minarx profiles
python3 -m minarx compact --capture-dir data/<capture> \
--protocol tls13 --mode 1rtt --profile tls13-full-aes256gcm \
--output data/session.minarx
python3 -m decryptor.derive --compacted data/session.minarx \
--recovery-dir data/<capture>Run the paper experiment over fresh, manifest-bound captures with:
python3 scripts/minarx_experiment.py data/<capture> [...] \
--output-root data/minarx-resultsThe command fails unless every reconstructed archive authenticates the
controlled application marker without a comparison key log. It writes
hash-bound JSON/CSV with raw capture, transport, opaque, layout, compression,
and final archive byte counts. The checked reference run is
analysis/results/minarx_live_2026-09-22.{json,csv}. Format semantics and the
reason savings are trace-shape dependent are documented in
docs/minarx_format.md.
For a development check of every supported mode, run:
python3 scripts/smoke_test.py
python3 scripts/smoke_test.py --archive-policiesThis performs fresh TLS 1.2 RSA, TLS 1.3 1-RTT, genuine TLS 1.3 0-RTT,
TLS 1.3 external-PSK, QUIC, and forced-rekey SSH captures on dynamically
selected loopback ports. It
requires the binaries from the setup section and working dumpcap permissions.
The test fails on an incomplete pipeline, missing or empty evidence, packet
drops, an SSH run with fewer than two authenticated transport epochs, or a
leaked capture/server process. Artifacts are created outside the repository
and removed after a complete pass; --keep retains them, and --verbose
prints each pipeline's output. Each mode has a 90-second timeout by default;
use --timeout-seconds on unusually slow systems.
Run the complete four-case PSK causality matrix with one command:
python3 scripts/psk_matrix_test.pyIt creates three live captures covering a pure-PSK child, a PSK-DHE child with
early data and a ticket-using grandchild, and an independent external PSK. It
checks positive recovery without endpoint ground truth and negative recovery
when each required PSK, fresh DH output, or child ticket state is withheld or
substituted. Use --keep or --output-root to retain the JSON evidence.
Measure the defender-side cost of aggressive OpenSSH rekeying with:
./scripts/build_openssh.sh -p "$PWD/openssh-benchmark" -c -S -b -i
python3 scripts/ssh_rekey_benchmark.py \
--rekey-limits default 64K --rtt-ms 0 20 80 --repetitions 5The matched matrix runs bulk and interactive workloads against the pinned
OpenSSH build and records client CPU, server CPU, throughput, first-byte time,
interactive latency, transfer gaps, context switches, and observed rekey
counts. It alternates limit order between repetitions and writes raw
samples.csv, aggregated summary.csv, per-run logs, and a hash-bound
results.json under data/ by default. Nonzero nominal RTT uses a portable
user-space TCP stream relay with half the requested delay in each direction;
the relay applies delay per forwarded socket read and is explicitly recorded
as an emulation, not presented as kernel-level netem measurement. This
benchmark requires /usr/bin/time and a second, uninstrumented OpenSSH build,
but not packet-capture permission: the capture build prints evidence on every
exchange and would bias aggressive-rekey timing. The benchmark rejects those
hooks by default;
--allow-instrumented --openssh-dir ./openssh/.local exists only for short
harness checks and produces results marked ineligible for paper timing.
To rerun archive policies over retained captures with isolated negative controls and machine-readable JSON/CSV output, use:
python3 scripts/archive_policy_test.py data/<capture> [...] --output-root results --keepFor every policy this withholds all ground truth, authenticates the application marker, flips one retained bit, withholds the simulated recovery result, and checks that transient decoder adapters have been removed. For each compact archive it also corrupts retained ciphertext and recomputes the outer manifest hash; recovery must still fail at the protocol-authentication boundary.
Output lands in data/<timestamp>-<protocol>-capture/ with subdirectories
pcap/, keys/, logs/, and derived/. TLS/QUIC derived secrets use NSS
keylog format. SSH writes a JSON recovery record containing its reconstructed
keys, authenticated-packet counts, oracle-release trace, and recovered channel
data. Every capture writes manifest.json with reproduction commands (secret
arguments are redacted), versions, machine details, the evidence boundary, and
SHA-256 hashes of the relevant code, binaries, and evidence. Successful
recovery also writes derived/recovery_provenance.json, binding the result to
hashes of the source manifest, passive archive, simulated quantum output,
comparison-only inputs, and recovery implementation.
The pipeline has three stages. First, capture/ runs a local client and server
while dumpcap records the wire. Invoking the capture writer directly ensures
that it has stopped and finalized the PCAP before analysis; tshark then
dissects the retained file. The SSH experiment additionally records the two
exact byte streams through a transparent loopback relay; this keeps the
protocol reconstruction test runnable on hosts where dumpcap lacks capture
permission. Second, decryptor/ consumes the simulated asymmetric-recovery
output and the passive archive. Third, it reconstructs the protocol key
schedule and proves success through authenticated application-data recovery.
TLS and QUIC success requires authenticated recovery of the known application
request or response. When a comparison-only key log is present, every expected
derived secret must also match it. Policy archives deliberately omit that log;
successful authenticated plaintext is the independent recovery proof. A
handshake-only result or absent plaintext marker remains a failure.
Reassembled and compact archives are decoded through a short-lived synthetic PCAP envelope so the same protocol decoders test every policy. Synthetic link, IP, TCP, and UDP headers are never stored in the archive, are excluded from its byte count, and are deleted after recovery. Compact pruning is conservative: TLS 1.3 ciphertext is retained when its handshake/application role is not visible at collection time, QUIC retains bytes needed for header protection, and SSH retains direction-separated streams because the tested ChaCha20 cipher encrypts packet lengths. Consequently compact output need not be smaller than reassembled output for every small capture.
The public Python API uses ExperimentConfig, CaptureResult, and
RecoveryResult from experiment.py. Capture and recovery dispatch directly
in-process: the exact returned capture directory is used, so concurrent runs
do not race through a “latest directory” lookup. A scope-bound process
lifecycle finalizes registered dumpcap writers and terminates registered
clients or servers after exceptional exits.
The implemented classical test configurations are deliberately narrow:
X25519 with TLS 1.3/QUIC, RSA key transport with AES128-SHA for TLS 1.2, and
Curve25519 plus chacha20-poly1305@openssh.com for SSH. The QUIC native
Handshake decoder supports AES-GCM cipher suites; the capture CLI rejects
non-X25519 groups. These boundaries are experiment scope, not claims about all
possible protocol configurations.
The SSH experiment deliberately forces curve25519-sha256,
ssh-ed25519, and chacha20-poly1305@openssh.com; it does not claim these are
current OpenSSH defaults. The decoder parses both public KEXINIT messages and
Curve25519 shares, recomputes the SSH mpint shared secret and exchange hash,
verifies the host signature, derives all RFC 4253 keys, verifies every
Poly1305 tag, and recovers SSH_TEST_OK. The endpoint-computed shared secret,
exchange hash, session ID, and derived keys live in
ssh_ground_truth.json and are comparison-only.
For a forced-rekey run, every hook event is retained. The attack process cannot read the private-value file directly: a separate oracle process releases a scalar only when queried with the matching public share. The decoder obtains a later share only after authenticating and decrypting the preceding transport epoch, records that release order, keeps the initial exchange hash as the SSH session identifier, and then advances to the next epoch. Removing the ground-truth file does not affect recovery.
hndl.py Main entrypoint
experiment.py Typed config/results and manifest validation
archive_policy.py Standalone raw/reassembled/compact archive policies
pyproject.toml Dependencies, console entry point, formatting/tests
capture/
capture.py Capture CLI
tls13/ Full, resumed, grandchild and external-PSK capture
tls12/ RSA key exchange capture
quic/ QUIC (TLS 1.3 over UDP) capture
ssh/ SSH (Curve25519) capture
decryptor/
derive.py Derivation CLI
tls13/ Full, early-data, resumed and external-PSK derivation
tls12/ derive_rsa.py
quic/ derive_quic.py
ssh/ derive_ssh.py
core/ Cryptographic primitives (HKDF, PRF, key schedule)
io/ PCAP parsing, key material I/O
analysis/ Cost models, mitigation experiments, figures
results/ CSV data from experiments
scripts/ Builds, integration tests, and SSH rekey benchmark
tests/ Fast public crypto, manifest, archive and cleanup tests
patches/ Source patches
analysis/validate_model.py performs the advertised four-protocol payload
sweep, including QUIC. It rejects failed or incomplete application transfers
and excludes pure TCP ACKs while retaining UDP frames. The analytical storage
curves are engineering models calibrated to the stated capture policy; they
are not information-theoretic lower bounds. analysis/monte_carlo_cost.py
charges the full retained inventory at each calendar year's recurring unit
price. Its inputs are observed traffic bytes, not application plaintext.
The capture CSV files under analysis/results/ are prior measurements, not
generated fixtures. Regenerate them after capture-harness or model changes
before using their numerical values in the paper; the scripts fail rather than
silently recording zero-byte captures when dumpcap cannot read the interface
or tshark cannot reopen an artifact.
On Ubuntu systems whose AppArmor profile permits dumpcap to write a capture in
the project but prevents tshark from reopening it, the tshark helper retries
from a private, short-lived directory under /tmp. The original PCAP remains
the evidence artifact and is never modified; auxiliary key logs are staged
with mode 0600 only for that subprocess invocation.
From this repository root, with the analysis dependencies installed:
python3 analysis/monte_carlo_cost.py --outdir analysis/figures
python3 analysis/quantum_work.py
python3 analysis/plot_rekey_counts.py --outdir analysis/figures
python3 -m pytest tests/test_cost_models.pyThe Monte Carlo command writes one figure, a percentile CSV, and its complete configuration to analysis/results/. It uses 10,000 draws and seed 42. Traffic units are decimal. The reference USD 0.00099 per billing GiB-month converts to USD 11.0641 per decimal TB-year. Every retained cohort pays the current calendar year's price. New cohorts receive a full acquisition-year charge and remain until the common horizon. Rates are drawn once per scenario and held fixed; percentiles describe the chosen scenarios, not forecast confidence.
The baseline retains one archived byte per selected observed byte. Use --retention-ratio to explore another archive policy, --traffic-zb for a different annual traffic reference, or --storage-cost for a different decimal TB-year price. Application-payload overhead is not applied to observed traffic. The model excludes interception, retrieval, request charges, object metadata, extra copies, and quantum hardware. The old media-purchase branch is removed: one-time media purchases are not comparable to recurring capacity charges.
The quantum accounting CSV compares 37 independent jobs with a chain of 37 dependent recoveries, as in passive SSH rekey recovery. Times are normalized to one job, not predicted hours on future hardware. The rekey plot marks recorded SSH counts without fitting a plaintext threshold to RekeyLimit. Its TLS step curves follow the configured application byte intervals.
To reproduce the manuscript figures in the enclosing research workspace, pass --outdir ../paper/figures/review_2 to the storage and rekey plot commands. The original submission's figures remain separate.
black .Install the development tools and run the fast published tests with:
python3 -m pip install -e '.[dev,analysis]'
python3 -m unittest discover -s tests -v