diff --git a/.agents/skills/README.md b/.agents/skills/README.md index ed379b6..37aa4d8 100644 --- a/.agents/skills/README.md +++ b/.agents/skills/README.md @@ -6,14 +6,19 @@ Canonical source: - Preferred: `The-Interdependency/skill-lib` - Temporary source: `The-Interdependency/a0/skill-lib` -Source commit: `The-Interdependency/skill-lib` @ `a0cb6285e37734609b4b487ae4a2e44c6108d2b8` (verbatim sync). +Source commit: `The-Interdependency/skill-lib` @ `5c46d0534fa0726a9078f0a242c66a217fbaa501` (verbatim sync). Installed skills: - `msdmd/` — Module Self-Declared Metadata Markdown - `test-build/` — test contract metadata blocks - `meta-module-build/` — metadata-first module scaffolding -- `manifest/` — living-spec generator for `CLAUDE.md` (vendored from `The-Interdependency/skill-lib@05ee7aa`); CI runs `generate.py --check`. Refresh with `python .agents/skills/manifest/generate.py --write`. +- `manifest/` — living-spec generator for `CLAUDE.md`; CI runs `generate.py --check`. Refresh with `python .agents/skills/manifest/generate.py --write`. Agents working in this repo should read `meta-module-build/SKILL.md` before creating new modules, routes, services, schemas, adapters, workers, engines, UI panels, migrations, or experiments. + +Usage guidance: +- Treat the source commit above as the exact canonical snapshot for vendored skill files. +- Run the repository's manifest/skill drift workflow after changing `.agents/skills/`. +- Update vendored canonical files only by propagation from `The-Interdependency/skill-lib`; keep repo-local additions explicitly local. diff --git a/.github/workflows/contract-boundary.yml b/.github/workflows/contract-boundary.yml index c6c61c4..93596d6 100644 --- a/.github/workflows/contract-boundary.yml +++ b/.github/workflows/contract-boundary.yml @@ -1,4 +1,4 @@ -name: contract-boundary +name: full-suite on: pull_request: @@ -6,20 +6,50 @@ on: branches: [ main ] jobs: - boundary: + test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.11", "3.13"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install package with dev dependencies run: | python -m pip install --upgrade pip python -m pip install -e ".[dev]" - - name: Run contract boundary tests + - name: Run full repository test suite run: | - PYTHONPATH=. python -m pytest -q tests/test_contract_spec.py + PYTHONPATH=. python -m pytest -q + + release-artifact: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 + with: + python-version: "3.11" + - name: Build wheel from declared package metadata + run: | + python -m pip install --upgrade pip + python -m pip wheel . --no-deps --wheel-dir dist + - name: Install wheel outside the source tree and smoke-test public package + shell: bash + run: | + set -euo pipefail + wheel=$(printf '%s\n' dist/pcea-*.whl) + test -f "$wheel" + python -m venv /tmp/pcea-release-smoke + /tmp/pcea-release-smoke/bin/python -m pip install --no-deps "$wheel" + cd /tmp + /tmp/pcea-release-smoke/bin/python - <<'PY' + from importlib.metadata import version + import pcea + + assert version("pcea") == "0.1.0" + assert callable(pcea.encrypt_state) + assert callable(pcea.decrypt_state) + assert pcea.PCEAInstance is not None + PY diff --git a/.github/workflows/manifest-check.yml b/.github/workflows/manifest-check.yml index e0e9d48..21b9d85 100644 --- a/.github/workflows/manifest-check.yml +++ b/.github/workflows/manifest-check.yml @@ -1,8 +1,11 @@ -name: manifest drift check +name: manifest and skill drift check # Living-spec gate: fails if CLAUDE.md's generated manifest block has drifted # from pyproject.toml / the repo tree. Refresh locally with: # python .agents/skills/manifest/generate.py --write +# +# Canonical-skill gate: checks the vendored subset byte-for-byte against the +# exact skill-lib source commit recorded in .agents/skills/README.md. on: pull_request: @@ -12,15 +15,43 @@ on: permissions: contents: read +env: + SKILL_LIB_SHA: 5c46d0534fa0726a9078f0a242c66a217fbaa501 + jobs: manifest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: '3.11' - name: Vendored generate.py matches skill-lib (no local fork) run: cd .agents/skills/manifest && sha256sum -c generate.py.sha256 - name: Check CLAUDE.md manifest block is in sync run: python .agents/skills/manifest/generate.py --root . --check + + skill-drift: + runs-on: ubuntu-latest + steps: + - name: Check out PCEA + uses: actions/checkout@v7 + with: + path: pcea + - name: Check out exact canonical skill-lib snapshot + uses: actions/checkout@v7 + with: + repository: The-Interdependency/skill-lib + ref: ${{ env.SKILL_LIB_SHA }} + path: skill-lib + persist-credentials: false + - uses: actions/setup-python@v7 + with: + python-version: '3.11' + - name: Verify vendored canonical skills and source citation + run: | + python skill-lib/tools/check_consumer_drift.py pcea \ + --canon-root skill-lib \ + --sha "$SKILL_LIB_SHA" \ + --strict-sha \ + --require-vendored diff --git a/README.md b/README.md index c44086f..c5d00d1 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,15 @@ PCEA encrypts pre-quantized integer neural architecture state using the relation Runtime state is structured as a list of seeds. Each seed is a `7×7` integer array: 7 circles × 7 tensors. +## Security status + +**Experimental research software.** PCEA has not undergone independent +cryptographic review and is not claimed suitable for protecting sensitive or +production data. Version 0.1.0, if released, is a research release of the +symmetric transform and its falsification/proving harnesses. Where real-world +confidentiality is required, use reviewed standard cryptography rather than +substituting PCEA for it. + ## Algorithm For each integer value at address `(seed_idx, circle_idx, tensor_idx)`: @@ -63,7 +72,7 @@ assert dec.decrypt(e2) == [state2] The shipped `pcea/` package is the symmetric, state-synchronized transform described above. Its correctness and contract tests live in `tests/test_cipher.py`, `tests/test_codec.py`, `tests/test_kdf.py`, `tests/test_instance.py`, and `tests/test_contract_spec.py`. -The `pcea-ucns/` directory is different: it is an attack-and-feasibility workspace for possible UCNS-native key-establishment or gonal/state-communication layers around PCEA. Those files do **not** upgrade the symmetric PCEA runtime into a proven public-key encryption system. They record candidate constructions, measured breaks, and open gates for any future PCEA-UCNS layer. See `pcea-ucns/README.md` for the concrete proving-ground workflow. +The `pcea-ucns/` directory is different: it is an attack-and-feasibility workspace for possible UCNS-native key-establishment, authenticated-session, or gonal/state-communication layers around PCEA. Those files do **not** upgrade the symmetric PCEA runtime into a proven public-key encryption or production-secure session system. They record candidate constructions, measured breaks, surviving regression checks, and open gates. See `pcea-ucns/README.md` for the concrete proving-ground workflow. Important testing boundaries: @@ -71,6 +80,7 @@ Important testing boundaries: - PCEA-UCNS tests are attack/regression harnesses. They are skipped when `ucns` is not installed, keeping the symmetric runtime testable without UCNS. - Passing a PCEA-UCNS harness means only that the measured behavior has not drifted; it is not a proof of cryptographic security. - Several PCEA-UCNS harnesses intentionally pin breaks or negative findings, including oracle-domain factorization, positional/factor-count attacks, prefix-read reconstruction, and Minkowski set-basis recovery. +- `ratcheted_session.py` is a non-runtime prototype around provisioned PCEA state; its current replay/transcript/rollback/key-separation tests are prerequisites for harder attack work, not a promotion certificate. - The gonal architecture tests include a measured PCEA-advanced candidate that resists simple frequency/known-plaintext probes in the harness, but the module still marks further attacks and the 53→32 bridge as gates before any shipped `gonal_cipher.py`. Useful commands: @@ -78,6 +88,7 @@ Useful commands: ```bash python -m pytest -q python -m pytest -q tests/test_cipher.py tests/test_codec.py tests/test_kdf.py tests/test_instance.py tests/test_contract_spec.py +python -m pytest -q tests/test_ratcheted_session.py tests/test_option_family_specs.py python -m pytest -q tests/test_attack_harness.py tests/test_positional_attack.py tests/test_quotient_attack.py tests/test_prefix_read_break.py tests/test_projection_action_candidate.py tests/test_pruning_scaling.py tests/test_attack1_minkowski_break.py tests/test_three_factor_attack.py tests/test_factor_count_sweep.py tests/test_gonal_architecture.py ``` @@ -95,7 +106,7 @@ This keeps PCEA cryptographic claims decoupled from UCNS analytic-frontier work. Enforcement: - `tests/test_contract_spec.py` is a release gate for this boundary. -- `.github/workflows/contract-boundary.yml` runs this gate in CI for pull requests and pushes to `main`. +- `.github/workflows/contract-boundary.yml` runs the **full repository suite** on Python 3.9, 3.11, and 3.13 for pull requests and pushes to `main`, and also builds/installs the declared wheel outside the source tree as a release-artifact smoke test. ## Install diff --git a/pcea-ucns/ARITY_FREEZE_AND_TEST.md b/pcea-ucns/ARITY_FREEZE_AND_TEST.md new file mode 100644 index 0000000..8d97d24 --- /dev/null +++ b/pcea-ucns/ARITY_FREEZE_AND_TEST.md @@ -0,0 +1,626 @@ +# PCEA Arity Freeze and Test Procedure + +**Status:** preregistered research procedure; no arity result is claimed by this document. + +**Purpose:** determine whether PCEA's relational structure contains a measurable direct-arity effect, whether that effect survives lower-arity partitioning, whether it is attributable to PCEA rather than the underlying hash/PRF, and only then whether it changes cryptanalytic attack cost. + +This procedure is intentionally fail-closed. A positive structural result is not a security result. A positive attack-cost result is not a security proof. No runtime promotion follows automatically from any result. + +## 1. Decision boundary + +The decision this run must inform is: + +> Does PCEA instantiate irreducible relational arity that contributes measurable cryptographic hardness beyond lower-arity decompositions and a matched conventional PRF control? + +The earliest load-bearing unknown is structural: + +> Is the current seven-circle construction actually heptadic, or is it a seven-member topology assembled from lower-arity local relations? + +The current frozen implementation derives one cell's PCEA key stream from exactly three previous-state contributors: the same circle plus the two heptagram neighbors at `-3` and `+3`. Therefore the current implementation must enter this experiment as the **A3 baseline**, not be called A7 merely because the state has seven circles. + +## 2. Authority and non-transfer + +This is a PCEA proving-ground experiment. It may test arity-shaped relations but may not promote an arity result into UCNS, METAPAT, or cryptographic canon. + +METAPAT supplies a useful question-form: tensor is simultaneous arrangement; relation is readable configuration within tensor. It does not make cryptographic arity a root axiom. Arity in this procedure is an application/research variable that must earn standing through intervention and partition tests. + +The prior arity research supplies the falsification form: a claimed `n`-ary relation is not earned if lower-arity partitions reproduce every relevant invariant. That form transfers. Consciousness, scale, Möbius ontology, or other domain claims do not transfer into this cryptographic test. + +## 3. Frozen baseline + +The implementation baseline is the PCEA release-readiness candidate: + +```text +repository: The-Interdependency/pcea +baseline_commit: f69ca59a278f81c9b2df80d7cb8053ca1cb8c5f5 +``` + +`baseline_commit` is intentionally earlier than the commit that added this procedure. Later documentation commits do not change the frozen transform. The experiment is defined against the baseline code objects below, not against whatever `HEAD` happens to be when Codex starts. + +The following baseline source objects are frozen for this experiment and MUST NOT be edited by the implementing actor: + +```text +pcea/cipher.py +pcea/codec.py +pcea/kdf.py +pcea/primes.py +pcea/instance.py +pcea/contract.py +pcea/__init__.py +pcea-ucns/ratcheted_session.py +``` + +The experiment branch may contain this procedure and new research-only files after the baseline commit. Before running any experiment, the implementing actor MUST verify that every frozen path is byte-identical to `baseline_commit`. + +A required freeze receipt is: + +```text +pcea-ucns/arity/freeze.json +``` + +It MUST record: + +```text +repository +baseline_commit +experiment_commit +frozen_path -> git blob SHA +python version +platform +experiment schema version +fixture generator version +all parameter sets +all deterministic fixture seeds +all metrics +all controls +all outcome labels +all escalation rules +``` + +If any frozen path differs from the baseline, classify the run `BLOCKED` and do not execute the experiment. + +## 4. Mutation boundary + +For v1, Codex may create or modify only: + +```text +pcea-ucns/arity/** +tests/test_arity_*.py +pcea-ucns/ARITY_FREEZE_AND_TEST.md +pcea-ucns/README.md +``` + +Do not modify `pcea/` or `pcea-ucns/ratcheted_session.py`. + +All new executable Python files must follow the repository's ratios/provenance conventions and remain stdlib-only except for pytest as an existing dev dependency. + +The arity implementation is a proving-ground harness, not a new public API. + +## 5. Definitions + +### 5.1 State-key arity + +For this experiment, **state-key arity** is the number of distinct prior-state circle carriers jointly supplied to the per-cell key-stream relation before the current plaintext value is transformed. + +This deliberately does not count the current plaintext cell itself. If a later experiment wants total transform arity, that must be named separately. + +### 5.2 Direct arity + +A variant `A_n` has declared direct state-key arity `n` only when one per-cell key derivation consumes all `n` labeled contributors as one joint input relation. + +A graph containing `n` objects connected only by dyads or triads is not automatically `n`-ary. + +### 5.3 Lower-arity partition + +For direct arity `n`, a lower-arity partition uses only proper subsets of the contributor set: + +```text +S subset [n], with |S| < n +``` + +A claimed `n`-ary residual survives structurally only if at least one frozen output invariant cannot be reconstructed from the complete family of lower-order components under the exact v1 reconstruction test. + +### 5.4 Arity residual + +`arity_residual` is a research diagnostic, not a security metric. + +For the exact Boolean toy-domain test, it is present for an output bit when that bit's algebraic normal form contains a nonzero degree-`n` coefficient over the `n` binary contributor variables. + +Equivalently, the exact Möbius/Boolean transform of the truth table has a nonzero coefficient on the full contributor set. + +Record: + +```text +full_degree_output_bits +output_bits_examined +full_degree_fraction +maximum_algebraic_degree +``` + +Do not replace this exact test with an ML predictor or a correlation proxy. + +## 6. Frozen contributor order and variants + +Use one seven-carrier rotational order so arity changes are not chosen after results: + +```text +OFFSETS = [0, -3, +3, -1, +1, -2, +2] +``` + +For the target circle `c`, indices are modulo 7. + +The v1 variants are prefixes of that order: + +```text +A2 = [0, -3] +A3 = [0, -3, +3] # exact current PCEA contributor relation +A5 = [0, -3, +3, -1, +1] +A7 = [0, -3, +3, -1, +1, -2, +2] +``` + +The A3 research adapter MUST reproduce frozen current PCEA output exactly for matched inputs. If it does not, the experiment is `BLOCKED`; repair the adapter, not the baseline. + +## 7. Matched controls + +Every `A_n` PCEA variant MUST have a matched `PRF-A_n` control. + +The control must: + +- use the same contributor order and count; +- use the same public address information; +- use the same provisioned/master secret entropy when a secret is in scope; +- use SHA-256/HMAC-SHA256, matching the experiment lane's primitive; +- produce the same output bit width; +- transform the same current plaintext cell/state; +- omit PCEA prime selection, base-`p` representation, Möbius codec, and PCEA state transform. + +The purpose is to answer: + +> Is the measured arity effect a PCEA contribution, or merely the ordinary high-order behavior of a cryptographic hash/PRF over multiple inputs? + +Also retain: + +```text +PCEA-A3-current +``` + +as the exact frozen-current baseline. + +## 8. Hypotheses and outcome labels + +Freeze these before execution. + +### H-STRUCT: structural direct-arity residual + +For at least one `A_n`, `n > 3`, at least one output bit has an exact degree-`n` component under the Boolean toy-domain test. + +- no full-degree component -> `FALSIFIED_STRUCTURAL_ARITY` +- full-degree component -> `SURVIVED_STRUCTURAL_ARITY` +- harness cannot establish exact truth table -> `BLOCKED` + +### H-PCEA: PCEA-specific arity contribution + +PCEA's structural arity diagnostics differ reproducibly from its matched `PRF-A_n` control under the frozen fixtures. + +- no material difference beyond the frozen equality/tolerance rules -> `FALSIFIED_PCEA_SPECIFIC_ARITY` +- reproducible difference -> `SURVIVED_PCEA_SPECIFIC_ARITY` +- comparison invalid or underpowered -> `UNRESOLVED` + +### H-HARD: cryptanalytic benefit + +After controlling master-secret entropy, transcript exposure, query budget, and arity, an arity-preserving PCEA variant requires greater frozen attack work or yields lower recovery/prediction success than its matched PRF control. + +- no advantage or PCEA is weaker -> `FALSIFIED_ARITY_HARDNESS_BENEFIT` +- advantage survives all frozen controls -> `SURVIVED_CURRENT_HARNESS` +- resource/attack family cannot decide -> `UNRESOLVED` + +`SURVIVED_CURRENT_HARNESS` is never rewritten as `secure`, `hard`, `production-ready`, or `reviewed`. + +## 9. Phase 0 — structural dependency audit + +Before implementing A2/A5/A7, generate a machine-readable dependency description of frozen current PCEA. + +For every encrypted cell identify: + +```text +current plaintext cell dependencies +prior-state cell dependencies +address dependencies +prime/base dependency +hash/KDF dependency +session-secret-derived dependency when the ratcheted wrapper is used +``` + +Emit: + +```text +pcea-ucns/arity/current_dependency_graph.json +``` + +Required v1 finding to verify, not assume: + +```text +current PCEA state-key direct arity = 3 +seven-circle topology = overlapping local relations +``` + +If inspection shows that statement is incomplete or wrong, record the exact dependency relation and revise only the interpretation section of the eventual report. Do not silently redefine arity after seeing later results. + +## 10. Phase 1 — exact structural arity test + +This is the minimal decisive experiment and MUST run before cryptanalytic scaling. + +### 10.1 Exact toy domain + +Isolate one target tensor coordinate and one target output cell. + +For each `A_n` and `PRF-A_n`: + +1. Hold all non-contributor prior-state cells at a frozen constant. +2. Treat each of the `n` contributor values as one binary variable in `{0,1}`. +3. Enumerate all `2^n` contributor assignments exactly. +4. Use frozen current plaintext values: + +```text +PLAINTEXT_VALUES = [0, 1, -1, 2, 127] +``` + +5. Use: + +```text +WORD_BITS = 8 +TARGET_CIRCLE = 0 +TARGET_TENSOR = 0 +SEED_INDEX = 0 +``` + +6. For every output bit, compute the exact Boolean algebraic normal form / Möbius transform over the contributor variables. +7. Record the highest degree and whether the degree-`n` coefficient is nonzero. + +The small `word_bits` value is an exact structural microscope only. It makes no 8-bit security claim. + +### 10.2 Exact lower-order reconstruction + +For each output bit, reconstruct the function after deleting all degree-`n` terms. + +Record: + +```text +truth_table_rows +exact_matches_without_degree_n +mismatches_without_degree_n +mismatch_fraction +``` + +If deleting the full-degree component changes no output for every fixture, the declared `n`-way component is absent under this representation. + +### 10.3 Phase-1 stop/escalation rule + +- If A5 and A7 both lack full-degree components: stop. H-STRUCT is falsified; do not run the maximal cryptanalytic program. +- If A5 or A7 has a full-degree component but the matched PRF control shows the same result: continue only through the PCEA-specific comparison; do not infer a PCEA security benefit. +- If PCEA shows a reproducible residual not present in the matched PRF control: enter Phase 2. +- Any adapter mismatch with A3 current output: `BLOCKED` until prerequisite repair. + +## 11. Phase 2 — sensitivity and PCEA-specific controls + +Phase 2 asks whether the structural result is robust and PCEA-specific. + +Use the same exact enumeration while varying only one frozen dimension at a time: + +```text +word_bits: [8, 12, 16] +target_tensor: [0, 1, 3, 6] +target_circle: [0, 1, 3, 6] +plaintext_values: frozen list above +``` + +Do not add parameter values after seeing results. + +For every PCEA variant and matched PRF variant record: + +```text +maximum algebraic degree +full_degree_fraction +changed-output fraction per single contributor intervention +collision count +output-bit balance +``` + +Balance, collision rate, and avalanche/sensitivity are diagnostics only; none is a security proof. + +H-PCEA survives only if the predeclared comparison shows a reproducible PCEA/control difference across the frozen parameter grid rather than one cherry-picked coordinate. + +## 12. Phase 3 — reduced-entropy cryptanalytic scaling + +Run only if the frozen escalation rule reaches this phase. + +The purpose is to measure attack scaling exactly at toy/reduced entropy before spending effort on realistic key sizes. + +### 12.1 Master-secret fixtures + +Use master-secret seeds with entropy: + +```text +B = [8, 12] +``` + +A `b`-bit seed is deterministically expanded with SHA-256 into the API-compatible secret bytes so all constructions use the same actual secret length while the exhaustive search space remains exactly `2^b`. + +Fixture secrets MUST be generated before attack execution from: + +```text +SHA256("pcea-arity-v1|secret-fixture||") +``` + +Freeze the first 16 trials for each `b`. + +### 12.2 Attacker knowledge + +The v1 attacker knows: + +```text +algorithm and source +variant and arity +all public parameters +session id +direction +sequence numbers +AAD +ciphertexts +chosen plaintexts supplied by the attack harness +all non-secret fixture-generation rules +``` + +The attacker does not receive the master-secret seed. + +State which prior plaintext/state values are attacker-known in each attack. Do not mix known-state and hidden-state attacks in one metric. + +### 12.3 Frozen attacks + +Run at minimum: + +1. **Exhaustive secret recovery** — enumerate candidate master-secret seeds until the transcript uniquely matches. +2. **Lower-arity partition-assisted recovery** — give the attacker the complete frozen family of lower-arity subset observations allowed by the game and measure whether candidate pruning beats ordinary exhaustive recovery. +3. **Next-output prediction** — after the frozen chosen-plaintext transcript, predict the next target output without the secret; record exact success rate over trials. +4. **Known-state versus hidden-state split** — run the same game once with previous plaintext state known and once with it withheld, keeping those results separate. + +Measure work primarily as: + +```text +candidate secrets evaluated +oracle/transcript queries +exact surviving candidates after each observation +``` + +Record wall time only as environment metadata. Do not use wall time as the scientific stopping rule. + +### 12.4 Phase-3 escalation + +If neither `b=8` nor `b=12` shows a PCEA/control separation in attack work, stop and classify H-HARD according to the frozen rules. + +Only if a separation survives may a later preregistration authorize `b=16` or larger. Do not extend key sizes merely because the run is already open. + +## 13. Conventional control interpretation + +The PRF control is load-bearing. + +Possible outcomes: + +```text +PCEA and PRF both show n-way structural terms + -> arity may be ordinary joint-hash behavior; no PCEA-specific claim + +PCEA shows less structural interaction than PRF + -> PCEA structure is not adding the hypothesized effect + +PCEA shows a reproducible structural residual absent from PRF + -> interesting PCEA-specific structure; proceed to attacks + +PCEA attack cost <= matched PRF attack cost + -> no demonstrated security benefit from PCEA arity + +PCEA attack cost > matched PRF under all frozen controls + -> survives current harness only; escalate to independent cryptanalysis +``` + +## 14. Required artifacts + +Codex must emit: + +```text +pcea-ucns/arity/freeze.json +pcea-ucns/arity/current_dependency_graph.json +pcea-ucns/arity/fixtures.json +pcea-ucns/arity/structural_results.json +pcea-ucns/arity/attack_results.json # only if Phase 3 is reached +pcea-ucns/arity/REPORT.md +``` + +Implementation files should remain under: + +```text +pcea-ucns/arity/ +``` + +Tests should be named: + +```text +tests/test_arity_freeze.py +tests/test_arity_current_baseline.py +tests/test_arity_structural.py +tests/test_arity_controls.py +tests/test_arity_attack_scaling.py # only if Phase 3 is reached +``` + +Every result artifact must state the exact experiment commit and freeze receipt hash. + +## 15. Full-suite requirement + +After every implementation commit: + +```bash +PYTHONPATH=. python -m pytest -q +``` + +The pre-existing PCEA suite must remain green. + +The arity tests must fail loudly if: + +- a frozen source object changes; +- the A3 adapter stops matching current PCEA; +- fixture generation changes without schema version change; +- an expected result artifact is missing; +- a result was produced from a different freeze receipt; +- an outcome label is outside the preregistered vocabulary. + +Do not convert optional UCNS skips into arity successes. + +## 16. Resource preflight + +Before each compute phase Codex must estimate whether the environment can reach the phase's natural terminal condition using the exact enumerations and trial counts above. + +- Phase 0: source analysis only; expected feasible. +- Phase 1: at most `2^7` contributor states per frozen plaintext/variant; expected feasible. +- Phase 2: bounded exact grid above; preflight before starting. +- Phase 3: exhaustive `2^8` and `2^12` candidate-secret spaces over frozen trials; preflight before starting. + +If completion feasibility is materially uncertain, do not start the phase. Record `BLOCKED_RESOURCE_PREFLIGHT` and the decisive resource uncertainty. + +Once a healthy phase begins, let it finish to its natural terminal condition. Do not add an arbitrary wall-clock timeout. + +## 17. Multi-actor protocol + +This research benefits from multiple actors, but only when their independence is preserved. + +### Actor A — freeze steward / harness builder + +Recommended: **Codex**. + +Actor A may: + +- implement this procedure exactly; +- create fixtures and research adapters; +- run the frozen tests; +- report results using only preregistered labels. + +Actor A may not: + +- modify frozen baseline files; +- alter parameters after seeing results; +- invent a new attack and silently fold it into v1; +- promote a survival result into a security claim. + +Actor A publishes its experiment commit and result hashes before seeing independent actor results. + +### Actor B — independent recovery/reimplementation + +Actor B receives: + +```text +this procedure +freeze.json +baseline source identity +fixture-generation specification +``` + +Actor B SHOULD reimplement the Phase-1 exact structural calculation independently rather than importing Actor A's arity-analysis functions. + +Actor B should not receive Actor A's interpretation of the result until its own result commit/hash is frozen. + +The strongest first independence check is: + +```text +same frozen truth tables / fixtures +independent implementation +same exact ANF/Möbius coefficients and outcome label +``` + +A mismatch is `UNRESOLVED_IMPLEMENTATION_DISAGREEMENT`, not a vote. + +### Actor C — adversarial attacker + +Actor C receives the frozen candidate and attack game but is allowed to design additional attacks **outside v1 scoring**. + +Actor C must separate: + +```text +preregistered v1 attacks +new exploratory attacks +``` + +A successful new attack can falsify a candidate immediately. A failed exploratory attack cannot upgrade standing because it was not part of the frozen acceptance criteria. + +### Actor D — adjudicator + +Actor D compares Actor A/B/C only after their commit/result hashes are frozen. + +The adjudicator checks: + +```text +freeze identity +fixture identity +implementation independence +metric identity +result agreement/disagreement +whether any exploratory attack falsified the candidate +``` + +The adjudicator does not average incompatible results. + +### Minimum useful actor count + +```text +2 actors: builder + independent replayer/attacker +3 actors: builder + independent replayer + independent attacker <- preferred +4 actors: add a separate adjudicator <- strongest +``` + +Parallel co-authoring of one harness is not a substitute for independent replay. + +## 18. Codex run instruction + +Codex should treat the following as its execution handoff: + +```text +Read repository CLAUDE.md and current vendored skills first. +Read pcea-ucns/ARITY_FREEZE_AND_TEST.md completely. + +Implement the preregistered PCEA arity experiment without changing any frozen +baseline source path. Begin with Phase 0 and Phase 1 only. Produce freeze.json, +the current dependency graph, deterministic fixtures, exact structural results, +and REPORT.md. Run the full repository test suite after the implementation. + +Obey the frozen escalation rule. Do not start Phase 2 or Phase 3 unless the +preceding phase's preregistered outcome authorizes it. Do not add parameter +values, attacks, or interpretations after seeing results. If a prerequisite +fails, classify it BLOCKED and repair only the owning experimental layer. + +If Phase 3 is reached, preflight the exact exhaustive workload before launch; +once healthy execution begins, let it finish. + +Commit each completed phase separately. The final report must state exact +commit SHAs, freeze receipt hash, all outcome labels, controls, and hmmm. + +Do not merge. Do not modify runtime PCEA. Do not claim security. +``` + +## 19. Completion condition + +The v1 run is complete when one of these occurs: + +```text +H-STRUCT falsified -> stop +H-PCEA falsified -> stop or redirect to non-PCEA arity research +H-HARD falsified -> stop; no demonstrated arity security benefit +SURVIVED_CURRENT_HARNESS -> stop and hand frozen artifacts to independent actors +BLOCKED / UNRESOLVED -> stop and name the prerequisite needed to decide +``` + +There is no momentum-based continuation. + +## hmmm + +- Algebraic degree on the Boolean toy domain is an exact structural diagnostic, but whether it is the best cryptographic translation of the broader arity concept remains open; that is why the matched PRF control and attack phases are mandatory. +- A high-order term can be created by SHA/HMAC itself. PCEA earns no arity-specific standing unless the matched controls separate it. +- If a future result suggests a security benefit, the next step is independent cryptanalysis of a separately preregistered candidate, not runtime promotion. +- Higher-dimensional `z,x` / `z,y` / `z,x,y` relation semantics are not silently imported into this v1 test. They require their own precise operational definition before becoming a PCEA variable. diff --git a/pcea-ucns/ARITY_GEMINI_ADJUDICATION.md b/pcea-ucns/ARITY_GEMINI_ADJUDICATION.md new file mode 100644 index 0000000..cbb81db --- /dev/null +++ b/pcea-ucns/ARITY_GEMINI_ADJUDICATION.md @@ -0,0 +1,269 @@ +# PCEA Arity — Gemini Fourth-Actor Adjudication + +**Status:** normative addendum to `ARITY_FREEZE_AND_TEST.md` section 17 for the v1 arity experiment. + +**Purpose:** preserve the epistemic value of a fourth actor by assigning Gemini only the post-freeze adjudication role. This addendum does not change the frozen hypotheses, fixtures, metrics, controls, attack games, escalation rules, or outcome vocabulary in `ARITY_FREEZE_AND_TEST.md`. + +## 1. Actor identity + +For v1: + +```text +Actor A: Codex — freeze steward / harness builder +Actor B: independent replayer — independent Phase-1 derivation/reimplementation +Actor C: independent attacker — adversarial analysis of the frozen candidate +Actor D: Gemini — adjudicator only +``` + +The fourth actor is valuable because it is not another collaborator. Gemini must remain outside construction, replay, attack design, repair, and reconciliation until A/B/C are complete. + +## 2. Hard timing rule + +**Do not call Gemini before Actors A, B, and C have each finished and frozen their outputs.** + +The sequence is: + +```text +A finishes -> freeze A +B finishes independently -> freeze B +C finishes independently -> freeze C +A+B+C locks verified -> assemble adjudication packet +only then -> call Gemini +``` + +If Gemini sees intermediate discussion, partial results, proposed repairs, or another actor's interpretation before all three locks exist, the fourth-actor independence claim is contaminated. Record that run as `UNRESOLVED_ADJUDICATOR_CONTAMINATION` and do not represent it as the v1 independent adjudication. + +## 3. Required A/B/C lock records + +Before Gemini is called, preserve one immutable lock record per actor: + +```text +pcea-ucns/arity/actors/A_LOCK.json +pcea-ucns/arity/actors/B_LOCK.json +pcea-ucns/arity/actors/C_LOCK.json +``` + +Each lock must record at minimum: + +```text +actor_label +actor_role +model/tool identity if known +baseline_commit +experiment/provenance commit SHA +freeze receipt hash +result artifact paths +SHA-256 of every result artifact +stated outcome label(s) +hmmm / unresolved boundaries +completed_at +``` + +The lock is a receipt, not a summary. It must point to the exact frozen artifacts Gemini will receive. + +If any actor needs to repair its own work after locking, the repaired result is a **new** result set with a new lock. Preserve the earlier lock; do not overwrite history. + +## 4. Independence rules before locking + +### Actor A — Codex + +May see only the preregistration, repository authority, frozen baseline, and its own execution outputs while building/running v1. + +A must freeze its result before receiving B or C conclusions. + +### Actor B — independent replayer + +Receives the preregistration, baseline identity, freeze receipt, and fixture-generation specification needed for independent recovery. + +B should independently implement the exact structural derivation. B must not import A's arity-analysis implementation and should not see A's interpretation before B freezes. + +### Actor C — independent attacker + +Receives the frozen candidate and the declared attack game. C may invent exploratory attacks, but must separate them from preregistered v1 scoring exactly as required by `ARITY_FREEZE_AND_TEST.md`. + +C should not receive A or B conclusions before C freezes. + +## 5. Adjudication packet + +After A/B/C are locked, assemble a read-only packet containing exactly: + +```text +1. pcea-ucns/ARITY_FREEZE_AND_TEST.md +2. this addendum +3. frozen baseline commit identity +4. freeze.json and its SHA-256 +5. A_LOCK.json + A frozen artifacts +6. B_LOCK.json + B frozen artifacts +7. C_LOCK.json + C frozen artifacts +8. exact hashes for every supplied file +9. repository authority files needed only to interpret standing/labels +``` + +Do **not** include: + +```text +chat transcripts about hoped-for outcomes +post-result reconciliation between A/B/C +suggested fixes +new acceptance criteria +arguments for why PCEA "should" work +security marketing language +``` + +The adjudicator should evaluate evidence, not inherit a consensus narrative. + +## 6. Gemini authority boundary + +Gemini may: + +- verify that all supplied artifacts correspond to the declared freeze; +- compare A/B/C results against the preregistered criteria; +- identify implementation or interpretation disagreements; +- determine whether exploratory attacks falsify a candidate; +- determine whether each claimed standing is supported by the frozen evidence; +- assign only the already-authorized outcome/standing vocabulary, plus the disagreement/contamination states defined by the protocol; +- state what remains `hmmm`. + +Gemini may not: + +- repair code; +- rerun the experiment as part of adjudication; +- tune parameters; +- add samples after seeing results; +- redefine arity; +- replace the matched PRF control; +- discard an inconvenient actor result; +- average incompatible results; +- invent a stronger acceptance criterion and retroactively apply it; +- weaken a criterion because the result is promising; +- promote `SURVIVED_CURRENT_HARNESS` to `secure`, `hard`, `production-ready`, or `reviewed`. + +If Gemini determines a rerun or repair is necessary, the adjudication result is `BLOCKED` or `UNRESOLVED` with the exact prerequisite. Repair happens in a later run under a new freeze; Gemini does not perform it inside v1 adjudication. + +## 7. Disagreement handling + +Actor disagreement is evidence, not a vote. + +Use these rules: + +```text +A == B == C and freeze valid + -> adjudicate against preregistered criteria + +A/B structural mismatch + -> UNRESOLVED_IMPLEMENTATION_DISAGREEMENT + +C produces a valid break + -> candidate is falsified for the attacked claim even if A/B agree + +C finds no break + -> no standing upgrade by itself + +artifact/hash/freeze mismatch + -> BLOCKED + +Gemini cannot determine whether criteria were met from supplied evidence + -> UNRESOLVED +``` + +A majority of models does not override an exact counterexample or a broken freeze. + +## 8. Required Gemini output + +Gemini must produce one adjudication record with this structure: + +```text +baseline_commit: +freeze_receipt_hash: +A_lock_hash: +B_lock_hash: +C_lock_hash: + +freeze_valid: yes | no | unresolved +independence_valid: yes | no | unresolved +A_B_reproduction_agreement: yes | no | unresolved +C_falsifying_attack_present: yes | no | unresolved + +H_STRUCT: +H_PCEA: +H_HARD: + +overall_standing: FALSIFIED | SURVIVED_CURRENT_HARNESS | UNRESOLVED | BLOCKED + +criterion_trace: + - exact evidence artifact/hash -> decision> + +disagreements: + - + +exploratory_attacks: + - + +forbidden_inferences_checked: + secure: not_claimed + production_ready: not_claimed + independent_crypto_review: not_claimed + +hmmm: + - +``` + +The adjudication itself must be frozen after completion with its own artifact hash. + +Recommended path: + +```text +pcea-ucns/arity/actors/D_GEMINI_ADJUDICATION.md +pcea-ucns/arity/actors/D_LOCK.json +``` + +## 9. Gemini handoff prompt + +Use this after A/B/C are locked: + +```text +You are Actor D, the independent adjudicator for the frozen PCEA arity v1 experiment. + +Adjudicate only. Do not repair code, rerun experiments, tune parameters, add tests, +or redesign the protocol. + +Read ARITY_FREEZE_AND_TEST.md and ARITY_GEMINI_ADJUDICATION.md first. Verify the +baseline commit, freeze receipt, A/B/C lock records, and artifact hashes. Then compare +the three frozen result sets strictly against the preregistered criteria. + +Do not treat agreement by majority as proof. A valid falsifying attack overrides +positive agreement. An A/B reproduction mismatch is unresolved rather than something +to average away. A broken freeze is BLOCKED. + +Return the required Gemini adjudication record. Use only the authorized standing +vocabulary. In particular, SURVIVED_CURRENT_HARNESS does not mean secure, hard, +production-ready, or independently cryptographically reviewed. + +Identify every criterion using the exact artifact/hash that supports your decision. +Preserve all unresolved boundaries under hmmm. +``` + +## 10. Why the fourth actor is delayed + +The fourth actor does not primarily add another search for bugs. Its strongest contribution is **independent judgment of whether the frozen evidence actually satisfies the frozen claim**. + +Calling Gemini early changes its role from adjudicator to collaborator and allows shared interpretation to propagate into A/B/C. Calling Gemini after A/B/C are immutable lets it detect: + +- post-hoc criterion drift; +- selective reconciliation; +- unsupported promotion of a structural result into a security result; +- ignored counterexamples; +- actor disagreement hidden by summary prose; +- freeze/provenance mismatch. + +Therefore the v1 sequence is load-bearing: + +```text +three independent producers first -> immutable evidence -> Gemini fourth +``` + +## hmmm + +- Model-family independence is not statistical independence; shared training priors may remain correlated even with strict information separation. +- If Gemini finds an adjudication prerequisite that was absent from the original protocol, that finding can motivate v2, but it cannot be retrofitted into v1 scoring. +- A later human cryptographer remains categorically different from fourth-model adjudication and would be required before any production cryptographic security claim. diff --git a/pcea-ucns/README.md b/pcea-ucns/README.md index 43e2ac0..647c486 100644 --- a/pcea-ucns/README.md +++ b/pcea-ucns/README.md @@ -37,12 +37,36 @@ PCEA transform and speculative UCNS-native key establishment. `pcea/` runtime must remain the symmetric, state-synchronized transform and must not silently depend on UCNS inversion or catalogue APIs. +6. **Exercise a provisioned authenticated-session wrapper without promoting it.** + `ratcheted_session.py` wraps the stateless PCEA transform with independent + directional chains, strict sequencing, transcript-bound HMAC authentication, + key separation, and verify-before-advance receive behavior. Its regression + harness covers wrong-secret rejection, transcript binding, replay, + reordering, failure rollback, key separation, and minimal public metadata. + It remains a proving-ground prototype pending harder attacks and independent + cryptographic review. + +7. **Preregister direct-arity falsification before changing the cipher.** + `ARITY_FREEZE_AND_TEST.md` freezes the current transform, classifies the + existing contributor relation as an A3 baseline to be verified, defines + A2/A3/A5/A7 research variants, matched PRF controls, exact lower-arity + partition tests, bounded reduced-entropy attack escalation, and an + independent multi-actor replay protocol. `ARITY_GEMINI_ADJUDICATION.md` + fixes Gemini as Actor D and forbids calling it until Actors A/B/C have + immutable result locks. The arity experiment must be executed without + modifying runtime PCEA. + ## What it cannot honestly do yet - It cannot claim PCEA-UCNS is secure public-key encryption. - It cannot turn a passing attack harness into a proof of hardness. -- It cannot replace authentication, nonce/session design, or key-management - requirements for a production channel. +- It cannot call the ratcheted-session prototype production secure or promote + it into `pcea/` without independent cryptographic review and the remaining + session-management attack work. +- It cannot replace nonce/session design, persistence/rollback design, + resynchronization, or key-management requirements for a production channel. +- It cannot claim seven-circle topology is heptadic cryptographic arity until + the frozen arity procedure earns that distinction. - It cannot ship a UCNS-KEM while the current documents mark the native UCNS key-establishment line as blocked or open. @@ -55,6 +79,8 @@ PCEA transform and speculative UCNS-native key establishment. | Key-space restriction | `quotient_attack.py`, `pruning_scaling.py` | Ask whether the private factor is unique inside a finite key space and whether pruning beats brute search | Useful distinction; cheap pruning measured as constant-factor, not a proof | | Structural readout breaks | `prefix_read_break.py`, `attack1_minkowski_break.py` | Look for direct reconstruction or algebraic inversion that bypasses key-space search | Negative gates: tested candidate families break | | Gonal/state communication | `gonal_architecture.py` | Explore PCEA-advanced private gonal rotation for state/token communication | Experimental; further bridge/attack gates remain | +| Provisioned authenticated session | `ratcheted_session.py` | Exercise ratcheting, directional key separation, transcript authentication, strict sequencing, and rollback-safe receive state around PCEA | Prototype harness survives its current regression set; not runtime or a security certification | +| Arity freeze/test | `ARITY_FREEZE_AND_TEST.md`, `ARITY_GEMINI_ADJUDICATION.md` | Determine actual direct state-key arity, exact higher-order residuals, PCEA-vs-PRF separation, attack scaling, then independent adjudication | Preregistered; Gemini must run fourth after A/B/C locks | | Candidate ledger | `candidate-ledger.json` | Track every UCNS-assisted PCEA candidate, its claim, public/private material, known attacks, harnesses, status, and next attack | Process guardrail; not a security proof | | Option D one-way-map gate | `one_way_map_gate.py` | Reject UCNS-native one-way-map sketches that lack quotient, prefix, set-basis, catalogue, enumeration, MITM, active, correctness, and scaling attack coverage | Attack-agenda gate; not a security proof | | Fed Option D UCNS map | `option_d_ucns_map.py` | Feed a face/payload spectrum projection into the Option D gate as the next concrete map to attack | Spec-level candidate; no security claim | @@ -62,10 +88,23 @@ PCEA transform and speculative UCNS-native key establishment. ## Potential next avenues -See `avenues.md` for a bounded research agenda: pre-shared and hybrid fallback -paths, UCNS-as-context binding, non-product one-way-map candidates, hidden -composition-order experiments, payload-depth tests, gonal bridge work, -commitment use, ratcheted PCEA sessions, and a candidate ledger. +See `avenues.md` for the wider bounded research agenda. For the provisioned +session specifically, the next boundary is adversarial: truncation, session-id +reuse, state-compromise/forward-secrecy limits, persistent rollback, concurrent +senders, and resynchronization behavior must be attacked before any runtime +promotion is considered. + +Before any independent cryptographic review treats seven-circle structure as a +security mechanism, execute `ARITY_FREEZE_AND_TEST.md`. Its first phase is the +minimal decisive test: prove or falsify a direct higher-order structural +residual under exact lower-arity partitioning and matched PRF controls. + +Use multiple actors only after the freeze. The fixed v1 sequence is: Codex +builds the preregistered harness and freezes Actor A; a second actor independently +reimplements the exact structural calculation and freezes Actor B; a third actor +attacks the frozen candidate and freezes Actor C; only after all three immutable +locks exist may Gemini be called as Actor D to adjudicate the frozen evidence. +Parallel co-authoring of one harness does not count as independent replay. ## How to use it @@ -81,10 +120,40 @@ Run the symmetric PCEA runtime gate only: python -m pytest -q tests/test_cipher.py tests/test_codec.py tests/test_kdf.py tests/test_instance.py tests/test_contract_spec.py ``` -Run the UCNS proving-ground tests: +Run the ratcheted-session regression harness only: + +```bash +python -m pytest -q tests/test_ratcheted_session.py +``` + +The prototype is loaded as a proving-ground file rather than imported from the +`pcea` package. Use it to attack the session design and preserve the runtime +boundary; do not treat it as a production API. + +Prepare the arity run by reading the complete preregistration before changing +any experimental code: + +```bash +cat pcea-ucns/ARITY_FREEZE_AND_TEST.md +``` + +The Codex handoff is embedded in that document. It begins with Phase 0 and +Phase 1 only and may escalate only according to the frozen outcome rules. + +Do not invoke the fourth actor during construction/replay/attack. After A/B/C +are finished and their lock records exist, prepare Gemini by reading: + +```bash +cat pcea-ucns/ARITY_GEMINI_ADJUDICATION.md +``` + +That addendum contains the exact Actor-D authority boundary, required input +packet, disagreement rules, output schema, and Gemini handoff prompt. + +Run the broader UCNS proving-ground tests: ```bash -python -m pytest -q tests/test_attack_harness.py tests/test_positional_attack.py tests/test_quotient_attack.py tests/test_prefix_read_break.py tests/test_projection_action_candidate.py tests/test_pruning_scaling.py tests/test_attack1_minkowski_break.py tests/test_three_factor_attack.py tests/test_factor_count_sweep.py tests/test_gonal_architecture.py +python -m pytest -q tests/test_attack_harness.py tests/test_positional_attack.py tests/test_quotient_attack.py tests/test_prefix_read_break.py tests/test_projection_action_candidate.py tests/test_pruning_scaling.py tests/test_attack1_minkowski_break.py tests/test_three_factor_attack.py tests/test_factor_count_sweep.py tests/test_gonal_architecture.py tests/test_option_family_specs.py tests/test_ratcheted_session.py ``` If `ucns` is not installed, UCNS-dependent tests skip by design. That skip is diff --git a/pcea-ucns/candidate-ledger.json b/pcea-ucns/candidate-ledger.json index e0965b4..2115c36 100644 --- a/pcea-ucns/candidate-ledger.json +++ b/pcea-ucns/candidate-ledger.json @@ -15,10 +15,10 @@ "public_material": ["algorithm parameters", "message numbers", "authenticated associated data", "optional UCNS context identifiers"], "private_material": ["provisioned seed", "traffic secrets", "last_state", "ratchet state"], "known_attacks": ["wrong-key recovery", "replay", "reordering", "truncation", "ratchet desynchronization", "unauthenticated state advancement"], - "harness": "pcea-ucns/option_family_specs.py", - "tests": ["tests/test_option_family_specs.py"], + "harness": "pcea-ucns/option_family_specs.py; pcea-ucns/ratcheted_session.py", + "tests": ["tests/test_option_family_specs.py", "tests/test_ratcheted_session.py"], "status": "recommended-engineering", - "next_attack": "Build a non-runtime ratcheted-session prototype and test replay, transcript mismatch, rollback-safe decrypt failure, and resynchronization metadata leakage." + "next_attack": "Attack the prototype for truncation, session-id reuse, state-compromise/forward-secrecy limits, persistent rollback, concurrency, and resynchronization behavior; require independent cryptographic review before runtime promotion." }, { "id": "hybrid-kem-keys-pcea", diff --git a/pcea-ucns/ratcheted_session.py b/pcea-ucns/ratcheted_session.py new file mode 100644 index 0000000..2157477 --- /dev/null +++ b/pcea-ucns/ratcheted_session.py @@ -0,0 +1,371 @@ +# ratios: loc_comments=223:97 imports_exports=7:4 calls_definitions=61:16 +# GPT/Claude generated; context, prompt Erin Spencer +"""Authenticated ratcheted-session prototype around the PCEA transform. + +This file belongs to the ``pcea-ucns`` proving ground. It is deliberately not +part of the shipped ``pcea`` package and makes no production-security claim. +It tests whether provisioned peers can add strict sequencing, transcript-bound +HMAC authentication, directional key separation, and verify-before-advance +state handling around the existing symmetric PCEA transform. + +Usage guidance:: + + alice = RatchetedSession( + provisioned_secret=b"A" * 32, + initial_state=[seed0], + session_id="demo", + role="initiator", + ) + bob = RatchetedSession( + provisioned_secret=b"A" * 32, + initial_state=[seed0], + session_id="demo", + role="responder", + ) + packet = alice.encrypt([seed1], aad=b"model-state") + recovered = bob.decrypt(packet, aad=b"model-state") + assert recovered == [seed1] + +Limitations: strict in-order delivery only; no persistence, resynchronization +protocol, nonce-generation policy, multi-sender concurrency, or independent +cryptographic review. A passing harness means only that these local invariants +survive the tests named here. +""" + +# === MODULE_BUILD === +# id: pcea_ratcheted_authenticated_session_prototype +# module_name: ratcheted_session +# module_kind: experiment +# summary: proving-ground wrapper adding strict sequencing, transcript HMAC authentication, directional key separation, and rollback-safe state advancement around PCEA +# owner: Erin Spencer +# public_surface: AuthenticationError, SequenceError, SessionPacket, RatchetedSession +# internal_surface: _derive_direction_root, _message_keys, _diversify_state, _packet_material, _advance +# auth_boundary: none +# storage_boundary: none +# network_boundary: none +# user_data_boundary: none +# admin_only: false +# tests: tests.test_ratcheted_session +# rollout: proving_ground_only +# rollback: delete this prototype and its test; do not alter shipped pcea runtime +# requires: pcea_cipher +# since: 2026-08-29 +# unresolved: independent cryptographic review; resynchronization protocol; nonce/session-id generation policy; multi-sender concurrency +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: pcea_session_round_trip +# given: paired peers share the same provisioned secret, initial PCEA state, session id, and opposite roles +# then: authenticated packets decrypt exactly and each direction advances independently +# class: correctness +# +# id: pcea_session_wrong_secret_rejected +# given: a packet is checked by a peer with a different provisioned secret +# then: authentication fails before plaintext recovery or receive-state advancement +# class: security +# +# id: pcea_session_transcript_binding +# given: packet ciphertext, associated data, session id, direction, sequence, or prior transcript differs +# then: the packet authentication tag differs or verification fails +# class: security +# +# id: pcea_session_replay_rejected +# given: an already accepted packet is presented again +# then: strict sequence validation rejects it +# class: security +# +# id: pcea_session_reordering_rejected +# given: a later packet arrives before the next expected packet +# then: strict sequence validation rejects it without preventing later in-order recovery +# class: security +# +# id: pcea_session_failure_does_not_advance +# given: authentication or sequence validation fails +# then: the same receiver can still accept the untampered next packet +# class: correctness +# +# id: pcea_session_key_separation +# given: one directional chain key and message sequence +# then: distinct labeled encryption-diversification and authentication keys are derived and both affect packet output +# class: security +# +# id: pcea_session_public_metadata_minimal +# given: a packet is emitted +# then: public metadata contains only version, session id, direction, sequence, ciphertext, and tag; no ratchet or PCEA state is exported +# class: security +# === END CONTRACTS === + +from __future__ import annotations + +import copy +import hashlib +import hmac +import json +from typing import NamedTuple + +from pcea.cipher import DEFAULT_WORD_BITS, decrypt_state, encrypt_state + +State = list[list[list[int]]] + +VERSION = "pcea-ratcheted-session-v0" +_INITIATOR = "initiator" +_RESPONDER = "responder" +_DIRECTION_FOR_ROLE = { + _INITIATOR: ("i2r", "r2i"), + _RESPONDER: ("r2i", "i2r"), +} + + +class AuthenticationError(ValueError): + """Raised when a packet's transcript-bound authentication fails.""" + + +class SequenceError(ValueError): + """Raised when a packet is replayed, skipped ahead, or uses the wrong direction.""" + + +class SessionPacket(NamedTuple): + """Public wire-shaped record for the proving-ground session.""" + + version: str + session_id: str + direction: str + sequence: int + ciphertext: State + tag: str + + +def _hmac(key: bytes, payload: bytes) -> bytes: + return hmac.new(key, payload, hashlib.sha256).digest() + + +def _derive_direction_root(secret: bytes, session_id: str, direction: str) -> bytes: + context = f"{VERSION}|root|{session_id}|{direction}".encode("utf-8") + return _hmac(secret, context) + + +def _message_keys(chain_key: bytes, sequence: int) -> tuple[bytes, bytes]: + sequence_bytes = sequence.to_bytes(8, "big", signed=False) + encryption_key = _hmac(chain_key, b"pcea-diversify|" + sequence_bytes) + authentication_key = _hmac(chain_key, b"authenticate|" + sequence_bytes) + return encryption_key, authentication_key + + +def _mask_for_cell( + encryption_key: bytes, + sequence: int, + seed_idx: int, + circle_idx: int, + tensor_idx: int, + word_bits: int, +) -> int: + byte_count = (word_bits + 7) // 8 + material = bytearray() + counter = 0 + prefix = ( + f"{VERSION}|cell|{sequence}|{seed_idx}|{circle_idx}|{tensor_idx}|".encode( + "utf-8" + ) + ) + while len(material) < byte_count: + material.extend(_hmac(encryption_key, prefix + counter.to_bytes(4, "big"))) + counter += 1 + return int.from_bytes(material[:byte_count], "big") & ((1 << word_bits) - 1) + + +def _diversify_state( + last_state: State, + encryption_key: bytes, + sequence: int, + word_bits: int, +) -> State: + modulus = 1 << word_bits + diversified: State = [] + for seed_idx, seed in enumerate(last_state): + diversified_seed: list[list[int]] = [] + for circle_idx, circle in enumerate(seed): + diversified_circle: list[int] = [] + for tensor_idx, value in enumerate(circle): + mask = _mask_for_cell( + encryption_key, + sequence, + seed_idx, + circle_idx, + tensor_idx, + word_bits, + ) + diversified_circle.append((value % modulus) ^ mask) + diversified_seed.append(diversified_circle) + diversified.append(diversified_seed) + return diversified + + +def _packet_material( + session_id: str, + direction: str, + sequence: int, + ciphertext: State, + aad: bytes, + transcript_hash: bytes, +) -> bytes: + payload = { + "aad": aad.hex(), + "ciphertext": ciphertext, + "direction": direction, + "sequence": sequence, + "session_id": session_id, + "transcript": transcript_hash.hex(), + "version": VERSION, + } + return json.dumps( + payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True + ).encode("utf-8") + + +def _advance( + chain_key: bytes, + transcript_hash: bytes, + material: bytes, + tag: bytes, +) -> tuple[bytes, bytes]: + next_transcript = hashlib.sha256(transcript_hash + material + tag).digest() + next_chain = _hmac(chain_key, b"ratchet|" + next_transcript) + return next_chain, next_transcript + + +class RatchetedSession: + """Strict in-order authenticated PCEA session for proving-ground use only.""" + + def __init__( + self, + provisioned_secret: bytes, + initial_state: State, + session_id: str, + role: str, + word_bits: int = DEFAULT_WORD_BITS, + ) -> None: + if not isinstance(provisioned_secret, bytes) or len(provisioned_secret) < 16: + raise ValueError("provisioned_secret must be at least 16 bytes") + if not initial_state: + raise ValueError("initial_state must be non-empty") + if not session_id: + raise ValueError("session_id must be non-empty") + if role not in _DIRECTION_FOR_ROLE: + raise ValueError("role must be 'initiator' or 'responder'") + if word_bits <= 0: + raise ValueError("word_bits must be positive") + + outbound, inbound = _DIRECTION_FOR_ROLE[role] + self._session_id = session_id + self._outbound_direction = outbound + self._inbound_direction = inbound + self._word_bits = word_bits + + self._send_chain = _derive_direction_root( + provisioned_secret, session_id, outbound + ) + self._recv_chain = _derive_direction_root( + provisioned_secret, session_id, inbound + ) + initial_transcript = hashlib.sha256( + f"{VERSION}|transcript|{session_id}".encode("utf-8") + ).digest() + self._send_transcript = initial_transcript + self._recv_transcript = initial_transcript + self._send_last = copy.deepcopy(initial_state) + self._recv_last = copy.deepcopy(initial_state) + self._send_sequence = 0 + self._recv_sequence = 0 + + @property + def send_sequence(self) -> int: + """Next outbound sequence number.""" + + return self._send_sequence + + @property + def receive_sequence(self) -> int: + """Next inbound sequence number.""" + + return self._recv_sequence + + def encrypt(self, state: State, aad: bytes = b"") -> SessionPacket: + """Authenticate one outbound PCEA state and advance only after success.""" + + sequence = self._send_sequence + encryption_key, authentication_key = _message_keys( + self._send_chain, sequence + ) + effective_last = _diversify_state( + self._send_last, encryption_key, sequence, self._word_bits + ) + ciphertext = encrypt_state(state, effective_last, self._word_bits) + material = _packet_material( + self._session_id, + self._outbound_direction, + sequence, + ciphertext, + aad, + self._send_transcript, + ) + tag = _hmac(authentication_key, material) + packet = SessionPacket( + VERSION, + self._session_id, + self._outbound_direction, + sequence, + copy.deepcopy(ciphertext), + tag.hex(), + ) + + next_chain, next_transcript = _advance( + self._send_chain, self._send_transcript, material, tag + ) + self._send_last = copy.deepcopy(state) + self._send_chain = next_chain + self._send_transcript = next_transcript + self._send_sequence += 1 + return packet + + def decrypt(self, packet: SessionPacket, aad: bytes = b"") -> State: + """Verify a packet before decrypting or advancing any receive state.""" + + if packet.version != VERSION or packet.session_id != self._session_id: + raise AuthenticationError("packet session binding failed") + if packet.direction != self._inbound_direction: + raise SequenceError("packet direction does not match this receiver") + if packet.sequence != self._recv_sequence: + raise SequenceError( + f"expected sequence {self._recv_sequence}, got {packet.sequence}" + ) + + sequence = self._recv_sequence + encryption_key, authentication_key = _message_keys( + self._recv_chain, sequence + ) + material = _packet_material( + self._session_id, + self._inbound_direction, + sequence, + packet.ciphertext, + aad, + self._recv_transcript, + ) + expected_tag = _hmac(authentication_key, material) + if not hmac.compare_digest(expected_tag.hex(), packet.tag): + raise AuthenticationError("packet authentication failed") + + effective_last = _diversify_state( + self._recv_last, encryption_key, sequence, self._word_bits + ) + state = decrypt_state(packet.ciphertext, effective_last, self._word_bits) + + next_chain, next_transcript = _advance( + self._recv_chain, self._recv_transcript, material, expected_tag + ) + self._recv_last = copy.deepcopy(state) + self._recv_chain = next_chain + self._recv_transcript = next_transcript + self._recv_sequence += 1 + return state +# ratios: loc_comments=223:97 imports_exports=7:4 calls_definitions=61:16 diff --git a/pcea_msdmd.ts b/pcea_msdmd.ts index b9424dd..665eb43 100644 --- a/pcea_msdmd.ts +++ b/pcea_msdmd.ts @@ -2,6 +2,110 @@ import { defineMsdmdCollection } from "./.agents/skills/msdmd/collection"; export default defineMsdmdCollection({ "declarations": [ + { + "block": "CONTRACTS", + "fields": { + "class": "correctness", + "given": "authentication or sequence validation fails", + "then": "the same receiver can still accept the untampered next packet" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_failure_does_not_advance" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "security", + "given": "one directional chain key and message sequence", + "then": "distinct labeled encryption-diversification and authentication keys are derived and both affect packet output" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_key_separation" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "security", + "given": "a packet is emitted", + "then": "public metadata contains only version, session id, direction, sequence, ciphertext, and tag; no ratchet or PCEA state is exported" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_public_metadata_minimal" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "security", + "given": "a later packet arrives before the next expected packet", + "then": "strict sequence validation rejects it without preventing later in-order recovery" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_reordering_rejected" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "security", + "given": "an already accepted packet is presented again", + "then": "strict sequence validation rejects it" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_replay_rejected" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "correctness", + "given": "paired peers share the same provisioned secret, initial PCEA state, session id, and opposite roles", + "then": "authenticated packets decrypt exactly and each direction advances independently" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_round_trip" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "security", + "given": "packet ciphertext, associated data, session id, direction, sequence, or prior transcript differs", + "then": "the packet authentication tag differs or verification fails" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_transcript_binding" + }, + { + "block": "CONTRACTS", + "fields": { + "class": "security", + "given": "a packet is checked by a peer with a different provisioned secret", + "then": "authentication fails before plaintext recovery or receive-state advancement" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_session_wrong_secret_rejected" + }, + { + "block": "MODULE_BUILD", + "fields": { + "admin_only": "false", + "auth_boundary": "none", + "internal_surface": "_derive_direction_root, _message_keys, _diversify_state, _packet_material, _advance", + "module_kind": "experiment", + "module_name": "ratcheted_session", + "network_boundary": "none", + "owner": "Erin Spencer", + "public_surface": "AuthenticationError, SequenceError, SessionPacket, RatchetedSession", + "requires": "pcea_cipher", + "rollback": "delete this prototype and its test; do not alter shipped pcea runtime", + "rollout": "proving_ground_only", + "since": "2026-08-29", + "storage_boundary": "none", + "summary": "proving-ground wrapper adding strict sequencing, transcript HMAC authentication, directional key separation, and rollback-safe state advancement around PCEA", + "tests": "tests.test_ratcheted_session", + "unresolved": "independent cryptographic review; resynchronization protocol; nonce/session-id generation policy; multi-sender concurrency", + "user_data_boundary": "none" + }, + "file": "pcea-ucns/ratcheted_session.py", + "id": "pcea_ratcheted_authenticated_session_prototype" + }, { "block": "MODULE_BUILD", "fields": { @@ -145,9 +249,209 @@ export default defineMsdmdCollection({ }, "file": "pcea/primes.py", "id": "pcea_primes" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_authentication_failure_does_not_advance_receive_state", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_failure_does_not_advance" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_failure_does_not_advance" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_labeled_keys_are_distinct_and_encryption_key_affects_ciphertext", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_key_separation" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_key_separation" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_packet_exports_no_resynchronization_or_ratchet_state", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_public_metadata_minimal" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_public_metadata_minimal" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_reordering_is_rejected_without_poisoning_receiver", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_reordering_rejected" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_reordering_rejected" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_replay_is_rejected", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_replay_rejected" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_replay_rejected" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_bidirectional_round_trip_and_independent_sequences", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_round_trip" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_round_trip" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_associated_data_and_session_id_bind_packet", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_transcript_binding" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_transcript_binding" + }, + { + "block": "CHECKS", + "fields": { + "call": "self::test_wrong_secret_fails_before_recovery", + "cleanup": "none", + "mutates": "none", + "proves": "pcea_session_wrong_secret_rejected" + }, + "file": "tests/test_ratcheted_session.py", + "id": "check_pcea_session_wrong_secret_rejected" } ], "edges": [ + { + "from": "check_pcea_session_failure_does_not_advance", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_failure_does_not_advance", + "to": "self::test_authentication_failure_does_not_advance_receive_state" + }, + { + "from": "check_pcea_session_failure_does_not_advance", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_failure_does_not_advance", + "to": "pcea_session_failure_does_not_advance" + }, + { + "from": "check_pcea_session_key_separation", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_key_separation", + "to": "self::test_labeled_keys_are_distinct_and_encryption_key_affects_ciphertext" + }, + { + "from": "check_pcea_session_key_separation", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_key_separation", + "to": "pcea_session_key_separation" + }, + { + "from": "check_pcea_session_public_metadata_minimal", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_public_metadata_minimal", + "to": "self::test_packet_exports_no_resynchronization_or_ratchet_state" + }, + { + "from": "check_pcea_session_public_metadata_minimal", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_public_metadata_minimal", + "to": "pcea_session_public_metadata_minimal" + }, + { + "from": "check_pcea_session_reordering_rejected", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_reordering_rejected", + "to": "self::test_reordering_is_rejected_without_poisoning_receiver" + }, + { + "from": "check_pcea_session_reordering_rejected", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_reordering_rejected", + "to": "pcea_session_reordering_rejected" + }, + { + "from": "check_pcea_session_replay_rejected", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_replay_rejected", + "to": "self::test_replay_is_rejected" + }, + { + "from": "check_pcea_session_replay_rejected", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_replay_rejected", + "to": "pcea_session_replay_rejected" + }, + { + "from": "check_pcea_session_round_trip", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_round_trip", + "to": "self::test_bidirectional_round_trip_and_independent_sequences" + }, + { + "from": "check_pcea_session_round_trip", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_round_trip", + "to": "pcea_session_round_trip" + }, + { + "from": "check_pcea_session_transcript_binding", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_transcript_binding", + "to": "self::test_associated_data_and_session_id_bind_packet" + }, + { + "from": "check_pcea_session_transcript_binding", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_transcript_binding", + "to": "pcea_session_transcript_binding" + }, + { + "from": "check_pcea_session_wrong_secret_rejected", + "kind": "calls", + "source_block": "CHECKS", + "source_id": "check_pcea_session_wrong_secret_rejected", + "to": "self::test_wrong_secret_fails_before_recovery" + }, + { + "from": "check_pcea_session_wrong_secret_rejected", + "kind": "claims_proves", + "source_block": "CHECKS", + "source_id": "check_pcea_session_wrong_secret_rejected", + "to": "pcea_session_wrong_secret_rejected" + }, { "from": "pcea_cipher", "kind": "owns", @@ -245,9 +549,22 @@ export default defineMsdmdCollection({ "source_block": "MODULE_BUILD", "source_id": "pcea_primes", "to": "none" + }, + { + "from": "pcea_ratcheted_authenticated_session_prototype", + "kind": "owns", + "source_block": "MODULE_BUILD", + "source_id": "pcea_ratcheted_authenticated_session_prototype", + "to": "Erin Spencer" + }, + { + "from": "pcea_ratcheted_authenticated_session_prototype", + "kind": "requires", + "source_block": "MODULE_BUILD", + "source_id": "pcea_ratcheted_authenticated_session_prototype", + "to": "pcea_cipher" } ], "gaps": [], - "repo": "pcea", - "source_commit": "fea83bb" + "repo": "pcea" }); diff --git a/pyproject.toml b/pyproject.toml index edd69cd..9810633 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,17 @@ [build-system] -requires = ["setuptools>=68"] +requires = ["setuptools>=77"] build-backend = "setuptools.build_meta" [project] name = "pcea" version = "0.1.0" description = "Prime Circular Encryption Algorithm — neural architecture state encryption" +readme = "README.md" authors = [ { name = "Erin Patrick Spencer", email = "wayseer@interdependentway.org" }, ] -license = { text = "MIT" } +license = "MIT" +license-files = ["LICENSE"] requires-python = ">=3.9" dependencies = [] diff --git a/tests/test_ratcheted_session.py b/tests/test_ratcheted_session.py new file mode 100644 index 0000000..500df70 --- /dev/null +++ b/tests/test_ratcheted_session.py @@ -0,0 +1,196 @@ +# ratios: loc_comments=103:51 imports_exports=4:8 calls_definitions=58:10 +# GPT/Claude generated; context, prompt Erin Spencer +"""Regression harness for the non-runtime ratcheted authenticated-session prototype.""" + +from __future__ import annotations + +import importlib.util +import pathlib + +import pytest + + +# === CHECKS === +# id: check_pcea_session_round_trip +# proves: pcea_session_round_trip +# call: self::test_bidirectional_round_trip_and_independent_sequences +# mutates: none +# cleanup: none +# +# id: check_pcea_session_wrong_secret_rejected +# proves: pcea_session_wrong_secret_rejected +# call: self::test_wrong_secret_fails_before_recovery +# mutates: none +# cleanup: none +# +# id: check_pcea_session_transcript_binding +# proves: pcea_session_transcript_binding +# call: self::test_associated_data_and_session_id_bind_packet +# mutates: none +# cleanup: none +# +# id: check_pcea_session_replay_rejected +# proves: pcea_session_replay_rejected +# call: self::test_replay_is_rejected +# mutates: none +# cleanup: none +# +# id: check_pcea_session_reordering_rejected +# proves: pcea_session_reordering_rejected +# call: self::test_reordering_is_rejected_without_poisoning_receiver +# mutates: none +# cleanup: none +# +# id: check_pcea_session_failure_does_not_advance +# proves: pcea_session_failure_does_not_advance +# call: self::test_authentication_failure_does_not_advance_receive_state +# mutates: none +# cleanup: none +# +# id: check_pcea_session_key_separation +# proves: pcea_session_key_separation +# call: self::test_labeled_keys_are_distinct_and_encryption_key_affects_ciphertext +# mutates: none +# cleanup: none +# +# id: check_pcea_session_public_metadata_minimal +# proves: pcea_session_public_metadata_minimal +# call: self::test_packet_exports_no_resynchronization_or_ratchet_state +# mutates: none +# cleanup: none +# === END CHECKS === + + +ROOT = pathlib.Path(__file__).parent.parent +_spec = importlib.util.spec_from_file_location( + "ratcheted_session", ROOT / "pcea-ucns" / "ratcheted_session.py" +) +ratchet = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(ratchet) + + +def _seed(base: int) -> list[list[int]]: + return [[base + circle * 7 + tensor for tensor in range(7)] for circle in range(7)] + + +def _pair( + secret: bytes = b"S" * 32, + session_id: str = "session-1", +) -> tuple[object, object]: + initial = [_seed(0)] + initiator = ratchet.RatchetedSession( + secret, initial, session_id=session_id, role="initiator" + ) + responder = ratchet.RatchetedSession( + secret, initial, session_id=session_id, role="responder" + ) + return initiator, responder + + +def test_bidirectional_round_trip_and_independent_sequences() -> None: + alice, bob = _pair() + + a1 = [_seed(100)] + packet_a1 = alice.encrypt(a1, aad=b"state") + assert bob.decrypt(packet_a1, aad=b"state") == a1 + + b1 = [_seed(200)] + packet_b1 = bob.encrypt(b1, aad=b"reply") + assert alice.decrypt(packet_b1, aad=b"reply") == b1 + + a2 = [_seed(300)] + packet_a2 = alice.encrypt(a2, aad=b"state") + assert bob.decrypt(packet_a2, aad=b"state") == a2 + assert alice.send_sequence == 2 + assert alice.receive_sequence == 1 + assert bob.send_sequence == 1 + assert bob.receive_sequence == 2 + + +def test_wrong_secret_fails_before_recovery() -> None: + alice, _ = _pair(secret=b"A" * 32) + _, wrong_bob = _pair(secret=b"B" * 32) + packet = alice.encrypt([_seed(10)]) + + with pytest.raises(ratchet.AuthenticationError): + wrong_bob.decrypt(packet) + assert wrong_bob.receive_sequence == 0 + + +def test_associated_data_and_session_id_bind_packet() -> None: + alice, bob = _pair() + packet = alice.encrypt([_seed(20)], aad=b"context-a") + + with pytest.raises(ratchet.AuthenticationError): + bob.decrypt(packet, aad=b"context-b") + assert bob.decrypt(packet, aad=b"context-a") == [_seed(20)] + + other_alice, _ = _pair(session_id="session-2") + other_packet = other_alice.encrypt([_seed(20)], aad=b"context-a") + assert other_packet.ciphertext != packet.ciphertext + assert other_packet.tag != packet.tag + + +def test_replay_is_rejected() -> None: + alice, bob = _pair() + packet = alice.encrypt([_seed(30)]) + assert bob.decrypt(packet) == [_seed(30)] + + with pytest.raises(ratchet.SequenceError): + bob.decrypt(packet) + + +def test_reordering_is_rejected_without_poisoning_receiver() -> None: + alice, bob = _pair() + first = alice.encrypt([_seed(40)]) + second = alice.encrypt([_seed(50)]) + + with pytest.raises(ratchet.SequenceError): + bob.decrypt(second) + + assert bob.decrypt(first) == [_seed(40)] + assert bob.decrypt(second) == [_seed(50)] + + +def test_authentication_failure_does_not_advance_receive_state() -> None: + alice, bob = _pair() + packet = alice.encrypt([_seed(60)], aad=b"bound") + tampered = packet._replace(tag=("00" * 32)) + + with pytest.raises(ratchet.AuthenticationError): + bob.decrypt(tampered, aad=b"bound") + + assert bob.receive_sequence == 0 + assert bob.decrypt(packet, aad=b"bound") == [_seed(60)] + assert bob.receive_sequence == 1 + + +def test_labeled_keys_are_distinct_and_encryption_key_affects_ciphertext() -> None: + chain = ratchet._derive_direction_root(b"K" * 32, "session-1", "i2r") + encryption_key, authentication_key = ratchet._message_keys(chain, 0) + assert encryption_key != authentication_key + + alice_a, _ = _pair(secret=b"A" * 32) + alice_b, _ = _pair(secret=b"B" * 32) + packet_a = alice_a.encrypt([_seed(70)]) + packet_b = alice_b.encrypt([_seed(70)]) + assert packet_a.ciphertext != packet_b.ciphertext + assert packet_a.tag != packet_b.tag + + +def test_packet_exports_no_resynchronization_or_ratchet_state() -> None: + alice, _ = _pair() + packet = alice.encrypt([_seed(80)]) + + assert packet._fields == ( + "version", + "session_id", + "direction", + "sequence", + "ciphertext", + "tag", + ) + forbidden = {"last_state", "chain_key", "ratchet", "traffic_secret", "resync"} + assert not forbidden.intersection(packet._fields) + assert len(packet.tag) == 64 +# ratios: loc_comments=103:51 imports_exports=4:8 calls_definitions=58:10