Skip to content

docs(security): RC28 open findings and handoff - #370

Open
BitHighlander wants to merge 9 commits into
developfrom
docs/rc28-handoff
Open

docs(security): RC28 open findings and handoff#370
BitHighlander wants to merge 9 commits into
developfrom
docs/rc28-handoff

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Docs only. State of the rc28 release set after three audit rounds, so none of it has to be rediscovered.

RC28 is not merge-ready.

PR State
#366 RNG gate RCT boundary fixed; scope finding open (High)
#367 HASHES labels Green, but does not fix the stale manifest (High)
#368 storage V17 Lockout + CRC fixed, 23/23; CRC untestable on emulator (Med)
#369 roadmap 8 architectural blockers open

Fixed and verified this round

  • Wallet lockoutstorage_setPin_impl hardcoded PIN_KDF_V19; it creates the wrap and runs on wallet creation, every PIN change and the V1 upgrade path. Under V17 the flag cannot round-trip, so the next boot failed every PIN on an intact wallet.
  • CRC tailflash_temp[2570] covered 2568 bytes, leaving the record's final byte unprotected on a path reaching storage_wipe().
  • APT cutoff — inclusive-counter init with a following-matches cutoff shipped alpha 3.227e-9, ~3.5x looser than the documented 2^-30.
  • RCT window reset — a five-byte run straddling byte 512 was accepted.
  • A PR that never compiled, and a constant-fold keeping CI red.

Two things worth pulling out of the list

#367 is a live defect, not hygiene. Hashing before renaming and signing means the published full hash describes the unsigned draft. That is very likely the origin of the wrong 32155c11… v7.14.1 hash already found pinned in Vault's table.

"Only ROM remains open" on the roadmap is false. ROM measurement has to come after the substrate, authority model, updater invariant, certificate transcript, cross-variant preservation and policy integrity are decided — each changes what would be measured.

Also captured

The local build recipe, which took three rounds to work out (nanopb "rU" patch + python shim, PB_NO_PACKED_STRUCTS=1 on macOS ARM64, the SecAESSTM32 worktree workaround), and a warning that scripts/format-source-files.sh under clang-format 22.1.1 rewrites 40+ untouched files including vendored pb_*.c — pin the version before anyone runs it repo-wide.

Plus the mandatory release note: installing RC28 on a device that ran RC27 wipes it, which is the anti-rollback policy working as designed.

Records the state of the rc28 release set after three audit rounds, so the
next person does not rediscover any of it.

RC28 is not merge-ready. #368 is closest; #366 cannot claim wallet-wide RNG
assurance; #367 still publishes a manifest describing the unsigned draft
rather than the signed binary; #369 has eight architectural blockers and is a
draft, not a decided design.

Captures what was fixed and verified this round (the storage_setPin_impl
wallet lockout, the CRC tail that reached storage_wipe, the APT cutoff that
shipped 3.5x looser than documented, the RCT reset at the APT window
boundary, a PR that never compiled, and a constant-fold that kept CI red) so
none of it gets re-litigated, and then everything still open with severity,
the reason it matters, and a suggested order.

Two things worth surfacing beyond the list. #367's hash-before-signing
ordering is the likely origin of the wrong v7.14.1 hash already found pinned
in Vault's table, so it is a live defect rather than a hygiene item. And
"only ROM remains open" on the clear-sign roadmap is false: ROM measurement
has to come after the substrate, authority model, updater invariant,
certificate transcript, cross-variant preservation and policy integrity are
decided, because each of them changes what would be measured.

Also records the local build recipe, which took three rounds to work out --
the nanopb "rU" patch and `python` shim, PB_NO_PACKED_STRUCTS=1 for macOS
ARM64, and the SecAESSTM32 worktree workaround -- plus the warning that
scripts/format-source-files.sh under clang-format 22.1.1 rewrites 40+
untouched files, so the version must be pinned before anyone runs it
repo-wide.
… ran

The four code findings are closed; each keeps its original text so nobody
re-derives it, with the resolution underneath.

The correction worth reading: `firmware-unit` has never completed on a local
macOS build. SIX suites hang at 100% CPU on the shared kkconfirm_preload
driver -- Authenticator, Ethereum, Mayachain, Osmosis, Thorchain and
Confirmation -- and all six were reproduced at the merge base 6ae3b96 with
none of this work applied. So "the storage suite is 23/23" was always a
filtered result, and any past claim of a clean full local run should be
treated with suspicion. Excluding those six, #368 is 327/327 and #366 is
339/339, in about eight seconds.

Section 4 keeps the one thing that can still undo #367: the checklist now
tells key holders to regenerate the manifest from the signed binaries, and
the first real release is the test of whether that instruction is followed.
…e was missing three traps

The formatting note said the local clang-format disagrees with CI's and left
it there. It does — 22.1.1 on PATH against CI's 20 — but brew llvm@20 is
already installed at /opt/homebrew/opt/llvm@20/bin/clang-format, so the job
was never unfixable locally, only run with the wrong binary. Records the exact
CI check as a loop, and --lines=A:B for fixing one hunk instead of rewriting a
file. Two real violations were found that way.

Build recipe gains the QR-Code-generator submodule, a warning not to init
trezor-firmware recursively (it drags in micropython and tinyusb), and the
gtest filter without which the suite never finishes.
Both Stage-1 gates were red since 280f3b6 -- lint-format on an 82-column
comment, static-analysis on a deliberate always-false comparison -- and every
build and test job downstream was SKIPPED behind them. So the branch's green
storage suite was a local claim only; CI had not compiled it.

Records cppcheck as the second locally-runnable gate, including where the
report actually goes: --error-exitcode=1 under bash -e kills the step before
it cats cppcheck_report.txt, so the findings are only in the uploaded
artifact unless you run it yourself.
…ings

The previous revision said all four code findings were closed. Two were not,
and the head hashes for #366 and #368 were a commit behind.

Both reopened findings were the same mistake wearing different clothes -- a fix
correct as far as it went, summarised as if it went further. #366 gated a list
of call sites and called it "every key-material draw", missing the Orchard
RedPallas nonce in a submodule nobody re-audits. #369 removed the certificate's
three-way field inconsistency and called the result "canonical" while every
offset after 0x02 was still a '*'. Neither was a coding error; both were scope
claims outrunning the work, and both were found by reading the claim against
the code rather than against the diff. That is now stated at the top, because
it is the most useful thing in this document for whoever reads it next.

Also recorded: the #367 self-test passed throughout the period its gate was
inoperative, because it exercised the one input shape in which the bug is
invisible. A green check on a gate is evidence about the test, not the gate.

Heads, statuses, the #369 blocker table (8 downgraded to proposed, 5 widened to
gate both phases) and the test counts are updated to match.
…able

Round-2 review reopened both code PRs. #368 failed both ARM builds on
_Alignas and then failed catalog validation on a test name renamed in
280f3b6; #366 failed every link on random_uniform and layout_warning_static,
and its continuous RCT/APT state was never fed by the default path. So the
previous claim that #369 was the sole reason RC28 is not merge-ready was
wrong, and both are now listed as reopened-and-fixed rather than closed.

The header now carries the pattern instead of a fourth round of individual
corrections, because it has repeated three times in different shapes: work
correct as far as it goes, reported as if it went further. The instance worth
acting on is the last one -- "verified locally" meant an emulator build, and an
emulator build proves nothing about shipping firmware. _Alignas compiles under
clang and is rejected by the ARM toolchain; a missing link edge only appears
under the archive ordering the ARM target uses. Neither is visible from
cmake -DKK_EMULATOR=ON.

There is a mechanical fix rather than a resolution to try harder, so it is now
in the header AND the build recipe: a four-minute Docker cross-compile in CI's
own image, both variants, no toolchain install. Every ARM claim in this
document was reproduced that way before being written down, including the ROM
delta that corrects the earlier "no ROM" claim for the bootloader.

Also records that #367's structural gate is not blocked on obtaining signing
keys -- pubkeys.h already has all five; it needs a host-side secp256k1
verifier.
The header explains why; §5 is where someone actually looks before running
anything.
#366 made random32() abort on a failed RNG verdict, and the bootloader's stack
canary draws through it. A device with a dead RNG would abort inside the
bootloader: no verify, no boot, no reflash. Unrecoverable, in the component
that exists to recover from everything else.

Deliberately NOT fixed here. A firmware release does not update anyone's
bootloader, so it cannot reach an RC28 user, and fixing it in an RC28 PR means
bootloader changes reviewed under a firmware deadline that ship months later.
All bootloader edits were reverted; tools/bootloader/main.c is byte-identical
to develop.

Records both prototyped fixes and the objdump check to confirm it, because
'nothing calls it' has already been wrong twice in this module.
…ranch died

Coverage/RedPallas/degraded-RNG-recovery are one project, because each answer
changes the others. All three were built during RC28 and descoped.

The part worth keeping is why inverting the default failed, since it is not
obvious and someone will try again: ECDSA blinding runs on the VERIFY path, so
a checked draw aborts signatures_ok() before the display exists; making
blinding raw does not help because generate_k_random() rejection-loops forever
on a stuck source; and the raw patch is only safe while USE_RFC6979=1, since
the same switch otherwise controls the real signing nonce.

The prerequisite is a degraded-RNG recovery mode across firmware and bootloader
crypto -- a device that still boots, still verifies, still signs with RFC6979
to move funds out, and still refuses to mint new key material.

fix/blinding-draws-raw is deleted rather than left unmerged, so nobody repins
to it on the strength of a commit message the loop condition disproves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant