Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deps/python-keepkey
56 changes: 56 additions & 0 deletions docs/release/7.14.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,62 @@ reproducible path that silently removes setup protections is not defensible.
| 5 — triage all residual findings | **met** | 30/30 triaged under index partitioning. 1 must-fix (#445, fixed), 3 already fixed, 0 false positives, 18 deferrals filed (#446–#463). |
| 6 — final adversarial round | **not started** | |

### Gate status at 818d8c83a — after the rc30 hardware round

Supersedes the 249f2d61b table above. Evidence for every row is in
`docs/release/rc30-evidence/`.

| Gate | State | What is actually true |
|---|---|---|
| 1 — #429 setup as a transaction | **met** | T1 on hardware. An injected `RecoveryDevice` mid-ceremony is refused (`Failure code=1`), `Cancel` gives `Aborted`, and the `EntropyAck` that used to be swallowed by the still-armed reset now returns `Not in Reset mode`. Device ends `initialized=False`, label `''` — the injected `pwned` left no residue. Phase B then completes with the user's own PIN and passphrase intact. |
| 2 — #428 approve exactly what is signed | **met for the paths measured** | `review*()` now propagates the verdict (#475, T2 8/8: a cancelled passphrase confirmation is not cached and the next request re-asks). Render overflow is no longer merely warned about — it is **paged** (#488, hardware 5/5). Source truncation stays warned, honestly. **Still unmeasured: custom-layout address/xpub screens** — `confirm_helper()` measures only `layout_standard_notification`, so `layout_notification_no_title_bold` bodies still clip with no warning (seen at T12 A4, tracked against #428). |
| 3 — independent re-exploitation | **not started** | Cannot be self-certified. |
| 4 — hardware OLED evidence | **met** | rc30 round, T1–T12 plus T13 for the pager. Twelve cards run on 7.14.2 silicon. Additionally, the screens are now captured in CI (#487): 345 → 375, including every suite this release changed. |
| 5 — triage all residual findings | **met** | 30/30 triaged. |
| 6 — final adversarial round | **not started** | |

### What the hardware round actually established

Two findings, and an honest accounting of the rest.

**AdvancedMode persists across a power cycle.** Measured at T8 leg C: still
enabled after a physical replug, which the code supports (`storage_commit`,
bit 12). **Both `efa18ad77` and `b53b63b0a` state in their commit messages that
"AdvancedMode is session state (it is off again after a power cycle)" and build
their risk analysis on it.** That is wrong on this build: once a user enables
blind-signing it stays enabled until explicitly revoked. The blast radius is
"until revoked", not "until unplugged". **Release-note correction.**

**#485** — the Cut Off screen promised to show the hidden text and redrew the
same clipped body. Fixed by #488. Found by reading `confirm_sm.c:441`, not by
the device.

**Most of the wire-level checks duplicated the emulator suite**, which asserts
the same `Failure` codes and signatures in CI. That is worth stating plainly so
the next round is aimed better: the screens and the press durations are what
hardware alone can establish, and they are now respectively automated (#487) and
measured (T13).

**One finding was retracted.** #484 and its fix PR #486 rested on "the device
confirmed with no press"; the tester was pressing. The refuting control — a
single untouched confirm screen — took one minute and belonged before the issue
was filed. A near-repeat during T13 was caught only by asking the tester what
they had done rather than inferring it from a timing.

### CI was measuring the wrong release

`FW_VERSION` detection used `grep -oP`, which BusyBox lacks, so it always fell
back to **7.14.0** on the 7.14.2 branch (#467, fixed in #487). Since SECTIONS
entries are selected by `ver_ge(fw_version, min_fw)`, **every test gated to
7.14.1 or later was excluded from the screenshot filter and from report
validation** for the whole release. That is why the suites this release changed
captured no screens.

Third instance of one pattern in this release: a check that ran, went green, and
measured something other than its name — after the Stage-1 skip (`ci-gate`,
#474) and the integration deadlock (#477). The per-test screenshot audit in #487
closes this variant.

### RELEASE INVARIANT: disclosure never yields to a stale test

**Tests adapt to newly required disclosure. Firmware disclosure is never removed to
Expand Down
65 changes: 65 additions & 0 deletions docs/release/rc30-evidence/T13-pager-hardware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# #488 pager — hardware validation

Device: 7.14.2, fw_hash `f19ad98f235aecd1` (= sha256 of the flashed
`firmware.keepkey.v7.14.2-b294e19-firmware.keepkey.bin`), uninitialised.
Vehicle: `Ping` with `button_protection`, 255-character digit-ramp body.

## VERDICT: PASS (5/5)

| # | check | result |
|---|---|---|
| Q1 | pages render on glass | PASS — `PING 1/3`, `2/3`, `3/3` |
| Q2 | a short click advances an intermediate page | PASS |
| Q3 | **a tap does NOT approve the final page** | **PASS** — 15s silence |
| — | a hold DOES approve the final page | PASS — `Success` at **1.650s** |
| Q4 | digit ramp continuous across seams | PASS |

The Q3/hold pair is the security property, and it is measured rather than
reported: the same finger, the same page, a tap yields silence and a hold yields
`Success` at 1.650s — consistent with `CONFIRM_TIMEOUT_MS` (1200) plus the
confirm animation.

Click to page, hold to approve. Reading what you are shown costs a click;
consenting to it still costs a hold.

## CI complements this, and neither is sufficient alone

CI captured the *rendering* (`PING 2/3`, `3/3`, and the short-body control
staying unnumbered) once the screenshot pipeline was fixed. It cannot capture
press durations — there is no physical button in the emulator. Hardware measured
the durations. Neither half proves the feature on its own.

`PING 1/3` came out blank in the CI capture. That is a capture race, not a
rendering defect: `confirm()` writes the first `ButtonRequest` before
`confirm_helper()` runs, and the pager counts pages before drawing page 1, so a
host that screenshots on receipt catches the canvas mid-work. It renders
correctly on glass. Cosmetic; worth fixing so CI shows all three pages.

## Process note — a false FAIL, caught by asking

The first Q3 run reported **FAIL: a CLICK approved the final page** and
recommended reverting #488. It was wrong. The test only observed "`Success`
arrived" and inferred the press type; the tester had held.

That is the identical error as fw #484 earlier the same day: inferring an
unobservable (how long a button was pressed) from an observable (a timing),
with no control able to separate them. It did not become a second retracted PR
only because the tester was asked what they did rather than the result being
taken at face value.

The instrumented re-run timestamps from the moment the final page is displayed,
so a sub-600ms approval and a >1.2s approval are distinguishable, and a tap that
approves nothing produces silence — a positive, checkable outcome instead of an
inference.

**Rule, restated:** never let a pass/fail criterion rest on how a human pressed a
button unless the two outcomes are separable in the data.

## Reproduce

cd deps/python-keepkey/tests
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 pager_q3.py # tap -> silence
PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 pager_hold.py # hold -> Success

Replug between runs; `hwpreflight.idle_or_die()` refuses to start on a dirty
device — it caught exactly that between these two runs.
Loading