Quantum keys can fail quietly—loss and noise can leave you with bits, but no secrecy. We model the cliff to expose silent breakage before it becomes a system risk.
Abstract. In satellite QKD operations, the key failure mode is “bits without secrecy.” We propose and validate a telemetry-to-secrecy pipeline that maps pass-time observables and forecast proxies, together with link dynamics, to finite-key secret-key-rate lower bounds (with explicit ε-budget allocation), yielding actionable go/no-go and sensitivity surfaces.
Featured result (post-pass): Secret-key rate and QBER vs channel loss; finite-key variants quantify confidence/ε-budget penalties.
Run ./py -m sat_qkd_lab.run sweep to produce plots in figures/.
This framework supports pass-time quality telemetry and post-pass security distillation under delayed-classical CONOPS (qubits downlinked during pass; reconciliation later over RF). During the pass, use pass-sweep and forecast-run to estimate viability from observables available in the downlink; compute QBER post-pass via replay and the experiment harness inputs.
- Quickstart
- Operator Workflows
- CLI Examples
- Layout
- Problem
- Core Claim
- Glossary (operational definitions)
- Physics/Math Background
- Method
- Approach
- Implementation
- Capabilities
- Results
- Verification / Golden checks
- Interpretation
- Why it matters
- Architecture
- Physical Assumptions (Link Model Card)
- Topics
- References
- Citations
- License
- Contact
Repo-local runners are ./py and ./pytest. If present, make them executable and verify they exist before proceeding (fail fast if they are missing).
git clone https://github.com/christopher-altman/sat-qkd-security-curves
cd sat-qkd-security-curves
[ -f ./py ] && chmod +x ./py
[ -f ./pytest ] && chmod +x ./pytest
test -x ./py || { echo "Missing ./py runner; see repo-local runners section." >&2; exit 1; }
test -x ./pytest || { echo "Missing ./pytest runner; see repo-local runners section." >&2; exit 1; }
PY=python3
$PY -V # must be >= 3.10
test -x .venv/bin/python || $PY -m venv .venv
./py -m pip install -U pip
./py -m pip install -e ".[dev]"
./py -m sat_qkd_lab.run --help
./pytest -qPurpose: Role-based workflows with reproducible commands, expected outputs, and explicit pass/fail gates.
Goal: Sanity-check link sensitivity (loss, pointing, turbulence/fading) and spot the security cliff.
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000Outputs:
figures/key_qber_vs_loss.png— QBER vs channel lossfigures/key_fraction_vs_loss.png— Secret fraction vs lossreports/latest.json— Full sweep metrics
What to look for:
- Monotonic QBER increase with loss (background/dark clicks dominating at high loss)
- Secret fraction cliff where QBER crosses abort threshold (~11%)
- Key rate per pulse approaching zero at the security boundary
./py -m sat_qkd_lab.run pass-sweep --max-elevation 70 --pass-duration 300 --turbulence --sigma-ln 0.3Outputs:
figures/key_rate_vs_elevation.png— Key rate over pass elevation profilefigures/loss_vs_elevation.png— Link loss budget vs elevationfigures/secure_window_per_pass.png— Secure window timingreports/latest_pass.json— Pass time-series and summary
What to look for:
- Elevation-dependent loss (airmass scaling)
- Turbulence-induced QBER variance (fading effects)
- Secure window fragmentation (outage events)
# Night-time baseline
./py -m sat_qkd_lab.run pass-sweep --max-elevation 70 --pass-duration 300
# Day-time (100x background increase)
./py -m sat_qkd_lab.run pass-sweep --max-elevation 70 --pass-duration 300 --day --day-bg-factor 100Outputs:
- Same as Workflow 2 (figures +
reports/latest_pass.json)
What to look for:
- Background-induced QBER floor in daytime scenario
- Reduction in secure window duration and total secret bits
- Shift in viability threshold (minimum usable elevation angle)
Goal: Show how finite-key penalties and security parameters change viability.
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000Outputs:
figures/key_qber_vs_loss.pngfigures/key_fraction_vs_loss.pngreports/latest.json
What to look for:
- Asymptotic secret fraction (≈ 1 − 2·h(QBER) for BB84)
- Abort threshold crossing (QBER > ~11%)
- Key rate per pulse scaling with sifted fraction
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 500000 --finite-key --eps-pe 1e-10 --eps-sec 1e-10 --eps-cor 1e-15Outputs:
figures/finite_key_comparison.png— Asymptotic vs finite-key ratesfigures/finite_size_penalty.png— Penalty factor vs lossfigures/finite_key_bits_vs_loss.png— Extractable secret bitsreports/latest.json(includesfinite_key_sweepfields)
What to look for:
- Finite-key penalty (gap between asymptotic and finite-key rates)
- Penalty scaling with block size (larger blocks → smaller penalty)
- Security boundary shift (finite-key rate hits zero earlier than asymptotic)
- Epsilon budget allocation (eps_pe + eps_sec + eps_cor = eps_total)
./py -m sat_qkd_lab.run attack-sweep --loss-min 20 --loss-max 50 --steps 9 --pulses 50000Outputs:
figures/attack_comparison_key_rate.png— Key rates under different attack modelsreports/latest.json(includesattack_sweepresults)
What to look for:
- Intercept-resend signature (QBER spike, key rate collapse)
- PNS impact on multi-photon pulses
- Attack detectability via QBER monitoring (deviation from expected noise floor)
Goal: Show detector/dark/background sensitivity and its effect on QBER and viability.
# High efficiency (eta = 0.4)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000 --eta 0.4
# Low efficiency (eta = 0.1)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000 --eta 0.1Outputs:
figures/key_qber_vs_loss.pngfigures/key_fraction_vs_loss.pngreports/latest.json
What to look for:
- Lower eta → fewer sifted bits at the same loss
- Lower eta → background-dominated regime at lower loss values
- Detection rate vs QBER tradeoff
# Low background (p_bg = 1e-6)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000 --p-bg 1e-6
# High background (p_bg = 1e-3)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000 --p-bg 1e-3Outputs:
figures/key_qber_vs_loss.pngfigures/qber_headroom_vs_loss.pngreports/latest.json
What to look for:
- High p_bg → QBER floor even at low loss
- Background-limited secure distance (maximum tolerable loss)
- Headroom reduction (distance to abort threshold)
./py -m sat_qkd_lab.run decoy-sweep --loss-min 20 --loss-max 50 --steps 16 --pulses 200000Outputs:
figures/decoy_key_rate_vs_loss.png— Decoy-state key ratereports/latest.json(includesdecoy_sweepresults)
What to look for:
- Single-photon contribution bounds (vacuum + weak decoy protocol)
- PNS resilience (decoy vs standard BB84 comparison)
- Intensity modulation effects (signal vs decoy vs vacuum)
Goal: Minimal cognitive load: run one command, read two numbers, decide go/no-go.
./py -m sat_qkd_lab.run pass-sweep --max-elevation 70 --pass-duration 300Outputs:
figures/key_rate_vs_elevation.pngfigures/secure_window_per_pass.pngreports/latest_pass.json
What to look for:
- Open
figures/key_rate_vs_elevation.pngand confirm positive key rate during the pass - Open
reports/latest_pass.jsonand readsummary.total_secret_bits(example target: > 1e6 bits) - Go/no-go: total_secret_bits > threshold AND QBER < ~11%
# Install dashboard dependencies (one-time)
./py -m pip install -e ".[dashboard]"
# Launch dashboard
./py -m sat_qkd_lab.dashboardAccess: Open browser to http://localhost:8501
What to look for:
- Interactive controls for loss, detector params, finite-key settings
- Live plots update as parameters change
- Summary metrics panel (QBER, key rate, abort status)
- Compare tab: Select two
reports/assumptions.jsonfiles to compute a structured diff (added/removed/changed parameters). - Export packets: Create timestamped bundles of reports + plots + assumptions snapshots.
Assumptions snapshot: reports/assumptions.json is written alongside reports/latest.json so you can reproduce runs and diff parameter changes.
All commands accept --help to display available flags, defaults, and usage examples:
./py -m sat_qkd_lab.run sweep --help
./py -m sat_qkd_lab.run pass-sweep --helpQuick run examples:
# BB84 sweep (simulated)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 200000
# Assumptions manifest (single source of truth)
./py -m sat_qkd_lab.run assumptions
# Mission narrative (simulated)
./py -m sat_qkd_lab.run mission
# Replay a prior sweep report without drift
./py -m sat_qkd_lab.run replay --report reports/latest.json --outdir .
# Finite-key sweep (simulated)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21 --pulses 500000 --finite-key --eps-pe 1e-10 --eps-sec 1e-10 --eps-cor 1e-15
# Decoy-state sweep (simulated)
./py -m sat_qkd_lab.run decoy-sweep --loss-min 20 --loss-max 50 --steps 16 --pulses 200000
# Attack comparison sweep (simulated)
./py -m sat_qkd_lab.run attack-sweep --loss-min 20 --loss-max 50 --steps 9 --pulses 50000
# Pass sweep with atmosphere flags (simulated)
./py -m sat_qkd_lab.run pass-sweep --max-elevation 70 --pass-duration 300 --day --turbulence --sigma-ln 0.3
# Experiment harness (help for inputs)
./py -m sat_qkd_lab.run experiment-run --help
# Forecast harness (help for inputs)
./py -m sat_qkd_lab.run forecast-run --help
# Calibration fitting (help for inputs)
./py -m sat_qkd_lab.run calibration-fit --help
# Constellation scheduler (help for inputs)
./py -m sat_qkd_lab.run constellation-sweep --help
# Clock sync estimator (help for inputs)
./py -m sat_qkd_lab.run clock-sync --help
# Sync estimate helper (help for inputs)
./py -m sat_qkd_lab.run sync-estimate --help
# OU fading simulator (help for inputs)
./py -m sat_qkd_lab.run fading-ou --help
# Basis bias simulator (help for inputs)
./py -m sat_qkd_lab.run basis-bias --help
# Coincidence simulation (simulated)
./py -m sat_qkd_lab.run coincidence-sim --loss-min 20 --loss-max 60 --steps 9 --outdir ..
├── pyproject.toml
├── README.md
├── .github
│ └── workflows
│ └── ci.yml
├── src
│ ├── main.py
│ └── sat_qkd_lab
│ ├── __init__.py
│ ├── attacks.py
│ ├── background_process.py
│ ├── basis_bias.py
│ ├── bb84.py
│ ├── calibration_fit.py
│ ├── calibration.py
│ ├── change_points.py
│ ├── clock_sync.py
│ ├── coincidence.py
│ ├── constellation.py
│ ├── dashboard.py
│ ├── decoy_bb84.py
│ ├── detector.py
│ ├── eb_observables.py
│ ├── eb_qkd.py
│ ├── event_stream.py
│ ├── experiment.py
│ ├── fading_samples.py
│ ├── fim_identifiability.py
│ ├── finite_key.py
│ ├── forecast_harness.py
│ ├── forecast.py
│ ├── free_space_link.py
│ ├── helpers.py
│ ├── hil_adapters.py
│ ├── link_budget.py
│ ├── optical_link.py
│ ├── optics.py
│ ├── ou_fading.py
│ ├── pass_model.py
│ ├── plotting.py
│ ├── pointing.py
│ ├── polarization_drift.py
│ ├── run.py
│ ├── scoring.py
│ ├── sweep.py
│ ├── telemetry.py
│ ├── timetags.py
│ ├── timing.py
│ └── windows.py
└── tests
├── conftest.py
├── test_adversary_system_realism.py
├── test_attack_surfaces.py
├── test_background_process.py
├── test_basic.py
├── test_basis_bias.py
├── test_bb84_protocol.py
├── test_bell_visibility_matrix.py
├── test_calibration_fit.py
├── test_calibration_model_card.py
├── test_calibration_residual_diagnostics.py
├── test_calibration.py
├── test_change_points.py
├── test_clock_sync.py
├── test_constellation.py
├── test_dashboard_helpers.py
├── test_decoy_realism.py
├── test_detector_attacks.py
├── test_detector_effects.py
├── test_eb_observables.py
├── test_eb_pass_experiment.py
├── test_engineering_outputs.py
├── test_event_stream_pipeline.py
├── test_fading_model.py
├── test_fading_samples.py
├── test_golden_curves.py
├── test_fdr_correction.py
├── test_fim_identifiability.py
├── test_finite_key_composable.py
├── test_forecast_harness.py
├── test_helpers_validation.py
├── test_hil_adapters.py
├── test_json_contract_stability.py
├── test_link_coincidence.py
├── test_motion_polarization_rotation.py
├── test_optical_link_integration.py
├── test_optics.py
├── test_ou_fading.py
├── test_parameter_validation.py
├── test_pass_sweep_ou_fading.py
├── test_pass_sweep_pulse_accounting.py
├── test_plotting_headroom_and_sweep_engineering.py
├── test_plotting_scale_settings.py
├── test_pointing_dynamics.py
├── test_polarization_drift.py
├── test_property_based.py
├── test_security_analysis.py
├── test_sync_estimate_command.py
├── test_sync_leakage_firewall.py
├── test_timetag_coincidence.py
├── test_timing_sync_layer.py
├── test_timing_timetags.py
└── test_windows_experiment.py
Satellite and fibre QKD systems live or die on a simple fact: bits can still flow while secrecy collapses. This repo is a reproducible “security curve” lab that translates link conditions (loss, background, detector effects, timing, and attack models) into security viability—so quiet failure becomes visible as an engineering threshold.
Operationally, it supports delayed-classical CONOPS: during a satellite pass, only qubits may be downlinked; basis reconciliation and key distillation can occur later via RF. In that setting, pass-time outputs are quality/viability proxies and forecasts, while QBER and key metrics are computed post-pass once classical context is available.
If we model loss, noise, and a simple active attack under instrument-realistic constraints, we see an operational security cliff: QBER crosses a threshold where privacy amplification yields zero secret key even while bits still flow. This lab exists to make that cliff measurable and repeatable as an engineering decision boundary.
- QBER (Quantum Bit Error Rate): Fraction of mismatched bits after sifting (range: 0 to 0.5). High QBER → low secrecy. Abort threshold: typically 11% for BB84.
- Secret fraction: Fraction of sifted bits remaining after privacy amplification (range: 0 to 1). Zero means no extractable key.
- Key rate per pulse: Secret bits generated per sent pulse (units: bits/pulse). Accounts for sifting factor (~0.5 for BB84) and secret fraction.
- Finite-key penalty: Gap between asymptotic and finite-key rates due to statistical uncertainty (larger blocks → smaller penalty).
- Abort threshold: QBER level where privacy amplification can no longer extract secrecy (11% for BB84, protocol-dependent).
- Headroom: Distance to abort threshold (qber_abort - qber_mean). Positive headroom → margin for noise fluctuations.
Transmittance η is the probability a photon survives the channel and reaches the receiver.
QBER e is the bit error rate after sifting, so it captures the net disturbance of the link.
Binary entropy h(e) expresses uncertainty per bit (0 to 1), and it rises quickly with noise.
For BB84 (and similarly for E91 after sifting), the asymptotic secret fraction decreases as e grows and vanishes as e → 0.5.
That's the security cliff: detections and sifted bits can remain nonzero while secrecy collapses.
Realism primitives (units explicit): timing jitter σ is typically 10–200 ps for SPDs and stored in seconds in code/JSON; coincidence window τc is sub‑ns and stored in seconds; fading variance Var(T) captures heavy‑tailed, time‑correlated free‑space optics where rare deep fades dominate.
Detector background p_bg is defined as a probability per pulse / per detection gate (not per second).
- Where signal comes from: Monte Carlo simulation of QKD protocol events (BB84, decoy-state BB84, and E91) plus (optional) pass-time link models that generate observables available during downlink; post-pass replay computes QBER, Bell/visibility witnesses (for E91), and security metrics once reconciliation context is provided.
- Why this architecture/data was chosen: BB84 is the canonical baseline; decoy-state BB84 is the practical workhorse for weak coherent sources (bounding single-photon contributions under PNS-style threats); E91 covers entanglement-based QKD where coincidence statistics and Bell/visibility witnesses provide additional operational structure. Loss in dB is the lingua franca of link budgets (fibre and free-space).
- What is being experimentally compared:
- post-pass security curves (QBER and key rate) across loss/noise/attack assumptions, by protocol family (BB84/decoy/E91)
- pass-time observables and viability proxies across elevation/time (pass sweeps), including coincidence quality (e.g., CAR/visibility) for E91-style links
- finite-key vs asymptotic security to prevent “average looks fine” self-deception
- Abort headroom is
qber_abort − qber_mean, the distance to the protocol's error threshold, with headroom treated as absolute QBER (e.g., 0.05 means a 5% margin to an 11% abort threshold). - CAR (coincidence‑to‑accidental ratio) is a signal‑quality witness in entanglement‑based QKD.
- Elevation profiles map to time‑varying loss and background during pass sweeps.
- Finite‑key penalties cap what can be claimed with limited samples, even when averages look good.
- Expected‑value sweeps, pass‑sweep time series, and the experiment/forecast harnesses are tied together under a single operational discipline.
- Scope: BB84/decoy, E91, CV-QKD (GG02 scaffold), finite-key, loss/noise sweeps, attack comparisons, satellite/free-space link models.
- Python package under
/src/sat_qkd_lab - CLI:
./py -m sat_qkd_lab.run <command> --help - Replay sweeps without drift:
./py -m sat_qkd_lab.run replay --report reports/latest.json --outdir . - Requirements:
pyproject.toml(numpy, matplotlib)
Detector/Background Model — The simulation includes a realistic detector model with:
eta: Detection efficiency (default 0.2)p_bg: Background/dark click probability per pulse (default 1e-4)
At high channel loss, background clicks dominate over signal clicks, causing QBER to approach 0.5. This models the operationally critical effect where the signal-to-noise ratio degrades.
Monte Carlo Confidence Intervals — Run multiple trials per loss value to obtain uncertainty estimates:
--trials N: Number of independent trials (enables CI when N > 1)--workers N: Parallel workers for faster execution--seed N: Base random seed for reproducibility
Outputs include mean, standard deviation, and 95% CI bounds for QBER, secret fraction, and key rate per pulse. CI bounds are clamped to physical ranges (e.g., QBER ≤ 0.5, key rate ≥ 0).
Key Rate Per Pulse — The simulation outputs key_rate_per_pulse, which properly accounts for the BB84 sifting factor (~1/2). This gives the asymptotic key rate normalized to sent pulses:
key_rate_per_pulse = (n_sifted / n_sent) × secret_fraction
Decoy-State BB84 — The decoy-sweep command implements vacuum + weak decoy protocol:
- Uses three intensities (signal, decoy, vacuum) to bound single-photon contributions
- Computes asymptotic key rate using standard decoy-state bounds
- Demonstrates PNS attack resilience (motivation for real satellite QKD)
References:
- Lim et al., Concise security bounds for practical decoy-state quantum key distribution. (Physical Review A 2014)
CV-QKD (GG02) Scaffold — The cv-sweep command provides a structural scaffold for Continuous-Variable QKD using Gaussian-modulated coherent states:
- Computes SNR and mutual information I(A:B) vs channel loss
- Holevo bound computation is stubbed (returns None)
- Secret key rate is not yet validated (requires full Holevo bound)
- Status: model / scaffold — NOT for production security claims
This is a minimal demonstration of CV-QKD interfaces and assumptions manifest integration. See docs/12_cvqkd_scaffold.md for validation gates required before production use.
Input Validation — CLI arguments are validated post-parse with clear error messages:
validate_int(name, value, min_value, max_value)— Integer bounds checkingvalidate_float(name, value, min_value, max_value, allow_nan, allow_inf)— Float bounds with NaN/inf controlvalidate_seed(seed)— Ensures seed is None or non-negative integer
Invalid inputs raise ValueError with the parameter name and invalid value.
Abort Handling — When QBER exceeds the abort threshold (default 11%), the protocol aborts and:
secret_fractionis set to 0.0 (no key extractable)n_secret_estis set to 0key_rate_per_pulsereturns 0.0
This ensures aborted trials contribute zero to aggregated statistics.
Link Budget — The link_budget.py module is an Option A scenario generator that maps elevation angles to loss values for demonstration. It is not a physically accurate optical link model. See optical_link.py for documentation of what a proper Option B model would require.
Link budget mapping is illustrative unless using Option B model.
Finite-Key Analysis — The --finite-key flag enables finite-size security analysis using Hoeffding-type concentration bounds. This computes conservative secret key length estimates that account for statistical uncertainty in parameter estimation and explicit ε-budgeting:
--eps-pe: Parameter estimation failure probability (default 1e-10)--eps-sec: Secrecy failure probability (default 1e-10)--eps-cor: Correctness failure probability (default 1e-15)--ec-efficiency: Error correction efficiency factor (default 1.16)--f-ec: Alias for--ec-efficiency--pe-frac: Fraction of sifted bits used for parameter estimation (default 0.5)--m-pe: Explicit parameter estimation sample size (overrides--pe-frac)--n-sent: Total pulses sent (overrides--pulseswhen set)--rep-rate+--pass-seconds: Alternative pulse accounting, withn_sent = rep_rate * pass_seconds
The finite-key rate is always ≤ the asymptotic rate, with the penalty decreasing as block size increases. The analysis uses:
- Hoeffding bounds for QBER estimation uncertainty
- Explicit error-correction leakage and ε-penalty terms
- Total security parameter:
eps_total = eps_pe + eps_sec + eps_cor
Finite-key math (toy-but-recognizable BB84 bound):
h2(q) = -q*log2(q) - (1-q)*log2(1-q), with q clamped into [1e-12, 1-1e-12]
delta = sqrt( ln(1/eps_pe) / (2*m_pe) )
qber_upper = min(0.5, q_hat + delta)
leak_ec_bits = f_ec * n_sifted * h2(qber_upper)
delta_eps_bits = 2*log2(2/eps_sec) + log2(2/eps_cor)
ell_bits = n_sifted * max(0, 1 - 2*h2(qber_upper)) - leak_ec_bits - delta_eps_bits
key_rate_per_pulse_finite = ell_bits / n_sent
References:
- Tomamichel et al., “Tight finite-key analysis for quantum cryptography” (Nature Communications 2012)
- Lim et al., “Concise security bounds for practical decoy-state QKD” (Physical Review A 2014)
Free-Space Optical Link Model — The pass-sweep command provides a physically-grounded free-space link budget for satellite-to-ground QKD:
- Diffraction/Coupling Model: Gaussian beam propagation with diffraction-limited divergence (
θ = 1.22λ/D) and receiver aperture coupling efficiency - Pointing Error/Jitter: Rayleigh-distributed angular error with configurable
--sigma-point(default 2 µrad) - Atmospheric Extinction: Kasten-Young airmass formula with zenith loss scaling by elevation
- Turbulence/Scintillation: Lognormal fading model (
--turbulenceflag,--sigma-lnparameter) - Day/Night Background:
--dayflag increases background noise by--day-bg-factor(default 100×)
# Night-time pass simulation (default)
./py -m sat_qkd_lab.run pass-sweep --max-elevation 70 --pass-duration 300
# Day-time pass with turbulence
./py -m sat_qkd_lab.run pass-sweep --day --turbulence --sigma-ln 0.3
# Custom optical parameters
./py -m sat_qkd_lab.run pass-sweep --tx-diameter 0.15 --rx-diameter 0.5 --altitude 600e3References:
- Liao et al., Satellite-to-ground quantum key distribution. (Nature 2017)
- Bourgoin et al., A comprehensive design and performance analysis of low Earth orbit satellite quantum communication. (NJP 2013)
The core simulation functions are:
sat_qkd_lab.bb84.simulate_bb84()— Single-photon BB84 Monte Carlo simulationsat_qkd_lab.decoy_bb84.simulate_decoy_bb84()— Decoy-state BB84 with vacuum + weak decoysat_qkd_lab.free_space_link.total_link_loss_db()— Free-space link loss from elevation anglesat_qkd_lab.sweep.sweep_pass()— Satellite pass simulation with free-space link model
Both return structured results with QBER, secret fraction, and key rate metrics.
# Single-trial BB84 sweep (fast)
./py -m sat_qkd_lab.run sweep --loss-min 20 --loss-max 60 --steps 21
# BB84 sweep with detector model and CI (10 trials per point)
./py -m sat_qkd_lab.run sweep --trials 10 --eta 0.2 --p-bg 1e-4
# Decoy-state BB84 sweep
./py -m sat_qkd_lab.run decoy-sweep --loss-min 20 --loss-max 50 --steps 16
# Finite-key analysis sweep
./py -m sat_qkd_lab.run sweep --finite-key --pulses 500000 --eps-pe 1e-10 --eps-sec 1e-10
# Finite-key rate vs total pulses (uses representative loss point)
./py -m sat_qkd_lab.run sweep --finite-key --rep-rate 1e7 --pass-seconds 30
# Free-space satellite pass sweep (night-time)
./py -m sat_qkd_lab.run pass-sweep --max-elevation 60 --pass-duration 300
# Free-space pass with turbulence and day-time background
./py -m sat_qkd_lab.run pass-sweep --day --turbulence --sigma-ln 0.3- Field names and filenames shown below are taken from current
reports/outputs; if you change schema or filenames, update this section in the same commit. - Sync protocol:
./py -m sat_qkd_lab.run sync-estimategeneratesreports/latest_sync_params.json(Planned if not present in this checkout).--sync-params <path>is accepted byexperiment-run,forecast-run, andcoincidence-sim. Inline sync is blocked unless--allow-inline-sync(or--allow-resync) is set. Coincidence reports includesync.locked,sync.source/sync.method,sync.offset_s(seconds),sync.drift_ppm, andsync.inline_sync_used. - Pass realism: enable
pass-sweep --fading-ouwith--fading-ou-mean,--fading-ou-sigma,--fading-ou-tau-s. Summary includessummary.fading.mean,summary.fading.var,summary.fading.corr_time_seconds, andsummary.outageswithcount,mean_duration_s, anddurations_s. Plots are generated on run (Planned if not present in this checkout):figures/fading_ou_time.png,figures/secure_window_fragmentation.png. - Polarization / basis bias: enable
pass-sweep --pol-rotation --pol-rotation-max-deg --basis-bias-max. Outputs includepol.rotation_angle_deg,basis.bias_mean,basis.bias_std, and time‑seriespol_rotation_deg/basis_bias. Plot generated on run (Planned if not present in this checkout):figures/pol_rotation_vs_elevation.png. - Calibration card:
calibration-fitwritesreports/latest_calibration_card.json(Planned if not present in this checkout) withmodel_card.fit.r2,model_card.fit.param_uncertainty,model_card.fim.cond,model_card.fim.identifiable, andmodel_card.identifiable. Warnings are emitted when identifiability is low. Plots generated on run (Planned if not present in this checkout):figures/calibration_quality_card.png,figures/calibration_residuals.png. - Assumptions manifest:
./py -m sat_qkd_lab.run assumptionsprints a stable JSON manifest;reports/latest.jsonnow embedsassumptions_manifestfor auditability. - Outputs (simulated):
reports/latest.jsonincludes results plusassumptions_manifest; all outputs are simulated under the assumptions manifest. - Blinding + JSON contract: append‑only JSON keys, required units (Hz, seconds, bits, bps, degrees where used), and no label leakage unless
--unblind. Tests coverreports/latest.json,reports/latest_pass.json,reports/latest_experiment.json,reports/forecast_blinded.json(Planned if not present), andreports/latest_sync_params.json(Planned if not present). - Finite-key boundary: outputs include
finite_key.bound,finite_key.status, andfinite_key.reason; insecure regimes return NO-SECRET-KEY with zero finite-key rates. - Dashboard: install with
./py -m pip install -e ".[dashboard]"and launch via./py -m sat_qkd_lab.dashboard(blinded by default). - Outputs (selected, present in this checkout):
reports/latest.json,reports/latest_pass.json,reports/latest_experiment.json,reports/latest_coincidence.json,reports/latest_background_process.json,reports/latest_optics_drift.json,reports/schedule_blinded.json; figures includefigures/key_qber_vs_loss.png,figures/key_fraction_vs_loss.png,figures/qber_vs_loss_ci.png,figures/secret_fraction_vs_loss_ci.png,figures/qber_headroom_vs_loss.png,figures/key_rate_vs_elevation.png,figures/secure_window_per_pass.png,figures/loss_vs_elevation.png,figures/car_vs_loss.png,figures/chsh_s_vs_loss.png,figures/visibility_vs_loss.png,figures/compensation_residuals.png. - JSON outputs (schema + stability): outputs are append‑only, require explicit units where applicable, and blinded outputs contain no labels unless
--unblind.{ "schema_version": "0.4", "generated_utc": "YYYY-MM-DDTHH:MM:SSZ", "pass_sweep": { "summary": { "qber_abort": 0.11 } }, "parameters": { "rep_rate": 1e8 } } - CI bounds: CI lower bounds are clamped to 0; QBER CI upper bounds are clamped to 0.5.
The model generates:
figures/key_qber_vs_loss.png— QBER vs channel lossfigures/key_fraction_vs_loss.png— Secret fraction vs channel loss (legacy alias:key_key_fraction_vs_loss.png)figures/qber_vs_loss_ci.png— QBER with 95% CI bands (when--trials > 1)figures/secret_fraction_vs_loss_ci.png— Secret fraction with 95% CI (legacy alias:key_rate_vs_loss_ci.png)figures/decoy_key_rate_vs_loss.png— Decoy-state key rate vs lossfigures/finite_key_comparison.png— Asymptotic vs finite-key rate (when--finite-key)figures/finite_key_bits_vs_loss.png— Extractable secret bits vs loss (when--finite-key)figures/finite_size_penalty.png— Finite-size penalty factor vs loss (when--finite-key)figures/finite_key_rate_vs_n_sent.png— Finite-key rate vs total pulses (when--finite-key)figures/key_rate_vs_elevation.png— Key rate vs elevation angle (whenpass-sweep)figures/secure_window_per_pass.png— Secure window timing over pass (whenpass-sweep)figures/loss_vs_elevation.png— Total link loss vs elevation (whenpass-sweep)reports/latest.json— Raw sweep metrics with schema version and timestamp
Figure 1. Estimated secret-key rate versus channel loss for BB84. (↑ featured figure) X-axis: channel loss (dB). Y-axis: key rate per pulse (bits/pulse). Simulated under the assumptions manifest. A scenario-generator sweep shows that link loss alone reduces detections but does not destroy secrecy until error rates dominate. Under an intercept–resend attack, the key rate per pulse collapses to zero at moderate loss even while sifted bits remain non-zero, revealing the security cliff where privacy amplification can no longer extract secrecy.
tests/test_golden_curves.pycontains fast deterministic assertions that the asymptotic secret fraction drops to zero near the BB84 abort threshold and that the satellite pass envelope key rate decreases (while QBER stays in [0, 0.5]) as equivalent link loss rises, ensuring the core physics invariants stay intact.
Figure 2. Quantum Bit Error Rate (QBER) versus channel loss for BB84. (↑ featured figure) X-axis: channel loss (dB). Y-axis: QBER (unitless). Simulated under the assumptions manifest. Without attack, QBER remains low until background/dark clicks dominate at high loss. An intercept–resend adversary injects a strong QBER signature, driving errors beyond the tolerable entropy budget. The plotted spikes illustrate the detectable breakage regime that classical throughput monitors would miss without explicit QBER estimation.
The output JSON includes these fields for CI sweeps:
{
"schema_version": "0.4",
"generated_utc": "2026-01-02T00:00:00Z",
"loss_sweep_ci": {
"no_attack": [{
"qber_mean": 0.025,
"qber_ci_low": 0.018,
"qber_ci_high": 0.032,
"secret_fraction_mean": 0.85,
"secret_fraction_ci_low": 0.80,
"secret_fraction_ci_high": 0.90,
"key_rate_per_pulse_mean": 0.0012,
"key_rate_per_pulse_ci_low": 0.0010,
"key_rate_per_pulse_ci_high": 0.0014,
"abort_rate": 0.0
}]
}
}For finite-key sweeps (--finite-key), the JSON includes additional fields:
{
"schema_version": "0.4",
"finite_key_sweep": {
"no_attack": [{
"loss_db": 25.0,
"qber": 0.025,
"qber_hat": 0.025,
"qber_upper": 0.032,
"ell_bits": 45000.0,
"key_rate_per_pulse_asymptotic": 0.0012,
"key_rate_per_pulse_finite": 0.0009,
"finite_size_penalty": 0.25,
"eps_total": 2.00001e-10,
"f_ec": 1.16,
"delta_eps_bits": 150.0
}]
},
"parameters": {
"finite_key": true,
"eps_pe": 1e-10,
"eps_sec": 1e-10,
"eps_cor": 1e-15,
"ec_efficiency": 1.16,
"pe_frac": 0.5,
"m_pe": null
}
}All CI lower bounds are clamped to 0; QBER CI upper is clamped to 0.5.
The curves show an engineering-facing “security budget.” Loss reduces detections (sifted key), while noise and attacks raise QBER. Once QBER rises enough, the secret-key rate per pulse collapses to zero: you can still exchange bits, but you cannot distill secrecy. This makes the QKD security story measurable and testable, rather than rhetorical.
Operators need to see where secrecy collapses long before it fails in the field. Loss alone reduces detections, but secrecy fails when error rates dominate; intercept–resend drives the secret-key rate to zero even when sifted bits remain. The curves show an engineering‑facing security budget: physics‑level behavior (loss/noise/attack) translates into operational metrics (QBER, secret‑key rate), which are imperative in integrated satellite + fiber QKD networks. This framework makes the cliff explicit and measurable.
The codebase operates in two complementary modes:
Simulation Mode (assumptions → curves): Configuration parameters and channel models flow through protocol simulators to produce security curves. Given loss profiles, detector parameters, and attack models, the system outputs QBER, secret fraction, and key rate metrics. This answers: “Given these assumptions, what is the security margin?”
Experiment/Telemetry Mode (observables → decisions): Pass-time observables are ingested, parameters are estimated, and finite-key security decisions are made post-pass. This answers: “Given these measurements, can we extract a secure key?”
flowchart LR
subgraph Config["Configuration"]
A[assumptions.py]
D[detector.py]
L[free_space_link.py]
end
subgraph Link["Link/Channel"]
FL[total_link_loss_db]
AT[atmosphere.py]
OU[ou_fading.py]
end
subgraph Protocol["Protocol"]
BB[bb84.py]
DC[decoy_bb84.py]
EB[eb_qkd.py]
end
subgraph Security["Security"]
FK[finite_key.py]
end
subgraph Output["Output"]
SW[sweep.py]
PL[plotting.py]
RN[run.py → JSON]
end
A --> RN
D --> BB
L --> FL
AT --> FL
OU --> FL
FL --> BB
FL --> DC
FL --> EB
BB --> SW
DC --> SW
EB --> SW
SW --> FK
FK --> SW
SW --> RN
RN --> PL
When extending the codebase, understanding where changes are safe is critical:
- Green zones (safe): Link budget geometry, atmospheric scenario models, plotting cosmetics, dashboard layout, adding tests
- Yellow zones (careful): Detector parameters, attack implementations, calibration fitting, timing estimation
- Red zones (core security):
finite_key.pybounds,bb84.pysifting logic,decoy_bb84.pysingle-photon estimators, epsilon budget arithmetic
For detailed module boundaries, extension points, and validation mapping, see the full Architecture & Dependency Contract.
The satellite-to-ground link model uses the following physical assumptions. Each parameter is traceable to code and CLI flags.
| Parameter | Default | Units | Code Source | CLI Override |
|---|---|---|---|---|
| Wavelength (λ) | 850 nm | m | free_space_link.py |
--wavelength |
| TX aperture (D_tx) | 0.30 m | m | free_space_link.py |
--tx-diameter |
| RX aperture (D_rx) | 1.0 m | m | free_space_link.py |
--rx-diameter |
| Pointing jitter (σ_point) | 2 µrad | rad | free_space_link.py |
--sigma-point |
| Satellite altitude | 500 km | m | free_space_link.py |
--altitude |
| Zenith atm. loss | 0.5 dB | dB | free_space_link.py |
--atm-loss-db |
| Turbulence (σ_ln) | 0.0 | — | free_space_link.py |
--sigma-ln |
| System loss | 3.0 dB | dB | free_space_link.py |
--system-loss-db |
| Detection efficiency (η) | 0.2 | — | detector.py |
--eta |
| Background prob. (p_bg) | 1e-4 | /pulse | detector.py |
--p-bg |
| Day background factor | 100× | — | free_space_link.py |
--day-bg-factor |
- Pointing jitter: 2D Rayleigh distribution (Gaussian magnitude)
- Airmass formula: Kasten-Young (accurate to ~0.5° elevation)
- Fading: Lognormal (weak-to-moderate turbulence) or OU process
- Background: Constant per-pulse probability, scaled for day/night
- Green zones (safe): Wavelength, aperture sizes, altitude, system loss
- Yellow zones (careful): Pointing jitter, η, p_bg, fading parameters
- Red zones (dangerous): Setting p_bg=0, η=1, or negative loss values
For complete traceability, validation mapping, and invariants, see the full Link Model Card.
quantum-key-distribution · qkd · quantum-cryptography · bb84 · decoy-state · e91 · ekert91 · entanglement-based-qkd · cvqkd · continuous-variable-qkd · gg02 · finite-key · privacy-amplification · information-reconciliation · noise-analysis · security-analysis · adversarial-attacks · simulation · satellite-communications · free-space-optics
-
Altman, C., Williams, C., Ursin, R., Villoresi, P., Sharma, V. Astronaut Development and Deployment of a Secure Space Communications Network. NASA NIAC/OCT; DARPA QUINESS (Macroscopic Quantum Communications).
-
Bennett, C. H., & Brassard, G. (1984). Quantum cryptography: Public key distribution and coin tossing. Proceedings of the IEEE International Conference on Computers, Systems and Signal Processing (Bangalore, India), 175–179.
-
Ekert, A. K. (1991). Quantum cryptography based on Bell's theorem. Physical Review Letters, 67(6), 661–663.
-
Shor, P. W., & Preskill, J. (2000). Simple proof of security of the BB84 quantum key distribution protocol. Physical Review Letters, 85, 441–444.
-
Renner, R., Gisin, N., & Kraus, B. (2005). An information-theoretic security proof for QKD protocols. Physical Review A, 72, 012332.
-
Gisin, N., Ribordy, G., Tittel, W., & Zbinden, H. (2002). Quantum cryptography. Reviews of Modern Physics, 74(1), 145–195.
-
Tomamichel, M., Lim, C. C. W., Gisin, N., & Renner, R. (2012). Tight finite-key analysis for quantum cryptography. Nature Communications, 3, 634.
-
Lim, C. C. W., Curty, M., Walenta, N., Xu, F., & Zbinden, H. (2014). Concise security bounds for practical decoy-state quantum key distribution. Physical Review A, 89, 022307.
-
Liao, S.-K., et al. (2017). Satellite-to-ground quantum key distribution. Nature, 549, 43–47.
-
Bourgoin, J.-P., et al. (2013). A comprehensive design and performance analysis of low Earth orbit satellite quantum communication. New Journal of Physics, 15, 023006.
If you use this work in your research, please cite:
@software{sat-qkd-curves2026,
title={Satellite Quantum Key Distribution Security Curves},
author={Altman, Christopher},
year={2026},
url={https://github.com/christopher-altman/sat-qkd-security-curves}
}MIT License. See LICENSE for details.
- Website: christopheraltman.com
- Research portfolio: lab.christopheraltman.com
- Portfolio mirror: christopher-altman.github.io
- GitHub: github.com/christopher-altman
- Google Scholar: scholar.google.com/citations?user=tvwpCcgAAAAJ
- Email: x@christopheraltman.com
Christopher Altman (2026)

