Threat Model¶
This document names the things we are trying to protect
(assets), the kinds of actors we plan against (adversaries),
where they can try to interact (attack surface), and the
mitigations currently in the repository. It is a companion to
SECURITY.md — that file tells researchers how to report a
vulnerability; this file explains what we consider a
vulnerability.
Version 1 — 2026-04-17. Review cycle: quarterly, or sooner if any of the three crypto / QKD / hardware surfaces change in a non-trivial way.
Assets¶
- Scientific integrity of published numbers — the hardware
result JSON files in
data/, the statistics quoted inCHANGELOG.md,docs/results.md, anddocs/preprint.md. An attacker who can flip a depth-point or a sign silently invalidates a publication. - Research credentials — the IBM Quantum token, Zenodo token, GoatCounter key, PyPI Trusted Publisher identity. Exfiltration would enable forged submissions under the ANULUM identity.
- End-user cryptographic output — the output of every function
in
crypto/. Includes entanglement-QKD session material, topology-authenticated key material, K_nm-derived secrets, and ML-DSA trigger signatures. Downstream code that trusts these bytes as secret material must hold against a passive network adversary. - Host resources of anyone who imports the library — CPU,
memory, disk, QPU quota. A pathological input to
analysis/koopman.pyorbridge/knm_hamiltonian.pyshould not exhaust the caller. - Downstream consumer trust — the chain that takes a git
commit, through a wheel, through PyPI, to a user's import. A
substituted wheel that passes
pip installinvalidates every user we have.
Adversaries we plan against¶
| # | Adversary | Capability | Incentive |
|---|---|---|---|
| A1 | Passive public-internet observer | Reads traffic between a user and PyPI / IBM Quantum / GitHub Pages / Zenodo | Eavesdrop on QKD sessions, infer research directions from download patterns |
| A2 | Malicious PyPI mirror / cache | Serves an altered sdist or wheel under our name | Supply-chain code execution on researcher workstations |
| A3 | Compromised Dependabot / GitHub Actions third-party action | Runs our CI with attacker-controlled code | Exfiltrate PYPI_API_TOKEN, QISKIT_IBM_TOKEN, ZENODO_TOKEN, or alter the release artefact |
| A4 | Forged hardware-result author | Submits fabricated data/<experiment>/*.json via a PR |
Inject false scientific claims we cite as evidence |
| A5 | Hostile user of the library | Calls public APIs with crafted input | Denial of service on the caller; code execution via deserialisation if we added any |
| A6 | Insider with commit access | Pushes a commit that silently tightens feedback_no_simplifications or removes a statistical caveat |
Long-range scientific fraud |
We explicitly do not plan against:
- Nation-state adversaries with implants on the maintainer's laptop. Mitigation there is out-of-scope for this repo.
- Adversaries with full write access to PyPI itself. Sigstore
Trusted Publishing (
.github/workflows/publish.yml) raises the bar but does not defeat PyPI compromise. - Adversaries with physical access to IBM Quantum data centres. Any mitigation belongs on IBM's side.
Attack surface¶
S1 — Crypto subpackage (crypto/)¶
entanglement_qkd.py,topology_auth.py,hierarchical_keys.py,knm_key.py,ml_dsa.py,ml_dsa_seal.py,noise_analysis.py,percolation.py,pqc_trigger.py.- Output is treated as secret. Every function must be deterministic
in its randomness (i.e. accept a
numpy.random.Generatorrather than usingnumpy.random.seed). - No function here imports from
os.urandom,secrets, or reads/dev/randomtoday. Downstream callers who need CSPRNG-grade bits beyond toy demos must pair the QKD output with their own CSPRNG stretch.
S2 — Hardware runner (hardware/runner.py)¶
- Reads
QISKIT_IBM_TOKEN/~/.qiskit/qiskit-ibm.json— never logged, never written to a result JSON. Audit: grepQISKIT_IBM_TOKEN/api_key/tokeninsrc/returns only read paths. - Writes result JSONs with
save_result; includes aprovenanceblock (seehardware/provenance.py) so an outsider can detect silent rewrites. CI gate:tests/test_phase1_dla_parity_reproduces.pyfails if any published statistic drifts. - Circuit-depth bound (
max_depth), shot-count bound (100 000), RNG isolation per module. Documented inSECURITY.md.
S3 — analysis/koopman.py and friends¶
- Past gap: unbounded
eigvalson a caller-supplied n²×n² matrix. Fixed in commitc7d4ccdbyMAX_OSCILLATORS_DEFAULT = 32and an explicit opt-in argument. 13 tests intests/test_koopman.pyexercise every guard branch. - Other modules with ingesting-user-input surface should receive similar validators. Open audit item B8.
S4 — Supply chain¶
- GitHub Actions SHAs are pinned, not tag-pinned, in every workflow. Dependabot monitors actions weekly, pip weekly, cargo weekly.
- Pre-commit hooks include
gitleaksand a customtools/check_secrets.pyvault-pattern scanner.tools/check_commit_trailers.pyenforces the required authorship line and an anti-slop word-list on commit subjects. - Releases carry CycloneDX SBOMs (
sbom.yml) and Sigstore signatures (publish.yml). Downstream verifies without a PyPI round-trip.
S5 — Dataset / result integrity¶
data/<experiment>/files are tracked in git; any change shows up ingit diff. The provenance block added in commit819adedembeds the producing commit SHA in every new result file, so a silent overwrite becomes detectable as agithash that does not match the claimed campaign.
S6 — private internal records/ and private local workspace/¶
- Gitignored. Contain drafts, private audit records, agent-generated audits. No secret should live there, but private plans do.
- Repo gitleaks hook scans both anyway in
--allmode.
Mitigations per adversary¶
| Adversary | Primary mitigation | Residual risk |
|---|---|---|
| A1 Passive observer | TLS everywhere (PyPI, IBM, GitHub, Zenodo all HTTPS-only). QKD output is intended to be the shared secret for a higher-level protocol, not the full protocol. | None beyond the TLS CA trust assumption. |
| A2 Malicious PyPI mirror | Sigstore-signed wheels + Trusted Publisher attestations. Reader verifies with sigstore verify identity. |
Attacker who compromises the anulum GitHub org OIDC identity can mint a valid signature; not defeated. |
| A3 Compromised GH Action | Every third-party action pinned by full commit SHA, not by tag. OpenSSF Scorecard Pinned-Dependencies gate. Dependabot flags new SHAs weekly. |
A compromise of the upstream repo that the SHA was taken from, before we updated, is still exploitable until detected. |
| A4 Forged hardware result | PRs with changes under data/ require maintainer review. Reproducer test (tests/test_phase1_dla_parity_reproduces.py) detects any drift in published numbers. |
A forger who edits both the JSON and the claimed statistics consistently would pass the reproducer. Human review is the last line. |
| A5 Hostile user | Input validation (koopman, planned for the rest). Depth + shot caps on hardware runner. No pickle deserialisation (per SECURITY.md). |
Novel crafted inputs may still find unchecked paths; audit item B8 tracks this. |
| A6 Insider with commit access | feedback_no_simplifications is a hard rule for agents. DEPRECATIONS.md forbids silent API removals. docs/falsification.md pins each scientific claim to an observable refutation criterion. CI runs mypy + ruff + tests, but cannot detect a coherent lie. |
Real threat. Maintainer is currently a single person; a second reviewer for CHANGELOG.md + docs/preprint.md edits is future governance work. |
Known gaps (tracked in the audit)¶
- B6 — Criterion-level Rust benchmarks with their own
regression gate. Today we have the Python-side regression gate
in
tests/test_perf_regression.py; Rust side is only timed indirectly. - B7 — Mutation testing baseline.
mutmuthas never been run; we do not know the test-quality score. - B8 — Fuzz tests on the non-koopman input boundaries.
NARROWED 2026-07-11: four coverage-guided
cargo-fuzztargets now cover the highest-exposure boundaries —program_ad_ir(Program AD JSON IR),studio_kuramoto_input(the browser-facing studio WASM kernel byte parsers + bounded replay),ml_dsa_ntt(NTT/INTT input domain with a bijection invariant), andknm_validators(the shared Rustvalidation::check_*guards + boundedbuild_knm_inner). The harness surfaced and fixed three fail-open validator defects:check_flat_squaren*noverflow andcheck_statevec_len1 << nshift overflow (found while writing the harness — both now return an error instead of panicking/wrapping), and — found by the running fuzzer on its first seed pass —check_positive/check_rangeaccepting NaN because their negated comparisons are false for NaN (a NaNdt/k_base/alphasailed through the positivity guard; both rewritten as positive predicates that fail closed). Residual: fuzzing is build-checked and seeded with short sanity runs, not yet a sustained CI campaign; PyO3-bound entry points (numpy-array kernels) remain exercised only via the Python property/panic-boundary suites. - C4 — Formal export-control (ECCN 5D002) assessment for the
crypto/subpackage. - C7 — Cross-validation against Dynamiqs / QuTiP / PennyLane on the same problem to detect silent numerical divergences.
Each of these degrades the confidence of one or more mitigations above and is logged as an open item in the internal gap audit.