tests: read the SSKR refusals and the generated shares off the screen - #147
Merged
aido merged 1 commit intoAug 4, 2026
Conversation
Two ways the SSKR flow can refuse had never been read off a screen, on any device, and the two-button generation test asserted only the first four words of the first share -- which are the CBOR tag and byte-string header, the same four words any share of any set produces. A share entered twice is added. hex_check() accepts that pair on purpose: valid header, identical cross-share metadata, genuine CRC-32s, nothing in the frame to reject. Combination is what fails, so the screen is decided by `reconstructed` rather than by the verdict, and reporting "doesn't match" there would send the user looking for shares belonging to another seed when what happened is that they entered one share twice. The unit tests pin the refusal at the SSKR layer; nothing pinned the screen. A threshold of 1 over 3 shares is added. The equivalent touch test has its assertion on the message commented out -- the status page lasts three seconds and cannot be caught -- and asserts only that the application comes back to the previous screen, which a build that silently ignored the choice would also satisfy. Here the refusal waits for the user and can be read. Generation now reads every share back off the display instead of the first four words of the first one. The shares cannot be pinned, the share-set identifier being drawn at random, but their shape can: three shares for three asked for, 29 ByteWords each, a common share-set header, and member indices 0, 1 and 2, so that three copies of one share would not pass. nano.py gains collect_shares(), which reassembles a share across the pages bnnn_paging splits it over and stops at the "Quit" step, get_next_data() clamping the share index rather than wrapping it. Each test was run against a deliberately broken build and confirmed to fail there, the binary being compared by hash before and after each mutation so that a build which did not recompile could not pass for a passing test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two ways the SSKR flow can refuse had never been read off a screen, on any
device, and the generation test asserted only the first four words of the first
share.
A share entered twice — new
bolos_ux_sskr_hex_check()accepts a pair made of the same share twice, anddoes so deliberately: valid CBOR header, identical cross-share metadata,
genuine CRC-32s — there is nothing in the frame to reject. Combination is what
fails, so the screen the user is shown is decided by
reconstructedratherthan by the verdict:
Collapsing the first branch into the third is a plausible mistake — combination
failed, so the seed does not match, and reporting a mismatch looks locally
reasonable. It would be the wrong answer: "doesn't match" tells the user their
shares belong to some other seed, sending them to look for the wrong shares,
when what happened is that they entered one share twice and need a different
one. The unit tests pin the refusal at the SSKR layer; nothing pinned the
screen.
test_two_button_refusals.pycovers the neighbouring case, a sharewhose CRC is wrong, which is refused one layer earlier.
A threshold of 1 — new
test_sskr_unsupported_values.pydrives the equivalent touch path, but itsassertion on the message is commented out — the touch status page lasts three
seconds and the test cannot catch it. What it asserts instead is that the
application returns to the "Generate SSKR" screen, which a build that silently
ignored the choice would also satisfy. On these devices the refusal is an
ordinary flow that waits for the user, so the message can simply be read.
The other half of that guarantee is structural and already holds:
sskr_threshold_getter()bounds the threshold list by the share count justchosen, so a threshold above the number of shares is never offered and there is
no screen to assert about it.
Generation — strengthened
test_sskr_generate_two_button.pygenerated a 2-of-3 split and asserted "tunanext keep gyro", the CBOR tag and byte-string header. That leaves the split
itself unchecked: the same four words come out of any share of any set.
The shares still cannot be pinned — the 16-bit share-set identifier is drawn at
random, so two runs produce different ByteWords — but their shape can, so each
share is now read back off the display and held to four things:
able,acid,also— ByteWords for0x00,0x01,0x02. That byte is the reserved nibble followed by the member index(BCR-2020-011), so this says both that the reserved nibble is zero and that
the three shares are numbered rather than being three copies of one share,
which would make the split worthless while still looking like a split.
The flow ends on a step that quits the application, so the shares cannot be fed
back into "Check SSKR" without a second run of the emulator; no round-trip is
attempted.
Helpers
nano.pygainscollect_shares(), which reads a share back across the pagesbnnn_pagingsplits it over, and_lines()under it, ordering the screen by yso that a title can be told from the body. Walking stops at the "Quit" step:
the flow loops, but
get_next_data()clamps the share index rather thanwrapping it, so going further comes back to the last share and stays there.
Checking
Each test was run against a deliberately broken build and confirmed to fail
there. The binary is compared by hash before and after each mutation, because a
build that fails to recompile leaves the previous one in place and turns a
falsification into a test that passes for no reason:
!reconstructedsent to the no-match flowidxinstead ofidx + 1The duplicate-share mutation touches only the
!reconstructedbranch andleaves the
hex_check()one alone, so its failure also confirms which of thetwo the pair actually goes through.