Gohr-style ResNet-1D-CNN distinguishers for reduced-round KeeLoq, trained as part of the cahlen/keeloq-python modernization project — a 2026 Python 3 / RTX 5090 port of a 2015 SAT-only cryptanalysis effort.
These are auxiliary models used to guide a Bayesian key-recovery search. They produce a scalar score indicating whether a ciphertext pair was generated from a chosen-input-difference distribution (vs. random), which the attack pipeline then uses to peel rounds off an M-round cipher back to a depth where a SAT solver can finish the job. See the GitHub repo for the attack pipeline and the keeloq neural CLI.
Checkpoints
File
Trained Depth
Attack Target
Val Accuracy
ROC-AUC
TPR @ FPR=1%
Status
d64.pt
56
64 rounds (peel K=8)
0.752
0.828
0.207
✅ Viable — 64-round full-key recovery in 1.6 s
Metrics are on 1 M held-out samples with a seed disjoint from training. Confusion matrix: TN=400997, FP=99003, FN=149119, TP=350881. Wall clock: 62 min on an RTX 5090. This is the only checkpoint we ship; we investigated deeper trained depths and found a sharp signal horizon that prevents them (see below).
Signal horizon — why d72 / d80 / d96 / d128 were not produced
We tested the distinguisher architecture at every depth from 56 to 120. Tiny-budget val-accuracies (best across multiple Δ candidates per depth):
Trained depth
Best val-acc
Status
56
0.688
Signal
57
0.539
Boundary (barely above noise)
58
0.534
Collapse
59
0.534
Collapse
60
0.528
Collapse
64, 68, 72, 76, 80
0.50 – 0.52
Collapse
88
0.517
Collapse
120
0.514
Collapse
The cliff is one round wide. Signal drops from 0.69 at depth 56 to 0.54 at depth 57 — essentially a step function. A single additional round of KeeLoq's NLF after depth 56 takes the residual differential feature below what this model family can discover, and the collapse is permanent for every deeper depth we tested.
We tested this with two distinct architectures side-by-side:
v1 (1×1-conv MLP-style, depth=5, width=512, ~3M params) — the architecture of d64.pt.
v2 (kernel-3 spatial-conv ResNet, depth=5, width=256, ~2M params) — the Gohr SPECK shape with explicit bit-neighbor inductive bias.
Both architectures reach val-acc 0.688 / 0.703 at depth 56 (equivalent signal), and both collapse identically at depths 88 and 120 (within statistical noise of 0.500). Adding spatial inductive bias does not unlock deeper depths. This strongly suggests the horizon is a property of KeeLoq's diffusion geometry at round counts ≥ 57, not an architectural choice.
Pushing the horizon further is still theoretically possible — the leading open directions are (1) 10–100× more training data at depth 57+, (2) distinguisher families spanning multiple depths combined with depth-matched beam search, (3) higher-order or multi-Δ differential approaches, (4) direct quantitative differential-trail analysis of KeeLoq to locate the theoretical minimum horizon — but all are out of scope for this release.
How the attack works (Gohr pattern on KeeLoq)
A distinguisher trained at depth D gives strong signal on D-round ciphertext pairs. For an M = D + K-round target cipher:
Collect N differential ciphertext pairs (c₀, c₁) where p₀ ⊕ p₁ = Δ.
For each of the outer K key bits, try both guesses: partially-decrypt all N pairs by one round, score the resulting (M−i)-round pairs with the distinguisher, accumulate log-evidence. Beam-search on accumulated evidence.
After K peels, the remaining D-round suffix has K key bits recovered (via beam) and 64−K still unknown; the XOR-aware ANF encoder + CryptoMiniSat handles those in seconds.
Every recovered key is cipher-verified (cipher.encrypt(p, k, M) == c) before reporting SUCCESS.
KeeLoq's 1-bit-per-round key schedule simplifies this substantially vs. Gohr's original SPECK attack (which guesses a 16-bit subkey chunk per peel): here every peel step is a binary decision, so the distinguisher only needs to resolve a binary signal.
Usage
git clone https://github.com/cahlen/keeloq-python
cd keeloq-python
uv sync --all-extras
# Download the viable distinguisher
mkdir -p checkpoints
hf download cahlen/keeloq-neural-distinguishers d64.pt --local-dir checkpoints/
# Attack a 64-round ciphertext (replace <c0>/<c1>/<pt>/<ct> with your bit strings)
uv run keeloq neural recover-key --checkpoint checkpoints/d64.pt \
--rounds 64 --diff-pair "<c0>:<c1>" --sat-pair "<pt>:<ct>" \
--beam-width 16 --sat-timeout 120
The CLI output is JSON with the recovered key, neural/SAT wall-clock split, bits recovered neurally, and verification status. Exit code 0 = SUCCESS, non-zero = terminal status (UNSAT / TIMEOUT / BACKTRACK_EXHAUSTED / etc).
Reproducibility
Each .pt file embeds its full TrainingConfig (rounds, delta, samples, batch_size, epochs, lr, weight_decay, seed, depth, width). Reproduce d64.pt from scratch with:
Loss: BCE. Optimizer: AdamW (wd=1e-5). LR schedule: cosine annealing from 2e-3.
Internal computation uses int64 lanes (PyTorch 2.11 + CUDA 13 doesn't implement rshift_cuda for uint32); public API stays uint32 for bit-pattern clarity.
Training data
Generated on-the-fly by keeloq.gpu_cipher.encrypt_batch — a bit-sliced CUDA KeeLoq that produces ~10⁶ encryptions/sec on a 5090. Per mini-batch of size N: the first N/2 rows are "real" (random key, random p₀, p₁ = p₀ ⊕ Δ, both encrypted under the same key); the second N/2 are "random" (independent plaintexts, independent keys). Labels balanced 50/50.