Skip to content

Commit fa4898d

Browse files
Merge pull request #50 from BitHighlander/feat/u5-storage-v20
test(storage): argue the V20 bump, and assert 18/19 stay unreadable
2 parents b8c4302 + 7ae7f0e commit fa4898d

2 files changed

Lines changed: 71 additions & 20 deletions

File tree

scripts/generate-test-report.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2813,16 +2813,29 @@ def _arg_shown(a):
28132813
['Wipe Device confirm', 'Import Recovery Sentence confirm',
28142814
'Home screen while locked out by the bitcoin-only band: no wallet',
28152815
'Bitcoin Account #0 / Address #0 after the band stamp is removed - the wallet is back']),
2816-
('U5', 'test_storage_version_gate', 'test_active_flash_format_is_v17',
2817-
'This build writes flash format V17',
2816+
('U5', 'test_storage_version_gate', 'test_active_flash_format_is_v20',
2817+
'This build writes flash format V20, and the bump is argued',
28182818
'An independent witness for the number the whole gate turns on. The compile-time '
28192819
'assert in storage.c compares STORAGE_VERSION against STORAGE_VERSION_LAST_SHIPPED - '
28202820
'two values in the same header, editable in one commit - so it cannot notice a release '
28212821
'that raises both. 7.15 deliberately reverted to V17; if V19 (or anything else) '
28222822
're-lands, this test fails and the bump has to be argued for in review rather than '
28232823
'discovered in the field. Reads the firmware sources, so it runs even where no '
28242824
'emulator can be restarted. No screen: it never touches the device, and the empty '
2825-
'list below says so.',
2825+
'list below says so.\n'
2826+
'7.16 moves to V20 to hold passkey credentials. It skips 18 and 19 because both were '
2827+
'ACTIVE formats in alpha builds before 6bebde7b2 reverted to V17 - 18 the clear-sign '
2828+
'identity block, 19 the PIN-KDF migration - so devices carrying those blobs exist, and '
2829+
'reusing a number would make this firmware PARSE one as passkey state rather than '
2830+
'refuse it. Upgrading preserves the wallet; downgrading to 7.15 or earlier erases it, '
2831+
'which is normal downgrade behaviour and is in the release note rather than left to be '
2832+
'discovered.',
2833+
[]),
2834+
('U5b', 'test_storage_version_gate', 'test_burned_versions_have_no_reader',
2835+
'Formats 18 and 19 have no reader, on purpose',
2836+
'The absence of a dispatch case is what sends a burned blob to the wipe path. That is '
2837+
'an easy thing to undo while tidying a switch statement, and undoing it would silently '
2838+
'restore the misparse - so the absence is asserted rather than assumed.',
28262839
[]),
28272840
('U6', 'test_storage_version_gate', 'test_version_never_drops_below_a_shipped_release',
28282841
'The version never goes backwards or into the band',

tests/test_storage_version_gate.py

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -398,30 +398,68 @@ def setUp(self):
398398
self.version = _define(self.h, "STORAGE_VERSION")
399399
self.last_shipped = _define(self.h, "STORAGE_VERSION_LAST_SHIPPED")
400400

401-
def test_active_flash_format_is_v17(self):
402-
"""alpha writes V17, the same format shipped v7.14.1.
401+
def test_active_flash_format_is_v20(self):
402+
"""7.16 writes V20. The bump is argued here, which is the point of the
403+
test: it asserts a LITERAL so that raising a header constant cannot
404+
quietly satisfy it.
403405
404-
This literal is an INDEPENDENT witness, on purpose. The compile-time
405-
assert in storage.c compares STORAGE_VERSION against
406+
The compile-time assert in storage.c compares STORAGE_VERSION against
406407
STORAGE_VERSION_LAST_SHIPPED -- two numbers in the same header, both
407-
editable in one commit, and raising LAST_SHIPPED to make a build
408-
compile is the exact edit docs/StorageVersionGate.md calls the highest
409-
severity review item in the file.
410-
411-
7.15 reverted the flash format from V19 back to V17 (6bebde7b2). V19
412-
migrated 17 -> 19 on the first boot, with no prompt, after which no
413-
downgrade was possible without a wipe; V18's clear-sign identity block
414-
is dead. The V19 serializer is still in the tree behind
415-
STORAGE_PIN_KDF_V19 == 0. If a release re-lands it, this test must
416-
fail and the bump must be argued for, not discovered in the field.
408+
editable in one commit -- so raising LAST_SHIPPED to make a build
409+
compile is the edit docs/StorageVersionGate.md calls the highest
410+
severity review item in the file. An independent witness is the only
411+
thing that catches it.
412+
413+
WHY 20 AND NOT 18. 18 was the clear-sign identity block and 19 the
414+
PIN-KDF migration. Both were ACTIVE, not merely drafted: e109404ee made
415+
19 live and 6bebde7b2 reverted the format to V17 for 7.15. Any device
416+
that ran an alpha build in that window carries a blob stamped 18 or 19
417+
whose layout has nothing to do with passkeys, and reading one as CTAP2
418+
state would misparse it rather than refuse it. 20 is unburned.
419+
420+
THE READER CHAIN. V17 blobs are read by storage_readV17 and restamped
421+
to STORAGE_VERSION; V20 blobs by storage_readV20. There is deliberately
422+
NO reader for 18 or 19: they remain in the ladder because the enum is
423+
positional and removing an entry renumbers everything after it, but a
424+
blob stamped with either falls through to the default and the device
425+
wipes. That is the documented behaviour for an unrecognised format and
426+
is strictly better than misparsing one.
427+
428+
ANTI-ROLLBACK. Once a device writes V20, installing 7.15 -- which knows
429+
only up to V17 -- maps the blob to StorageVersion_NONE and storage_init
430+
resets it. The device wipes. That is normal downgrade behaviour and is
431+
stated here so it is a known consequence rather than a field report.
432+
A signed UPGRADE never wipes; only going backwards does.
433+
434+
RELEASE NOTE. "7.16 changes the on-device storage format to hold
435+
passkey credentials. Upgrading preserves your wallet. Downgrading to
436+
7.15 or earlier will ERASE it -- back up your recovery phrase before
437+
downgrading."
417438
"""
418439
self.assertEqual(
419-
17, self.version,
420-
"STORAGE_VERSION is %d, not the V17 format 7.15 reverted to. A bump "
440+
20, self.version,
441+
"STORAGE_VERSION is %d, not the V20 format 7.16 introduces. A bump "
421442
"is a deliberate release act (docs/StorageVersionGate.md): confirm "
422443
"the reader chain, the anti-rollback story, and the release notes, "
423444
"then update this test." % self.version)
424-
self.assertEqual(17, self.last_shipped)
445+
self.assertEqual(20, self.last_shipped)
446+
447+
def test_burned_versions_have_no_reader(self):
448+
"""18 and 19 must never be parsed by 7.16.
449+
450+
They were real formats in alpha builds before the 7.15 revert, so
451+
devices carrying them exist. A reader for either would parse a
452+
clear-sign identity block or a PIN-KDF blob as passkey state. The
453+
absence of a case in the dispatch is what sends them to the wipe path,
454+
and this test is what stops one being added back by someone tidying up
455+
the switch.
456+
"""
457+
self.assertNotIn("case StorageVersion_18:", self.c,
458+
"18 is a burned format; a reader would misparse blobs "
459+
"written by pre-revert alpha builds")
460+
self.assertNotIn("case StorageVersion_19:", self.c,
461+
"19 is a burned format; a reader would misparse blobs "
462+
"written by pre-revert alpha builds")
425463

426464
def test_version_never_drops_below_a_shipped_release(self):
427465
"""Lowering STORAGE_VERSION wipes every device upgrading FROM a shipped

0 commit comments

Comments
 (0)