Skip to content

Commit 1a6723b

Browse files
cahlenclaude
andcommitted
docs: phase3b quickstart and CLAUDE.md integration
Add Phase 3b status section to CLAUDE.md covering neural pipeline location, Gohr-pattern constraint, checkpoint policy, hybrid-attack pair streams, and CUDA XOR limitation. Append Phase 3b Quick start section to README.md with train/recover-key examples and pair-stream explanation. Also apply ruff format to bench_attack.py and test_neural_e2e_64r.py (no logic changes). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cffc210 commit 1a6723b

4 files changed

Lines changed: 97 additions & 25 deletions

File tree

CLAUDE.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,44 @@
22

33
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44

5+
## Phase 3b status (2026)
6+
7+
Neural differential cryptanalysis pipeline in `src/keeloq/neural/`, following
8+
Gohr 2019 adapted for KeeLoq's 1-bit-per-round key schedule. CLI:
9+
10+
- `keeloq neural train --rounds N --delta 0xΔ --samples M --out <ckpt>`
11+
- `keeloq neural evaluate --checkpoint <ckpt> --rounds N`
12+
- `keeloq neural recover-key --checkpoint <ckpt> --rounds N --diff-pair <c0>:<c1> --sat-pair <pt>:<ct>`
13+
- `keeloq neural auto --rounds N --trained-depth D --samples M --checkpoint-out <path>`
14+
15+
**Gohr-pattern constraint.** A distinguisher trained at depth D gives strong signal only on
16+
D-round pairs. Peeling K rounds with a single distinguisher works well only for small K. For
17+
deep-round attacks, train a family of distinguishers at strategic depths (e.g. D=56 for a
18+
64-round attack peeling K=8 rounds). The `auto` subcommand handles this automatically.
19+
`neural-target-bits` controls how many prefix key-bits the neural phase covers before handing
20+
off to SAT.
21+
22+
**Checkpoint policy.** Checkpoints are NOT committed to the repo by default (external GPU
23+
contention prevented full training runs for d64.pt/d96.pt/d128.pt). Produce them via:
24+
25+
uv run keeloq neural auto --rounds 64 --trained-depth 56 \
26+
--samples 10000000 --pairs 512 --checkpoint-out checkpoints/d64.pt
27+
28+
The regression test `tests/test_neural_e2e_64r.py` auto-skips when `checkpoints/d64.pt` is
29+
absent. Benchmark runner (`benchmarks/bench_neural.py` + `benchmarks/neural_matrix.toml`) is
30+
smoke-safe against missing checkpoints.
31+
32+
**Hybrid-attack CLI.** The `recover-key` subcommand takes two distinct argument streams:
33+
`--diff-pair <c0>:<c1>` (differential ciphertext pairs for the neural distinguisher) and
34+
`--sat-pair <pt>:<ct>` (plaintext:ciphertext pairs for the SAT suffix). These are separate
35+
because a differential attack doesn't require known plaintexts; only the SAT phase does.
36+
37+
**CUDA XOR limitation.** `rshift_cuda` for uint32 is not implemented in PyTorch 2.11 + CUDA
38+
13. All XOR / delta application is done CPU-side before tensors are moved to the GPU.
39+
40+
Checkpoints in `checkpoints/` with training metadata embedded. Results under
41+
`docs/phase3b-results/`. Spec/plan in `docs/superpowers/`.
42+
543
## Phase 1 status (2026)
644

745
The repo is mid-migration from the 2015 Python 2 scripts (now in `legacy/`, frozen) to a

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,31 @@ Reproduce the 2015 README result (160 rounds, 25 hints, 2 pairs):
4444
See `docs/superpowers/specs/2026-04-22-phase1-foundation-design.md` for the
4545
design and `docs/superpowers/plans/2026-04-22-phase1-foundation-plan.md` for
4646
the implementation plan.
47+
48+
## Phase 3b (neural cryptanalysis) — Quick start
49+
50+
Train and attack end-to-end (GPU recommended, ~10 M samples):
51+
52+
uv run keeloq neural auto --rounds 64 --trained-depth 56 \
53+
--samples 10000000 --pairs 512 \
54+
--checkpoint-out checkpoints/d64.pt
55+
56+
Or, if you have a pre-trained checkpoint:
57+
58+
uv run keeloq neural recover-key --checkpoint checkpoints/d64.pt \
59+
--rounds 64 --diff-pair <c0>:<c1> --sat-pair <pt>:<ct> \
60+
--beam-width 16 --sat-timeout 120
61+
62+
**How it works (Gohr pattern).** A ResNet-1D-CNN distinguisher is trained at depth
63+
`--trained-depth D` (e.g. 56). It then peels K = rounds − D rounds (e.g. 8) one at a time
64+
via Bayesian beam search (`recover_prefix`), recovering the top K key bits. The remaining
65+
suffix bits are handed to Phase 1's XOR-aware encoder + CryptoMiniSat. Every recovered key
66+
is cipher-verified before reporting SUCCESS.
67+
68+
**Separate pair streams.** `--diff-pair` carries differential ciphertext pairs (c0:c1) used
69+
by the neural distinguisher. `--sat-pair` carries known plaintext:ciphertext pairs used by
70+
the SAT solver. They are distinct because a differential attack does not require known
71+
plaintexts.
72+
73+
See `docs/phase3b-results/benchmark.md` (generated by the benchmark runner after training)
74+
for neural-hybrid vs pure-SAT comparison across round counts.

benchmarks/bench_attack.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
Reads benchmarks/matrix.toml, runs each config, writes a CSV + markdown summary.
44
"""
5+
56
from __future__ import annotations
67

78
import csv
@@ -58,9 +59,7 @@ def run_matrix(matrix_path: Path, out_dir: Path) -> Path:
5859
pairs = [(p, encrypt(p, key, rounds)) for p in pts[:n_pairs]]
5960
hint_bits = run["hint_bits"]
6061
hints = (
61-
{i: (key >> (63 - i)) & 1 for i in range(64 - hint_bits, 64)}
62-
if hint_bits > 0
63-
else None
62+
{i: (key >> (63 - i)) & 1 for i in range(64 - hint_bits, 64)} if hint_bits > 0 else None
6463
)
6564

6665
print(f"[bench] running {run['name']!r}...", flush=True)
@@ -73,23 +72,24 @@ def run_matrix(matrix_path: Path, out_dir: Path) -> Path:
7372
timeout_s=run["timeout_s"],
7473
)
7574
print(
76-
f" -> status={result.status} "
77-
f"wall_time_s={result.solve_result.stats.wall_time_s:.3f}",
75+
f" -> status={result.status} wall_time_s={result.solve_result.stats.wall_time_s:.3f}",
7876
flush=True,
7977
)
80-
rows.append({
81-
"name": run["name"],
82-
"rounds": rounds,
83-
"num_pairs": n_pairs,
84-
"hint_bits": hint_bits,
85-
"encoder": run["encoder"],
86-
"solver": run["solver"],
87-
"status": result.status,
88-
"wall_time_s": f"{result.solve_result.stats.wall_time_s:.3f}",
89-
"num_vars": result.solve_result.stats.num_vars,
90-
"num_clauses": result.solve_result.stats.num_clauses,
91-
"num_xors": result.solve_result.stats.num_xors,
92-
})
78+
rows.append(
79+
{
80+
"name": run["name"],
81+
"rounds": rounds,
82+
"num_pairs": n_pairs,
83+
"hint_bits": hint_bits,
84+
"encoder": run["encoder"],
85+
"solver": run["solver"],
86+
"status": result.status,
87+
"wall_time_s": f"{result.solve_result.stats.wall_time_s:.3f}",
88+
"num_vars": result.solve_result.stats.num_vars,
89+
"num_clauses": result.solve_result.stats.num_clauses,
90+
"num_xors": result.solve_result.stats.num_xors,
91+
}
92+
)
9393

9494
csv_path = out_dir / "results.csv"
9595
with csv_path.open("w", newline="") as f:

tests/test_neural_e2e_64r.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Phase 3b 64-round hybrid attack using committed d64.pt (floor commitment)."""
2+
23
from __future__ import annotations
34

45
import time
@@ -34,21 +35,26 @@ def test_64_round_full_key_recovery() -> None:
3435

3536
n_pairs = 512
3637
gen = torch.Generator(device="cpu").manual_seed(31337)
37-
pts0 = torch.randint(0, 1 << 32, (n_pairs,), generator=gen,
38-
dtype=torch.int64).to(dtype=torch.uint32, device="cuda")
38+
pts0 = torch.randint(0, 1 << 32, (n_pairs,), generator=gen, dtype=torch.int64).to(
39+
dtype=torch.uint32, device="cuda"
40+
)
3941
pts0_cpu = pts0.cpu()
4042
delta_t = torch.tensor(delta, dtype=torch.uint32)
4143
pts1 = (pts0_cpu ^ delta_t).to("cuda")
4244
keys = torch.tensor(
4345
[[target_key & 0xFFFFFFFF, (target_key >> 32) & 0xFFFFFFFF]] * n_pairs,
44-
dtype=torch.uint32, device="cuda",
46+
dtype=torch.uint32,
47+
device="cuda",
4548
)
4649
c0 = encrypt_batch(pts0, keys, rounds=attack_depth)
4750
c1 = encrypt_batch(pts1, keys, rounds=attack_depth)
48-
diff_pairs = [(int(c0[i].item()) & 0xFFFFFFFF,
49-
int(c1[i].item()) & 0xFFFFFFFF) for i in range(n_pairs)]
50-
sat_pairs = [(int(pts0_cpu[i].item()) & 0xFFFFFFFF,
51-
int(c0[i].item()) & 0xFFFFFFFF) for i in range(n_pairs)]
51+
diff_pairs = [
52+
(int(c0[i].item()) & 0xFFFFFFFF, int(c1[i].item()) & 0xFFFFFFFF) for i in range(n_pairs)
53+
]
54+
sat_pairs = [
55+
(int(pts0_cpu[i].item()) & 0xFFFFFFFF, int(c0[i].item()) & 0xFFFFFFFF)
56+
for i in range(n_pairs)
57+
]
5258

5359
t0 = time.perf_counter()
5460
result = hybrid_attack(

0 commit comments

Comments
 (0)