diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a71a747f..d94785308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,24 @@ tag: the USB `bcdDevice` build counter (bumped on every behavior change), and ## [Unreleased] +### Added + +- **A status-only panel build (`--features panel`) for a screen board with no + touch.** It paints the device's own status — the build `bcdDevice`, the chip + serial, whether USB is attached, and uptime — on a plain 4-wire SPI LCD, and it + shows a **PRESS THE KEY** banner for as long as a user-presence wait is pending, + so the press the LED would blink for is named on the screen too. It adds no + presence source of its own: the BOOTSEL button still decides, the banner never + renders an operation or a relying party, and `shows_confirm()` stays false — this + is *not* the trusted display and every host ceremony is unchanged. The reference + board is ALIENTEK's RP2350A small-system board (1.14" ST7789V2, 240×135, on + SPI1). Panel wiring and SPI clock come from a board file's `[panel]` section, + defaulting to that board's pins (`cs`=9, `dc`=8, backlight=25 — active-low, + through the board's PNP high-side switch); SCK/MOSI on + GPIO10/11 are hard-wired. The feature is `dep:`-gated and mutually exclusive with + `display` (one geometry and one presence source per image), so a standard key is + unaffected. Build it `LED_KIND=none ... --features panel`. **`bcdDevice` → `0x09DB`.** + ## [0.4.11] - 2026-09-08 The catch-up release, and the one where the instruments were audited harder than diff --git a/assurance/board/PLAT-TIMER-002.toml b/assurance/board/PLAT-TIMER-002.toml index f6c0db9ea..6e3ee329e 100644 --- a/assurance/board/PLAT-TIMER-002.toml +++ b/assurance/board/PLAT-TIMER-002.toml @@ -12,7 +12,7 @@ # THE FIRST REWRITE CARRIED THE SAME DEFECT ONE NOTCH SMALLER, and the correction # is what the run below is built out of. It asked for a NO-TOUCH image "because # the run has to hold a presence wait open for the keepalives" -- and a no-touch -# build has no presence wait at all: firmware/src/presence.rs:237-240 is +# build has no presence wait at all: firmware/src/presence.rs:244-247 is # `#[cfg(feature = "no-touch")] { rsk_sdk::Presence::Confirmed }`, which is what # PLAT-BUILD-004 already says in its own words. The configuration deleted the # instrument the method named, so PASS was unreachable and the INCONCLUSIVE arm @@ -40,8 +40,8 @@ # series belong to which, rather than leaving that to a reader holding both. assumption = "PLAT-TIMER-002" -method = """Power a DEFAULT-image board and LEAVE IT ALONE for at least 8589.934592 s -- two comparator periods, 2 h 23 min 9.93 s -- rounding up, so 2 h 24 m is the figure to run. No replug, no reset. THE LENGTH IS DERIVED, and in two steps rather than one, because the first version of this record collapsed them and got the arithmetic wrong: it called two periods "the shortest run that crosses at least one boundary whatever that phase is", and that is false. The period is 2^32 microseconds = 4294.967296 s = 71 min 34.967296 s. (1) A run of length L from an unknown phase p crosses a boundary iff L > 2^32 - p, so the worst phase needs ONE period and not two -- 71 min 35 s already guarantees a crossing whatever the phase at boot was. (2) What the second period buys is a different thing, and it is the reason to run it: the first crossing lands at T - p, which is at most one period in, so two periods always leave a full period AFTER the crossing -- and FAIL below is a stall of up to a whole period, which a run ending at the crossing could not tell from a run that simply stopped. NOTHING MAY TALK TO THE BOARD INSIDE THE WINDOW, and that is load-bearing rather than convenient: every CTAPHID and CCID request arms a fresh short alarm the moment it lands (crates/rsk-usb/src/ctaphid.rs:775, crates/rsk-usb/src/ccid.rs:408), which re-arms the comparator and repairs a missed match before anything could see it, so a chatty host does not strengthen this run, it empties it. Prove the silence instead of assuming it: on a host, capture the bus for the whole window (usbmon, or Wireshark's USB capture) and keep the capture beside this record; on a charger there is no host to prove anything about, and that is the simpler arm for SILENCE and the weaker one for everything else -- no bus capture, no journal to read, and so no reboot instrument at all -- which is why that arm owes a supply that can be shown not to have dropped, a logging supply or a current trace kept beside this record, and answers INCONCLUSIVE without one. THE OBSERVABLE IS THE LED, and the board produces it alone -- no host, no finger, nothing held open. One render task per backend is spawned on the high-priority executor at firmware/src/main.rs:1022 (ws2812, the default), firmware/src/main.rs:960 (gpio) or firmware/src/main.rs:968 (pimoroni), and each loops on an alarm it re-arms for as long as the board has power: firmware/src/led.rs:618 and firmware/src/led.rs:727 are 5 ms, firmware/src/led.rs:692 is the gpio soft-PWM's 2 ms period. One of those -- or the worker's 16 ms button poll, firmware/src/worker.rs:307 -- is the earliest deadline in the queue at every instant of the run, so the arm that straddles each crossing is one of them and nothing has to be arranged for it to be. What a camera sees is the render: the ambient effect is a ~2 s breathing cycle once a host has configured the device and a twinkle before that, and the gpio and pimoroni backends blink 500 ms on, 500 ms off in both of those states. Record it end to end -- a camera with a running clock in frame, or a photodiode into a logging ADC. SAMPLE WELL ABOVE THE AMBIENT CADENCE, ten frames a second or better, and not the one frame a second the run's length makes tempting: the cadence is 0.5 s on the blinking backends, and a sampler near that rate aliases into a constant, which reads as the FAIL below rather than as an instrument that was too slow. WHAT THIS RECORD DOES NOT YET HAVE, said the way PLAT-FLASH-001 says it. The crossing is not announced by the device, so it is located by arithmetic over the recording's own clock rather than pinned to a device event, and the run therefore proves the ABSENCE of a stall across a window that certainly contains a boundary rather than pinning the survival to the boundary itself. The device's own clock IS readable, and the first version of this record was wrong to say that no instrument in this tree reports the device's uptime: every audit-journal entry is stamped with the ms-since-attach of firmware/src/usb_attach.rs:34-36 -- firmware/src/worker.rs:469 is where the dispatch hands it over -- and tools/rsk/audit.py:170 prints it as the `uptime` column. But reading it costs a command, and a command inside the window is the traffic that masks the defect, so it BRACKETS the run instead of sampling it -- and THE CLOSING STEP HAS TO WRITE, which the first version of this sentence got wrong: enumerate, `rsk audit enable` (PIN and one touch, before the window opens), go quiet, close with `rsk audit disable` (crates/rsk-fido/src/vendor.rs:518 appends the EV_AUDIT_CFG before the flag clears) or with `rsk audit verify` (crates/rsk-fido/src/journal.rs:559 appends an EV_CHECKPOINT; that arm additionally needs a provisioned OTP DEVK, so `disable` is the one every board can run), and only THEN `rsk audit log`, whose newest entry is what dates the run on the device's own counter as well as on the host's. A READ-ONLY CLOSE DATES NOTHING. AUDIT_READ appends no entry at all -- crates/rsk-fido/src/vendor.rs:481 is a bare `journal::vendor_read` -- so after a quiet window the newest entry is still the PRE-WINDOW enable, carrying the uptime it had then. And `EV_BOOT` is appended LAZILY, on the first journalled event of a power cycle (crates/rsk-fido/src/journal.rs:215-217), so a reboot inside the window followed by a read-only close writes nothing and the exported window is BYTE-IDENTICAL with and without one. MEASURED against this tree's own journal code, on the host rather than on a board, with enable at 1000 ms and 8589935 ms of silence: read-only close, newest `uptime` 1000 ms, and the rebooted run byte-identical to the quiet one; writing close, newest `uptime` 8590935 ms on the quiet run against a second `EV_BOOT` and 4294967 ms on the rebooted one. An uptime the device volunteers with no command behind it, or a boundary it signals on its own, is the second instrument this row still owes, and it is the one that would turn this from an absence into a location.""" -boot_config = """The DEFAULT image, and this time with no deviation from the shipped configuration at all -- the run needs no finger and no ceremony, so the no-touch build the first version asked for is not merely unnecessary, it is the build with nothing left to observe. Both halves of what is under test are set by this tree's own build: firmware/Cargo.toml enables embassy-rp's `time-driver`, which is what fixes the tick at 1 MHz and so fixes the comparator's period, and firmware/src/main.rs:524 takes `embassy_rp::config::Config::default()`, whose clk_ref is XOSC at 12 MHz. A build that changed either would be a different period and a different run. TWO THINGS THE IMAGE MUST NOT BE. Not `LED_KIND=none`, which compiles no render task at all and so removes the only observable an unattended board has; the default is `ws2812`, and gpio and pimoroni serve here equally. And not a board whose stored LED configuration has been dimmed to zero or put in steady mode, because both stop the render from changing while the alarms underneath it keep running, and this run reads the render. The factory `EF_LED_CONF` is what the expectation below is written against.""" +method = """Power a DEFAULT-image board and LEAVE IT ALONE for at least 8589.934592 s -- two comparator periods, 2 h 23 min 9.93 s -- rounding up, so 2 h 24 m is the figure to run. No replug, no reset. THE LENGTH IS DERIVED, and in two steps rather than one, because the first version of this record collapsed them and got the arithmetic wrong: it called two periods "the shortest run that crosses at least one boundary whatever that phase is", and that is false. The period is 2^32 microseconds = 4294.967296 s = 71 min 34.967296 s. (1) A run of length L from an unknown phase p crosses a boundary iff L > 2^32 - p, so the worst phase needs ONE period and not two -- 71 min 35 s already guarantees a crossing whatever the phase at boot was. (2) What the second period buys is a different thing, and it is the reason to run it: the first crossing lands at T - p, which is at most one period in, so two periods always leave a full period AFTER the crossing -- and FAIL below is a stall of up to a whole period, which a run ending at the crossing could not tell from a run that simply stopped. NOTHING MAY TALK TO THE BOARD INSIDE THE WINDOW, and that is load-bearing rather than convenient: every CTAPHID and CCID request arms a fresh short alarm the moment it lands (crates/rsk-usb/src/ctaphid.rs:775, crates/rsk-usb/src/ccid.rs:408), which re-arms the comparator and repairs a missed match before anything could see it, so a chatty host does not strengthen this run, it empties it. Prove the silence instead of assuming it: on a host, capture the bus for the whole window (usbmon, or Wireshark's USB capture) and keep the capture beside this record; on a charger there is no host to prove anything about, and that is the simpler arm for SILENCE and the weaker one for everything else -- no bus capture, no journal to read, and so no reboot instrument at all -- which is why that arm owes a supply that can be shown not to have dropped, a logging supply or a current trace kept beside this record, and answers INCONCLUSIVE without one. THE OBSERVABLE IS THE LED, and the board produces it alone -- no host, no finger, nothing held open. One render task per backend is spawned on the high-priority executor at firmware/src/main.rs:1071 (ws2812, the default), firmware/src/main.rs:1009 (gpio) or firmware/src/main.rs:1017 (pimoroni), and each loops on an alarm it re-arms for as long as the board has power: firmware/src/led.rs:618 and firmware/src/led.rs:727 are 5 ms, firmware/src/led.rs:692 is the gpio soft-PWM's 2 ms period. One of those -- or the worker's 16 ms button poll, firmware/src/worker.rs:307 -- is the earliest deadline in the queue at every instant of the run, so the arm that straddles each crossing is one of them and nothing has to be arranged for it to be. What a camera sees is the render: the ambient effect is a ~2 s breathing cycle once a host has configured the device and a twinkle before that, and the gpio and pimoroni backends blink 500 ms on, 500 ms off in both of those states. Record it end to end -- a camera with a running clock in frame, or a photodiode into a logging ADC. SAMPLE WELL ABOVE THE AMBIENT CADENCE, ten frames a second or better, and not the one frame a second the run's length makes tempting: the cadence is 0.5 s on the blinking backends, and a sampler near that rate aliases into a constant, which reads as the FAIL below rather than as an instrument that was too slow. WHAT THIS RECORD DOES NOT YET HAVE, said the way PLAT-FLASH-001 says it. The crossing is not announced by the device, so it is located by arithmetic over the recording's own clock rather than pinned to a device event, and the run therefore proves the ABSENCE of a stall across a window that certainly contains a boundary rather than pinning the survival to the boundary itself. The device's own clock IS readable, and the first version of this record was wrong to say that no instrument in this tree reports the device's uptime: every audit-journal entry is stamped with the ms-since-attach of firmware/src/usb_attach.rs:34-36 -- firmware/src/worker.rs:469 is where the dispatch hands it over -- and tools/rsk/audit.py:170 prints it as the `uptime` column. But reading it costs a command, and a command inside the window is the traffic that masks the defect, so it BRACKETS the run instead of sampling it -- and THE CLOSING STEP HAS TO WRITE, which the first version of this sentence got wrong: enumerate, `rsk audit enable` (PIN and one touch, before the window opens), go quiet, close with `rsk audit disable` (crates/rsk-fido/src/vendor.rs:518 appends the EV_AUDIT_CFG before the flag clears) or with `rsk audit verify` (crates/rsk-fido/src/journal.rs:559 appends an EV_CHECKPOINT; that arm additionally needs a provisioned OTP DEVK, so `disable` is the one every board can run), and only THEN `rsk audit log`, whose newest entry is what dates the run on the device's own counter as well as on the host's. A READ-ONLY CLOSE DATES NOTHING. AUDIT_READ appends no entry at all -- crates/rsk-fido/src/vendor.rs:481 is a bare `journal::vendor_read` -- so after a quiet window the newest entry is still the PRE-WINDOW enable, carrying the uptime it had then. And `EV_BOOT` is appended LAZILY, on the first journalled event of a power cycle (crates/rsk-fido/src/journal.rs:215-217), so a reboot inside the window followed by a read-only close writes nothing and the exported window is BYTE-IDENTICAL with and without one. MEASURED against this tree's own journal code, on the host rather than on a board, with enable at 1000 ms and 8589935 ms of silence: read-only close, newest `uptime` 1000 ms, and the rebooted run byte-identical to the quiet one; writing close, newest `uptime` 8590935 ms on the quiet run against a second `EV_BOOT` and 4294967 ms on the rebooted one. An uptime the device volunteers with no command behind it, or a boundary it signals on its own, is the second instrument this row still owes, and it is the one that would turn this from an absence into a location.""" +boot_config = """The DEFAULT image, and this time with no deviation from the shipped configuration at all -- the run needs no finger and no ceremony, so the no-touch build the first version asked for is not merely unnecessary, it is the build with nothing left to observe. Both halves of what is under test are set by this tree's own build: firmware/Cargo.toml enables embassy-rp's `time-driver`, which is what fixes the tick at 1 MHz and so fixes the comparator's period, and firmware/src/main.rs:573 takes `embassy_rp::config::Config::default()`, whose clk_ref is XOSC at 12 MHz. A build that changed either would be a different period and a different run. TWO THINGS THE IMAGE MUST NOT BE. Not `LED_KIND=none`, which compiles no render task at all and so removes the only observable an unattended board has; the default is `ws2812`, and gpio and pimoroni serve here equally. And not a board whose stored LED configuration has been dimmed to zero or put in steady mode, because both stop the render from changing while the alarms underneath it keep running, and this run reads the render. The factory `EF_LED_CONF` is what the expectation below is written against.""" expected = """The alarm comparator's wrap is invisible to everything above it. PASS = across a run of at least two periods with no host traffic inside the window, the recorded LED never stops changing -- no interval longer than 10 s in which it holds one value, against an ambient cycle of 1 s on the blinking backends and about 2 s on the breathing one -- and the bus capture (or the absence of a host) shows the window was quiet. AND THE BOARD NEITHER REBOOTED NOR LOST ITS SUPPLY: on the bracketed arm no second `EV_BOOT` in the post-window journal and a closing `uptime` accounting for the whole recording, on the charger arm a supply shown not to have dropped. That condition sits on the PASS and not only on the FAIL because the mirror is real: a board that browns out every thirty seconds never reaches a crossing at all, and its recording is a render that never stops changing -- this row's PASS over a run that tested nothing. FAIL = one interval of the order of the period itself, up to 71 min 35 s, in which the LED holds a single value and then resumes. That is what a missed match costs: the deadline is not late by a millisecond, it waits for the counter's low half to bring the same armed value round again, and it is the ONE alarm the whole queue is served by, so what stalls is every timer on the device at once. A FAIL REFUTES THE STATEMENT -- scripts/platform_gate.py:1337 maps `fail` to `refuted` -- so the interval alone is not enough, and two causes that have nothing to do with the comparator produce exactly it. A brown-out that recovers leaves a dead stretch and then repaints the render. A probe that halts a core and resumes it does the same, and TIMER0's DBGPAUSE makes that a DOCUMENTED way to stop this counter -- assurance/board/PLAT-TIMER-003.toml refuses a debugger by name and this row did not. So a FAIL is owed, beside the interval: no debugger attached at any point in the window; a supply shown not to have dropped; and, on the bracketed arm, no second `EV_BOOT` and a closing `uptime` accounting for the whole recording. INCONCLUSIVE = a run shorter than two periods; a recording with a gap in it, because a crossing that cannot be shown to have been watched is not a crossing that was survived; ANY host traffic inside the window, because a request re-arms the comparator and would repair a missed match before the LED could show it, which would make the run an observation of the host rather than of the wrap; a debugger attached at any point, since a halted core stops this counter by design and the run is then about the probe; a supply that dropped, or a second `EV_BOOT` in the post-window journal, or a closing `uptime` materially shorter than the recording -- a reboot, which the second `EV_BOOT` is what names, both repaints the render and restarts the comparator's phase, while a short `uptime` with NO such entry is the counter having stopped, the last arm below; neither the stall nor its absence is this row's to read on either; the charger arm run with no supply record, which has no reboot instrument at all; and an image or an LED configuration with no animation in it -- `LED_KIND=none`, brightness zero, steady mode -- which observes an absent instrument rather than a wrap; and a series that stops and NEVER resumes before the recording ends, which is not this row's FAIL at all -- a comparator whose counter has stopped can be observed neither to miss a match nor to make one, so this row has nothing left to read. WHAT THIS RECORDING SETTLES HERE AND NOT IN assurance/board/PLAT-TIMER-003.toml, which reads the same series for progress. The FAIL above is that row's PASS: a stall of the order of one period that ENDS is a counter that went on counting and brought the armed value round again. The stall that never ends is that row's FAIL and this row's INCONCLUSIVE, above. And the one arm both records answer the same way is a freeze beginning less than a period before the recording ends, inconclusive for both because nothing has yet told the two apart -- which is the second thing the second period buys, beside the one the method already names.""" outcome = "planned" board = "" diff --git a/assurance/board/PLAT-TIMER-003.toml b/assurance/board/PLAT-TIMER-003.toml index 313db82ec..2ab7e95fd 100644 --- a/assurance/board/PLAT-TIMER-003.toml +++ b/assurance/board/PLAT-TIMER-003.toml @@ -22,8 +22,8 @@ # INCONCLUSIVE, because a comparator whose counter has stopped can be observed # neither to miss a match nor to make one. assumption = "PLAT-TIMER-003" -method = """Take PLAT-TIMER-002's run -- a DEFAULT-image board powered and LEFT ALONE for at least 8589.934592 s with its LED recorded end to end -- and read the SAME series for a second question: did the render ever stop and stay stopped. TWO CONDITIONS THIS QUESTION ADDS TO THAT RUN, and neither is hygiene. NO DEBUGGER ATTACHED: TIMER0 carries a DBGPAUSE register whose own words are `Set bits high to enable pause when the corresponding debug ports are active`, so a probe that halts a core is a DOCUMENTED way to stop this very counter, and a run taken with one attached observes the probe instead of the silicon. AND A SUPPLY THAT DOES NOT DROP for the whole window: a brown-out reboots the board, and a reboot restarts the counter and repaints the render, which on the recording reads as a run that never froze. THE READING IS ARITHMETIC over the recording's own clock, the same clock that record locates its crossings by -- find the longest interval in which the sampled LED holds one value, and ask whether anything changed after it. Sample as that record says, ten frames a second or better and not the one frame a second the run's length makes tempting, for the same reason there and a sharper one here: a sampler near the ambient cadence aliases into a constant, and here that manufactures the FAIL out of an instrument that was too slow. THE DEVICE'S OWN CLOCK IS THE SECOND WITNESS, and for this row it is worth more than a date stamp. firmware/src/main.rs:800 stamps the origin at boot, immediately before the USB pull-up; firmware/src/usb_attach.rs:34-36 is the milliseconds since it, the clock the FIDO layer measures every window against; tools/rsk/audit.py:170 prints it as the `uptime` column. So a counter that stopped for an interval reports an uptime SHORTER than the wall-clock length of the recording, whether or not it restarted, and a board that rebooted reports one shorter still. Reading it costs a command, and a command inside the window is the traffic PLAT-TIMER-002 refuses, so it BRACKETS the run exactly as it does there, and THE CLOSING STEP IS A COMMAND THAT WRITES: enumerate, `rsk audit enable` (PIN and one touch) before the window opens, go quiet, then `rsk audit disable` -- or `rsk audit verify`, which additionally needs a provisioned OTP DEVK -- and only THEN `rsk audit log`. A READ-ONLY CLOSE CANNOT PRODUCE THIS ROW'S PASS AT ALL, and that is the defect this sentence carried until 2026-09-05. AUDIT_READ appends nothing (crates/rsk-fido/src/vendor.rs:481), so after a quiet window the newest entry is the PRE-WINDOW enable, its `uptime` is materially shorter than the recording ON A PERFECTLY HEALTHY BOARD, and the PASS below was unreachable by construction. The confound the witness exists to exclude was invisible in the same breath: `EV_BOOT` is appended lazily on the first journalled event of a power cycle (crates/rsk-fido/src/journal.rs:215-217), so a reboot inside a read-only window writes nothing and the exported window is byte-identical with and without one. MEASURED against this tree's journal code, on the host rather than on a board, with enable at 1000 ms and 8589935 ms of silence: read-only, newest `uptime` 1000 ms and the rebooted run byte-identical; closing with the write, 8590935 ms on the quiet run and, on the rebooted one, a SECOND `EV_BOOT` and 4294967 ms. On the charger arm there is no host to ask and no journal to read, and what stands in for the second witness is a supply that can be shown not to have dropped.""" -boot_config = """PLAT-TIMER-002's, unchanged, because it is the same run on the same image: the DEFAULT build, firmware/src/main.rs:524's `embassy_rp::config::Config::default()`, and a stored `EF_LED_CONF` still at the factory look. The same three images are refused for the same reason and it is sharper here -- `LED_KIND=none` compiles no render task, brightness zero paints nothing, steady mode stops the frame changing while the alarms underneath it keep running -- and each of those is an image in which the LED holds one value for the whole run with the counter in perfect health, which is this row's FAIL signature exactly. A run on any of them observes an absent instrument and answers INCONCLUSIVE, never FAIL.""" +method = """Take PLAT-TIMER-002's run -- a DEFAULT-image board powered and LEFT ALONE for at least 8589.934592 s with its LED recorded end to end -- and read the SAME series for a second question: did the render ever stop and stay stopped. TWO CONDITIONS THIS QUESTION ADDS TO THAT RUN, and neither is hygiene. NO DEBUGGER ATTACHED: TIMER0 carries a DBGPAUSE register whose own words are `Set bits high to enable pause when the corresponding debug ports are active`, so a probe that halts a core is a DOCUMENTED way to stop this very counter, and a run taken with one attached observes the probe instead of the silicon. AND A SUPPLY THAT DOES NOT DROP for the whole window: a brown-out reboots the board, and a reboot restarts the counter and repaints the render, which on the recording reads as a run that never froze. THE READING IS ARITHMETIC over the recording's own clock, the same clock that record locates its crossings by -- find the longest interval in which the sampled LED holds one value, and ask whether anything changed after it. Sample as that record says, ten frames a second or better and not the one frame a second the run's length makes tempting, for the same reason there and a sharper one here: a sampler near the ambient cadence aliases into a constant, and here that manufactures the FAIL out of an instrument that was too slow. THE DEVICE'S OWN CLOCK IS THE SECOND WITNESS, and for this row it is worth more than a date stamp. firmware/src/main.rs:849 stamps the origin at boot, immediately before the USB pull-up; firmware/src/usb_attach.rs:34-36 is the milliseconds since it, the clock the FIDO layer measures every window against; tools/rsk/audit.py:170 prints it as the `uptime` column. So a counter that stopped for an interval reports an uptime SHORTER than the wall-clock length of the recording, whether or not it restarted, and a board that rebooted reports one shorter still. Reading it costs a command, and a command inside the window is the traffic PLAT-TIMER-002 refuses, so it BRACKETS the run exactly as it does there, and THE CLOSING STEP IS A COMMAND THAT WRITES: enumerate, `rsk audit enable` (PIN and one touch) before the window opens, go quiet, then `rsk audit disable` -- or `rsk audit verify`, which additionally needs a provisioned OTP DEVK -- and only THEN `rsk audit log`. A READ-ONLY CLOSE CANNOT PRODUCE THIS ROW'S PASS AT ALL, and that is the defect this sentence carried until 2026-09-05. AUDIT_READ appends nothing (crates/rsk-fido/src/vendor.rs:481), so after a quiet window the newest entry is the PRE-WINDOW enable, its `uptime` is materially shorter than the recording ON A PERFECTLY HEALTHY BOARD, and the PASS below was unreachable by construction. The confound the witness exists to exclude was invisible in the same breath: `EV_BOOT` is appended lazily on the first journalled event of a power cycle (crates/rsk-fido/src/journal.rs:215-217), so a reboot inside a read-only window writes nothing and the exported window is byte-identical with and without one. MEASURED against this tree's journal code, on the host rather than on a board, with enable at 1000 ms and 8589935 ms of silence: read-only, newest `uptime` 1000 ms and the rebooted run byte-identical; closing with the write, 8590935 ms on the quiet run and, on the rebooted one, a SECOND `EV_BOOT` and 4294967 ms. On the charger arm there is no host to ask and no journal to read, and what stands in for the second witness is a supply that can be shown not to have dropped.""" +boot_config = """PLAT-TIMER-002's, unchanged, because it is the same run on the same image: the DEFAULT build, firmware/src/main.rs:573's `embassy_rp::config::Config::default()`, and a stored `EF_LED_CONF` still at the factory look. The same three images are refused for the same reason and it is sharper here -- `LED_KIND=none` compiles no render task, brightness zero paints nothing, steady mode stops the frame changing while the alarms underneath it keep running -- and each of those is an image in which the LED holds one value for the whole run with the counter in perfect health, which is this row's FAIL signature exactly. A run on any of them observes an absent instrument and answers INCONCLUSIVE, never FAIL.""" expected = """The counter advances for the whole of a bounded window, and the render is what says so. PASS = the recorded series has NO interval in which the sampled LED takes one value and never changes again -- every quiet stretch is followed by more change, right up to the last sample -- AND, on the bracketed arm, the `uptime` of the entry THE CLOSING COMMAND APPENDED is not materially shorter than the wall-clock length of the recording and no second `EV_BOOT` stands in the window, which together rule out a reboot that repainted the render; on the charger arm a supply shown not to have dropped stands in for both. FAIL = the series stops changing and stays stopped to the end of the recording, with at least one full period, 4294.967296 s, recorded after it stops. That is a counter that quit: every task in the queue is waiting on an alarm that can no longer match, and the three properties this row supports -- `EveryOpQuiesces`, `EveryWaitReleases`, `EveryWalkCloses` -- are false of every operation in flight and of every one begun afterwards, each being an EVENTUALLY claim on a device where nothing is ever again. THE SECOND WITNESS IS OWED HERE TOO, and was not until 2026-09-05: this arm sat under a PASS carrying an uptime condition and carried none itself, while a FAIL REFUTES the statement (scripts/platform_gate.py:1337 maps `fail` to `refuted`). So a FAIL needs, beside the frozen series: on the bracketed arm a closing `uptime` materially shorter than the recording and no second `EV_BOOT` saying the board rebooted, and on the charger arm a supply shown not to have dropped. WITHOUT IT THE ARM IS SATISFIED BY A HALTED PROGRAM RATHER THAN A STOPPED COUNTER, which is the nearest thing this tree has to this row's own failure and is reachable in it: firmware/Cargo.toml:75 is `panic-halt = \"1\"` and nothing arms a watchdog reset anywhere -- `WATCHDOG` appears only as the scratch register firmware/src/pin_lock.rs:54 writes -- so a panicking render task freezes the frame for ever with the counter in perfect health. AND IF THE CLOSING COMMAND DOES NOT COME BACK, that is a reading rather than a gap: a stopped counter wedges everything that awaits a `Timer`, so a device that still ENUMERATES and will not answer the close answers this arm the same way a short `uptime` does. A device that does not enumerate at all is the INCONCLUSIVE arm below instead, because a halted core produces that too and nothing outside the device tells the two apart. INCONCLUSIVE, and every arm of it but the last is about the RUN. That matters because the last one IS about the configuration and could otherwise be read as this row forbidding its own PASS: it does not, because the image `boot_config` names takes none of these arms -- the shipped default compiles a render task, animates it out of the factory `EF_LED_CONF`, needs no debugger and no host, and so produces the PASS above on any supply that holds. The arms: a recording with a gap, since a stretch nobody watched cannot be shown to have been a stretch of change; the device does not enumerate after the window, which is the halted program's other half and an arm of its own rather than a note on the one above -- a panic takes USB down with the render, so the second witness is unreadable at exactly the moment the reading calls for it, and a frozen frame with no journal behind it does not tell a stopped counter from a stopped program; a freeze beginning inside the last period, which is PLAT-TIMER-002's stall and this row's failure with nothing yet to tell them apart; a debugger attached, since DBGPAUSE makes a halted core a documented way to stop this counter and the run would then be about the probe; a supply that dropped, or a SECOND `EV_BOOT` in the post-window journal, since a reboot repaints the render and reads as continued progress -- and it is that entry and not a short `uptime` that names a reboot, because a stopped counter shortens the `uptime` as well and THAT case is this row's FAIL above, not this arm; a recording sampled at or near the ambient cadence, which aliases into a held value; and an image or an LED configuration with no animation in it, which observes an absent instrument. WHAT THE SHARED RECORDING DOES NOT SETTLE HERE, said because a reader who has just read the other record will carry its verdict across. PLAT-TIMER-002's PASS is not this row's: it is satisfied by a counter that stops one second after the recording ends. PLAT-TIMER-002's FAIL is this row's PASS, since a stall that resumes is a counter that went on counting. Only the never-resuming stall is this row's FAIL, and it leaves that row INCONCLUSIVE rather than joining it.""" outcome = "planned" board = "" diff --git a/assurance/board/PLAT-TRNG-002.toml b/assurance/board/PLAT-TRNG-002.toml index 9125bb168..8378d3f4d 100644 --- a/assurance/board/PLAT-TRNG-002.toml +++ b/assurance/board/PLAT-TRNG-002.toml @@ -27,9 +27,9 @@ # PLAT-TRNG-003, which is `accepted-risk` and owes no record; a run taken outside # desk conditions answers INCONCLUSIVE below rather than widening this one. assumption = "PLAT-TRNG-002" -method = """Capture the RAW pre-whitening ROSC stream from ONE board with a bench image, then estimate its min-entropy off-line. THE CAPTURE NEEDS NO INSTRUMENT BEYOND THE BOARD, which is the premise the row this replaces got wrong: write TRNG_DEBUG_CONTROL (0x138) with AUTO_CORRELATE_BYPASS, TRNG_CRNGT_BYPASS and VNC_BYPASS all SET, write SAMPLE_CNT1 (0x130) with 0, then read EHR_DATA0 through EHR_DATA5 in a loop. That is the bootrom's own sequence, printed in the RP2350 datasheet of 29 July 2025 at §12.12.4.1 p.1214 as `sample_cnt1 = 0; trng_debug_control = -1u` followed by reads of EHR_DATA, and shipping pico-sdk uses it in `src/rp2_common/pico_rand/rand.c`. Read EHR_DATA5 LAST on every block, because §12.12.3 makes that read what clears the result registers and a loop that does not will re-read a stale block. TAKE THE CAPTURE TWICE, and which one carries the verdict is the part a reader must not get backwards: once with SAMPLE_CNT1 at the shipped 1000, which is THE ONE THE CRITERION IS READ OFF, and once at 0, which is the bootrom's throughput setting and the MOST correlated stream this part offers, kept as the control. The bypass bits are independent of the sample period, so both captures are raw. Stream at least 1,000,000 consecutive raw samples in each, hash both captures, and record both hashes and the bench image's own sha256 here. THE ESTIMATE: run the SP800-90B §6.3 non-IID estimator suite over the capture and take the MINIMUM over the estimators, which is what that document defines H_min to be; run §5's IID permutation tests as well and record whether they pass, because an IID-track estimate over a source that fails them is the shape that produces a flattering number. THE SECOND HALF, on the same board in the same session, and it is the only way this project can put a RATE behind firmware/src/main.rs:609: re-enable the three checks, then read AUTOCORR_STATISTIC (0x134, p.1218) over a FIXED TEN-MINUTE WINDOW at `sample_count` 25, the driver default, and over a second ten-minute window at the shipped 1000, recording for each window BOTH the trip count and the number of EHR blocks the part produced inside it. THE BASIS IS WALL CLOCK AND NOT BLOCKS, and this field read `over the same number of blocks` until the clause was priced: SAMPLE_CNT1 is the cycles BETWEEN samples, so the two settings differ in throughput BY DESIGN, and PLAT-TRNG-001's own hardware observation — the 48-byte seed is two EHR blocks and took 30-105 s at 25 against about 1.5 s at 1000 — puts one block at roughly 15-50 s there against roughly 0.75 s here. Matching this record's own capture length would then be about 5,200 blocks, which at the 25 setting alone is somewhere between 22 and 75 hours by that same observation: not unsatisfiable, but the same family as the `0 < 0` conjunct one field over, a run nobody takes. TEN MINUTES is the smallest window that still yields hundreds of blocks at the shipped setting — roughly 800, against a dozen to forty at 25, and that gap is itself part of the answer — while both windows stay inside one bench session. Equal TIME is the honest comparison when the settings are designed to differ in throughput, and recording the block count beside the trip count leaves the per-block rate derivable anyway, so the change of basis costs the reader nothing it had. What stands behind that raise today is one wall-clock observation of a boot on one Waveshare unit, recorded at `2cdcc282e9605d601631b4b06727b2f746325727`, and a boot time is not a health-check failure rate. THE BENCH IMAGE IS NOT FOR A PROVISIONED KEY: its whole purpose is to hand a host the source the product whitens, so it goes on a throwaway board and the board is re-dragged afterwards.""" -boot_config = """A BENCH IMAGE, and its identity matters here more than on any other record because this is deliberately not the shipped firmware. Build it from this tree with the bypass writes the method names, no applet, no store and no USB identity that could be mistaken for the product; record its sha256 in `firmware_sha256` and keep the capture's hash beside it. THE SHIPPED IMAGE CANNOT TAKE THIS MEASUREMENT AT ALL, said because a reader may expect it to: firmware/src/main.rs:610 is the only `Trng::new` in the tree and it constructs the driver with all three bypasses CLEAR, so every byte the product can produce has already been through the von Neumann balancer and the two continuous checks. A run taken on the shipped image measures the WHITENED output, which is the test this row's statement says cannot answer the question, and it answers INCONCLUSIVE. Desk conditions, one board, one stepping recorded in `stepping`; temperature, supply and process are PLAT-TRNG-003's and this record must not be read as reaching them.""" -expected = """The raw source clears the floor the shipped whitening needs at the sample spacing the product uses, and the floor is THIS RECORD'S OWN rather than a vendor's. WHY HALF A BIT PER RAW BIT, argued here because the record this replaces cited a datasheet figure that does not exist, and argued in the direction a review had to correct once already: a von Neumann debiaser emits AT MOST one bit per two raw bits, so if h is the raw min-entropy per bit and R the balancer's output-per-input rate, the input min-entropy standing behind one whitened bit is h/R, and R is at most one half. At h = 0.5 that is at least one full bit, AT EVERY RATE THE BALANCER CAN ACHIEVE, and at a slower real rate more. SO THE FLOOR IS SUFFICIENT FOR THAT COUNTING BOUND AND IT IS NOT NECESSARY, which is the opposite of what this field said first: below 0.5 the bound stops holding, the device does not, and a real source at h = 0.25 sits at R near 0.13 and still clears one bit per whitened bit. Von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it; what does is CORRELATION between consecutive samples, which is the thing `sample_count` was raised to reduce and the thing the non-IID track is chosen to see. That is why the verdict is read off the capture at the SHIPPED spacing and not off the bootrom's period-0 one. It is deliberately not derived from §12.12.1's `approximately 7.5 kb/s of entropy`, which is a rate and not a per-bit figure. PASS = the SP800-90B §6.3 non-IID estimator suite, run over at least 1,000,000 consecutive raw samples captured with all three bypass bits SET and SAMPLE_CNT1 AT THE SHIPPED 1000, returns a minimum-over-estimators min-entropy of at least 0.5 bits per raw bit, AND the period-0 control capture and the AUTOCORR_STATISTIC readings at `sample_count` 25 and at 1000 were TAKEN over the two equal-time windows `method` names — trip count and block count both — and are recorded in `actual`. THE SECOND CONJUNCT IS A RECORDING OBLIGATION AND NOT A COMPARISON, and that is deliberate rather than loose: on a healthy unit the autocorrelation check may trip zero times at BOTH settings, so a clause demanding a strict decrease from zero would be a route no passing world walks — which is the defect this whole split exists to remove, reintroduced one conjunct further in. What the two numbers are for is the row's prose and not its verdict: a trip rate that does not fall at 1000 says the raise firmware/src/main.rs:609 made bought something other than what its comment claims, and that is worth recording whichever way it comes out. FAIL = the SHIPPED-SPACING capture's estimate comes back below 0.5 bits per raw bit, on the die this project owns and at the conditions it runs at. That refutes this row's statement in the statement's own terms — the floor is what the row claims and the floor is what was missed — and it deliberately claims no more. What it costs the device stays OPEN: the counting bound the floor secures is sufficient and not necessary, so a real balancer rate below one half spends more raw bits per whitened bit and may still carry a full bit into each, and nobody here has measured that rate. It is not a small open question, because every key, nonce, blinding factor and pinUvAuthToken drawn after boot rests on the seed either way and PLAT-TRNG-001's three continuous checks pass such a source by construction. A FAIL here is a demand for the rate measurement, not a proof the device is broken; overstating it is how the record this replaces came to name a figure nobody published. A FAIL is gate-forced to `refuted` (scripts/platform_gate.py:1337 maps `fail` to `refuted`), so it owes two things beside the number: TRNG_DEBUG_CONTROL and SAMPLE_CNT1 READ BACK after the capture and recorded, and the capture's own hash. A bypass write that did not take yields a WHITENED capture, and a whitened capture estimating low is a broken instrument rather than a weak source, which is the nearest thing to this row's failure that is reachable by accident. INCONCLUSIVE = any run that is not about the source at the spacing the statement names, and each arm is one way to get there: a verdict read off the period-0 CONTROL capture rather than the shipped-spacing one, which is a stream the device never draws and the mismatch this record was rewritten to remove; a capture shorter than the length the estimator suite is specified over; a capture taken through the shipped image, or with any bypass bit clear on read-back, which is whitened output and is the very test the statement says cannot answer this; a capture whose EHR_DATA5 read was not last on every block, since that read is what clears the result registers and a stale block repeats itself into the stream; an estimate taken on the IID track over a capture that fails SP800-90B §5's permutation tests, which is the flattering-number shape; a run at a temperature or a supply outside desk conditions, which is PLAT-TRNG-003's question and not this one's; and a result ASSEMBLED FROM MORE THAN ONE BOARD, because this row is one die: a spread across parts is PLAT-TRNG-003's claim and not this one's, so a second board is a second run of this row and never a wider one.""" +method = """Capture the RAW pre-whitening ROSC stream from ONE board with a bench image, then estimate its min-entropy off-line. THE CAPTURE NEEDS NO INSTRUMENT BEYOND THE BOARD, which is the premise the row this replaces got wrong: write TRNG_DEBUG_CONTROL (0x138) with AUTO_CORRELATE_BYPASS, TRNG_CRNGT_BYPASS and VNC_BYPASS all SET, write SAMPLE_CNT1 (0x130) with 0, then read EHR_DATA0 through EHR_DATA5 in a loop. That is the bootrom's own sequence, printed in the RP2350 datasheet of 29 July 2025 at §12.12.4.1 p.1214 as `sample_cnt1 = 0; trng_debug_control = -1u` followed by reads of EHR_DATA, and shipping pico-sdk uses it in `src/rp2_common/pico_rand/rand.c`. Read EHR_DATA5 LAST on every block, because §12.12.3 makes that read what clears the result registers and a loop that does not will re-read a stale block. TAKE THE CAPTURE TWICE, and which one carries the verdict is the part a reader must not get backwards: once with SAMPLE_CNT1 at the shipped 1000, which is THE ONE THE CRITERION IS READ OFF, and once at 0, which is the bootrom's throughput setting and the MOST correlated stream this part offers, kept as the control. The bypass bits are independent of the sample period, so both captures are raw. Stream at least 1,000,000 consecutive raw samples in each, hash both captures, and record both hashes and the bench image's own sha256 here. THE ESTIMATE: run the SP800-90B §6.3 non-IID estimator suite over the capture and take the MINIMUM over the estimators, which is what that document defines H_min to be; run §5's IID permutation tests as well and record whether they pass, because an IID-track estimate over a source that fails them is the shape that produces a flattering number. THE SECOND HALF, on the same board in the same session, and it is the only way this project can put a RATE behind firmware/src/main.rs:658: re-enable the three checks, then read AUTOCORR_STATISTIC (0x134, p.1218) over a FIXED TEN-MINUTE WINDOW at `sample_count` 25, the driver default, and over a second ten-minute window at the shipped 1000, recording for each window BOTH the trip count and the number of EHR blocks the part produced inside it. THE BASIS IS WALL CLOCK AND NOT BLOCKS, and this field read `over the same number of blocks` until the clause was priced: SAMPLE_CNT1 is the cycles BETWEEN samples, so the two settings differ in throughput BY DESIGN, and PLAT-TRNG-001's own hardware observation — the 48-byte seed is two EHR blocks and took 30-105 s at 25 against about 1.5 s at 1000 — puts one block at roughly 15-50 s there against roughly 0.75 s here. Matching this record's own capture length would then be about 5,200 blocks, which at the 25 setting alone is somewhere between 22 and 75 hours by that same observation: not unsatisfiable, but the same family as the `0 < 0` conjunct one field over, a run nobody takes. TEN MINUTES is the smallest window that still yields hundreds of blocks at the shipped setting — roughly 800, against a dozen to forty at 25, and that gap is itself part of the answer — while both windows stay inside one bench session. Equal TIME is the honest comparison when the settings are designed to differ in throughput, and recording the block count beside the trip count leaves the per-block rate derivable anyway, so the change of basis costs the reader nothing it had. What stands behind that raise today is one wall-clock observation of a boot on one Waveshare unit, recorded at `2cdcc282e9605d601631b4b06727b2f746325727`, and a boot time is not a health-check failure rate. THE BENCH IMAGE IS NOT FOR A PROVISIONED KEY: its whole purpose is to hand a host the source the product whitens, so it goes on a throwaway board and the board is re-dragged afterwards.""" +boot_config = """A BENCH IMAGE, and its identity matters here more than on any other record because this is deliberately not the shipped firmware. Build it from this tree with the bypass writes the method names, no applet, no store and no USB identity that could be mistaken for the product; record its sha256 in `firmware_sha256` and keep the capture's hash beside it. THE SHIPPED IMAGE CANNOT TAKE THIS MEASUREMENT AT ALL, said because a reader may expect it to: firmware/src/main.rs:659 is the only `Trng::new` in the tree and it constructs the driver with all three bypasses CLEAR, so every byte the product can produce has already been through the von Neumann balancer and the two continuous checks. A run taken on the shipped image measures the WHITENED output, which is the test this row's statement says cannot answer the question, and it answers INCONCLUSIVE. Desk conditions, one board, one stepping recorded in `stepping`; temperature, supply and process are PLAT-TRNG-003's and this record must not be read as reaching them.""" +expected = """The raw source clears the floor the shipped whitening needs at the sample spacing the product uses, and the floor is THIS RECORD'S OWN rather than a vendor's. WHY HALF A BIT PER RAW BIT, argued here because the record this replaces cited a datasheet figure that does not exist, and argued in the direction a review had to correct once already: a von Neumann debiaser emits AT MOST one bit per two raw bits, so if h is the raw min-entropy per bit and R the balancer's output-per-input rate, the input min-entropy standing behind one whitened bit is h/R, and R is at most one half. At h = 0.5 that is at least one full bit, AT EVERY RATE THE BALANCER CAN ACHIEVE, and at a slower real rate more. SO THE FLOOR IS SUFFICIENT FOR THAT COUNTING BOUND AND IT IS NOT NECESSARY, which is the opposite of what this field said first: below 0.5 the bound stops holding, the device does not, and a real source at h = 0.25 sits at R near 0.13 and still clears one bit per whitened bit. Von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it; what does is CORRELATION between consecutive samples, which is the thing `sample_count` was raised to reduce and the thing the non-IID track is chosen to see. That is why the verdict is read off the capture at the SHIPPED spacing and not off the bootrom's period-0 one. It is deliberately not derived from §12.12.1's `approximately 7.5 kb/s of entropy`, which is a rate and not a per-bit figure. PASS = the SP800-90B §6.3 non-IID estimator suite, run over at least 1,000,000 consecutive raw samples captured with all three bypass bits SET and SAMPLE_CNT1 AT THE SHIPPED 1000, returns a minimum-over-estimators min-entropy of at least 0.5 bits per raw bit, AND the period-0 control capture and the AUTOCORR_STATISTIC readings at `sample_count` 25 and at 1000 were TAKEN over the two equal-time windows `method` names — trip count and block count both — and are recorded in `actual`. THE SECOND CONJUNCT IS A RECORDING OBLIGATION AND NOT A COMPARISON, and that is deliberate rather than loose: on a healthy unit the autocorrelation check may trip zero times at BOTH settings, so a clause demanding a strict decrease from zero would be a route no passing world walks — which is the defect this whole split exists to remove, reintroduced one conjunct further in. What the two numbers are for is the row's prose and not its verdict: a trip rate that does not fall at 1000 says the raise firmware/src/main.rs:658 made bought something other than what its comment claims, and that is worth recording whichever way it comes out. FAIL = the SHIPPED-SPACING capture's estimate comes back below 0.5 bits per raw bit, on the die this project owns and at the conditions it runs at. That refutes this row's statement in the statement's own terms — the floor is what the row claims and the floor is what was missed — and it deliberately claims no more. What it costs the device stays OPEN: the counting bound the floor secures is sufficient and not necessary, so a real balancer rate below one half spends more raw bits per whitened bit and may still carry a full bit into each, and nobody here has measured that rate. It is not a small open question, because every key, nonce, blinding factor and pinUvAuthToken drawn after boot rests on the seed either way and PLAT-TRNG-001's three continuous checks pass such a source by construction. A FAIL here is a demand for the rate measurement, not a proof the device is broken; overstating it is how the record this replaces came to name a figure nobody published. A FAIL is gate-forced to `refuted` (scripts/platform_gate.py:1337 maps `fail` to `refuted`), so it owes two things beside the number: TRNG_DEBUG_CONTROL and SAMPLE_CNT1 READ BACK after the capture and recorded, and the capture's own hash. A bypass write that did not take yields a WHITENED capture, and a whitened capture estimating low is a broken instrument rather than a weak source, which is the nearest thing to this row's failure that is reachable by accident. INCONCLUSIVE = any run that is not about the source at the spacing the statement names, and each arm is one way to get there: a verdict read off the period-0 CONTROL capture rather than the shipped-spacing one, which is a stream the device never draws and the mismatch this record was rewritten to remove; a capture shorter than the length the estimator suite is specified over; a capture taken through the shipped image, or with any bypass bit clear on read-back, which is whitened output and is the very test the statement says cannot answer this; a capture whose EHR_DATA5 read was not last on every block, since that read is what clears the result registers and a stale block repeats itself into the stream; an estimate taken on the IID track over a capture that fails SP800-90B §5's permutation tests, which is the flattering-number shape; a run at a temperature or a supply outside desk conditions, which is PLAT-TRNG-003's question and not this one's; and a result ASSEMBLED FROM MORE THAN ONE BOARD, because this row is one die: a spread across parts is PLAT-TRNG-003's claim and not this one's, so a second board is a second run of this row and never a wider one.""" outcome = "planned" board = "" stepping = "" diff --git a/assurance/bundle/SEC-FIDO-001.toml b/assurance/bundle/SEC-FIDO-001.toml index 5ecd28b79..4a694fc3e 100644 --- a/assurance/bundle/SEC-FIDO-001.toml +++ b/assurance/bundle/SEC-FIDO-001.toml @@ -304,7 +304,7 @@ c_mac_flag = "no_authorization_bypass_begin_mac_is_the_flag SUCCESSFUL, 233 s gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — this slice's own row, and the ledger behind it is `assurance/ghost_actions.toml`; the invariant the gate is fixed to at module level is this property's" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the token axes this slice owns: the walk, soft-lock and window sites are held against their tags there and nowhere else" gate_assumption = "5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — re-derived, not edited, and the rot this time is the mirror of last time's. The earlier line went stale in the ASSIGNMENT while the total held; this one went stale in the TOTAL as well — `RekeyOrderModelled` joined the registry and `PowerOnClearsScratch2`'s TRUE arm grew by the two `Historical_Boot*` configurations that carry it — so both halves moved at once, and it was the pair rule that saw it, because the total is a bare integer and the bare-integer rule matches it anywhere in the derived line" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this slice, and the reason is recorded in the firmware-always-uv settling question" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this slice, and the reason is recorded in the firmware-always-uv settling question" gate_registry = "SEC-FIDO-001 NoAuthorizationBypass BOUNDED cfgs=50 mut=12 co=12 kani=4 fuzz=0 rust=2 test=0 — `cfgs` counts the configurations CHECKING this invariant, which is not evidence about it; `mut` and `co` are the mutants that redden it and the code twins that were killed, and the freshness axis carries what a growing `cfgs` costs. Re-derived: the count this line used to carry was one short" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-002.toml b/assurance/bundle/SEC-FIDO-002.toml index 613a80ab5..c5995d2d6 100644 --- a/assurance/bundle/SEC-FIDO-002.toml +++ b/assurance/bundle/SEC-FIDO-002.toml @@ -270,7 +270,7 @@ stops_harnesses_in_file = "the five harnesses the column does not count. Seven l bound_harnesses_counted = 2 stops_harnesses_counted = "any claim that the two counted harnesses are the strongest available. The count is what the column credits, not a judgement about which proofs are load-bearing" shipped_relation = "`git grep -n 'cancel_requested.store(true'` is exactly TWO sites — crates/rsk-device/src/presence.rs:120 inside `request_cancel`'s SCOPE_FIDO test and :131 inside `cancel_otp_wait`'s SCOPE_OTP test — and both carry a tag, so both halves of the cancel decision are covered, which is the thing that had to be confirmed rather than assumed. The four `set_wait_scope` call sites are firmware/src/worker.rs:434, :528, :663 and :665. Two files are credited to the `rust` column and one of them carries no tag; seven harnesses live in the proof file and two are counted. These bounds are counts of SITES and not shrinks of a shipped constant, because a review has no other unit — the same weakness SEC-FIDO-007 recorded about its own review row" -cfg = "none — every site above is unconditional production code on a button build; the two `cfg(feature = \"display\")` forwarders at firmware/src/presence.rs:263-269 are counted apart and are AS-XT-4's" +cfg = "none — every site above is unconditional production code on a button build; the two `cfg(feature = \"display\")` forwarders at firmware/src/presence.rs:270-276 are counted apart and are AS-XT-4's" features = "none" [[method]] @@ -305,7 +305,7 @@ bound_columns_gap = 24 stops_columns_gap = "everything on those twenty-four columns. A gap is the honest remainder — nobody has judged — and three of them are board columns that move `presence.source` off BOOTSEL, where the decision's producer is not the one this evidence was taken over" bound_board_columns_moving_presence = 3 stops_board_columns_moving_presence = "the three boards themselves. Naming which gap columns change the presence source says where the evidence would have to be retaken and never that it holds there" -shipped_relation = "this is the only row of the eleven-bundle programme with `out-of-scope` cells, and the disposition is DECIDED rather than open: on the four `no-touch` images `ButtonWait::wait` — the sole caller of `set_up_pending(true)` on a non-display build — is never called, because `ButtonPresence::wait` is `cfg(not(feature = \"no-touch\"))` and `request`/`request_ceremony` return `Presence::Confirmed` outright (firmware/src/presence.rs:202-258). So no presence DECISION is produced at all, and a property about where a decision may be applied has no subject. `gap` would say nobody has judged; `out-of-scope` says the claim is not made, which is the true state — and the tree already treats those builds that way, `firmware/Cargo.toml` saying 'Never ship a no-touch build' and `release-build.yml` refusing to release one. NOTE WHAT SURVIVES: `Arbiter` and all three tagged functions ARE compiled into those images; what is absent is the wait, not the arbitration. The 24 `gap` cells are the honest remainder and three of them are board columns that move `presence.source` off BOOTSEL" +shipped_relation = "this is the only row of the eleven-bundle programme with `out-of-scope` cells, and the disposition is DECIDED rather than open: on the four `no-touch` images `ButtonWait::wait` — the sole caller of `set_up_pending(true)` on a non-display build — is never called, because `ButtonPresence::wait` is `cfg(not(feature = \"no-touch\"))` and `request`/`request_ceremony` return `Presence::Confirmed` outright (firmware/src/presence.rs:209-265). So no presence DECISION is produced at all, and a property about where a decision may be applied has no subject. `gap` would say nobody has judged; `out-of-scope` says the claim is not made, which is the true state — and the tree already treats those builds that way, `firmware/Cargo.toml` saying 'Never ship a no-touch build' and `release-build.yml` refusing to release one. NOTE WHAT SURVIVES: `Arbiter` and all three tagged functions ARE compiled into those images; what is absent is the wait, not the arbitration. The 24 `gap` cells are the honest remainder and three of them are board columns that move `presence.source` off BOOTSEL" cfg = "none" features = "none — `no-touch` is the feature the four columns add, and no evidence in this bundle was built with it" @@ -414,7 +414,7 @@ s_display_gap = "the `display` build is the other open direction and it is the R gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — and the name it holds is a SIBLING's: `scripts/ghost_gate.py` fixes `INVARIANT` to `NoAuthorizationBypass` at module level, so this property's own ghost, which has two writers, is held by no route ledger and only by its mutants' verdict ratchet" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the token axes and none of the tagged owners this bundle names is in it; transcribed because the roster rule demands every gate line whether or not this property moved it" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — three of the five have their TRUE arm run against this property, in AlwaysUv.cfg, PermWide.cfg and ForceChange.cfg, which is one more than either predecessor bundle can say; PowerOnClearsScratch2 and RekeyOrderModelled are the two that do not, because both live in RSKeyBootHardening and the overlap with the configurations checking this invariant is empty. Re-derived after `RekeyOrderModelled` entered the registry: it moved the roster without moving that overlap" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a recorded model tier, a bounded tier and a host-test slice build no image, so nothing here settles a cell. This property owns four of those out-of-scope cells and they are the only ones in its row" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a recorded model tier, a bounded tier and a host-test slice build no image, so nothing here settles a cell. This property owns four of those out-of-scope cells and they are the only ones in its row" gate_registry = "assurance-gate: SEC-FIDO-002 BOUNDED, cfgs=42 mut=5 co=5 kani=2 fuzz=0 rust=2 test=0 — and `rust=2` is this bundle's first finding: `assurance_gate.grep_word` returns `f.name`, so the axis for this property is the list `['presence.rs', 'presence.rs']`, two entries carrying the SAME string, one of which holds three genuine `Refines` tags and the other none. `kani=2` undercuts a proof file of seven harnesses, two of them the invariant's clauses and two more their non-vacuity guards" # ---- 6. Raw artifact or log -------------------------------------------------- @@ -535,7 +535,7 @@ registered = "yes — assurance/platform.toml PLAT-PRES-002, pending, class mode id = "AS-XT-4" statement = "The `display` build implements the one-hold-one-ceremony latch somewhere else: it compiles `ButtonWait` out entirely and the panel's own release debounce takes over." kind = "build configuration" -discharger = "the firmware-display and waveshare-touch-lcd matrix cells, which are `gap` for this property today. firmware/src/presence.rs:99-101 gates the button backend on `not(feature = \"display\")` and :163-166 swaps `Presence` to `crate::display::TouchPresence`; crates/rsk-display/src/presence.rs:45-48 clears the stale cancel in the panel's OWN loop, not in `ButtonWait::wait`, which is why formal/RSKeySecurityState.tla:461-466 says `OpenWaitFor` stands for two different drops and BugNoDropStaleCancelAtEntry removes both at once. The module states the direction at :468-472 — NARROWER than a display build, so it keeps a defence that build implements elsewhere rather than one it does not have" +discharger = "the firmware-display and waveshare-touch-lcd matrix cells, which are `gap` for this property today. firmware/src/presence.rs:103-105 gates the button backend on `not(feature = \"display\")` and :163-166 swaps `Presence` to `crate::display::TouchPresence`; crates/rsk-display/src/presence.rs:45-48 clears the stale cancel in the panel's OWN loop, not in `ButtonWait::wait`, which is why formal/RSKeySecurityState.tla:461-466 says `OpenWaitFor` stands for two different drops and BugNoDropStaleCancelAtEntry removes both at once. The module states the direction at :468-472 — NARROWER than a display build, so it keeps a defence that build implements elsewhere rather than one it does not have" expressible = "no — a feature flag's effect on which module runs the ceremony, not a boolean the model reads" registered = "yes — assurance/platform.toml PLAT-BUILD-003, pending, class build-configuration. Its discharge_owner is `contributor` and not the `maintainer` this bundle proposed: the route is the two matrix cells and the display build's own ceremony code, all of it host-testable in crates/rsk-display, and the glass is PLAT-DISPLAY-001's" @@ -543,7 +543,7 @@ registered = "yes — assurance/platform.toml PLAT-BUILD-003, pending, class bui id = "AS-XT-5" statement = "On the four `no-touch` images no presence DECISION is produced at all, which is why this property's cells there are `out-of-scope` and not `gap` — and the arbitration code is still compiled in, so 'the decision is gone' and 'the code is gone' are different sentences." kind = "build configuration" -discharger = "reading the cfg arms, and it is decided rather than open. firmware/src/presence.rs:202-258: `request` and `request_ceremony` return `Presence::Confirmed` unconditionally under the feature, `poll_pressed` returns false, and `ButtonPresence::wait` is `cfg(not(feature = \"no-touch\"))`, so `ButtonWait::wait` — the only caller of `set_up_pending(true)` on a non-display build — is never reached and `up_pending()` is constantly false. The three tagged owners ARE in those images. firmware/Cargo.toml says 'Never ship a no-touch build' and release-build.yml refuses to release one" +discharger = "reading the cfg arms, and it is decided rather than open. firmware/src/presence.rs:209-265: `request` and `request_ceremony` return `Presence::Confirmed` unconditionally under the feature, `poll_pressed` returns false, and `ButtonPresence::wait` is `cfg(not(feature = \"no-touch\"))`, so `ButtonWait::wait` — the only caller of `set_up_pending(true)` on a non-display build — is never reached and `up_pending()` is constantly false. The three tagged owners ARE in those images. firmware/Cargo.toml says 'Never ship a no-touch build' and release-build.yml refuses to release one" expressible = "no — a cargo feature's effect on reachability, not a model constant" registered = "yes — assurance/platform.toml PLAT-BUILD-004, pending, class build-configuration" diff --git a/assurance/bundle/SEC-FIDO-003.toml b/assurance/bundle/SEC-FIDO-003.toml index f594cd981..646011e77 100644 --- a/assurance/bundle/SEC-FIDO-003.toml +++ b/assurance/bundle/SEC-FIDO-003.toml @@ -397,7 +397,7 @@ f_fuzz = "1, and it is one FILE. `fuzz/fuzz_targets/fido_session.rs:1430-1433` a gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle and transcribed because the roster demands every gate line. The ghost THIS property is about is a second name in the same mechanism: TokenBypass, SetPinWrite, ChangePinWrite and CmBeginViaPpuat write it, and no gate derives that count at all" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the token axes and this property is a token property, yet none of its six tagged owners is in any of the six axes: the ledger is about who WRITES the token record and this invariant is about what a write must leave behind" gate_assumption = "assumption-gate: 5 standing assumption(s) — AlwaysUvShipped, ForceChangeModelled, PowerOnClearsScratch2, RekeyOrderModelled and WidePerms. Three of the five are checked BOTH WAYS against this invariant, which is more than most of the registry has, and the other two are checked on neither arm by any configuration that checks this property. The per-constant TRUE/FALSE arm counts of the same line are deliberately NOT transcribed, for the reason gate_registry gives about `cfgs` and with a measurement behind it: they are a denominator over the whole configuration roster, they moved twice while this bundle was being written as a sibling slice's `PermWideMut_*` family arrived in the working tree, and the same movement has already turned this exact leaf RED on both bundles that were committed before it. Which arms this property's evidence rides on is stated in the method table by CONFIGURATION, which cannot drift with the roster. AND THE PRICE OF THAT CHOICE IS NOW MEASURED: dropping the arm counts leaves this leaf one bare integer, and the bare-integer rule looks for it ANYWHERE in the derived line — so when `RekeyOrderModelled` took the total to five, the `4 standing assumption(s)` that stood here stayed GREEN off `PowerOnClearsScratch2 FALSE=4` while the ten sibling bundles went red, and it was repaired by hand rather than by the gate. A transcription carrying fewer numbers is not a safer transcription, it is a less-checked one" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check and two proofs read no board preset and no flash geometry, so nothing here settles a cell. This property's own row is the widest claim in the family, and it is `equivalent` on three columns and `out-of-scope` on none" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check and two proofs read no board preset and no flash geometry, so nothing here settles a cell. This property's own row is the widest claim in the family, and it is `equivalent` on three columns and `out-of-scope` on none" gate_registry = "assurance-gate: SEC-FIDO-003 NoTokenAfterInvalidation BOUNDED mut=7 co=6 kani=2 fuzz=1 rust=3 test=0 — the `cfgs` figure of the same line is deliberately not transcribed, because it is a live glob over `formal/*.cfg` that a sibling slice's in-flight configurations moved twice while this bundle was written, and because a configuration NAMING an invariant is not one asserting it. Of the six above, the two a reader will over-read are `co` and `rust`. `co=6` against `mut=7` is not a hole: the seventh switch is `unreachable` at the CODE level with the measurement recorded. `rust=3` counts FILES and not tags: there are 6 tags, and the one owner that carries none sits in a file already counted, so the column cannot see it" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-004.toml b/assurance/bundle/SEC-FIDO-004.toml index 2d03a292b..217b797a8 100644 --- a/assurance/bundle/SEC-FIDO-004.toml +++ b/assurance/bundle/SEC-FIDO-004.toml @@ -343,7 +343,7 @@ k_absent = "no bounded proof, no fuzz target and no tests/*.py exercises this in gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle, and transcribed because the roster rule demands every gate line whether or not this property moved it. It is also the line that does NOT cover this property, for the reason `g_ghost_ledger` gives" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the session token's axes, and the persistent grant is not one of them, so none of the production sites this bundle enumerates appears in it" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — three of the five have an arm that asserts this invariant, which is more than most MODELLED-ONLY rows have; PowerOnClearsScratch2 and RekeyOrderModelled both live in RSKeyBootHardening and the overlap with the configurations checking this one is empty for each" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check, a review and a host-test slice read no board preset and no flash geometry, so nothing here settles a cell" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check, a review and a host-test slice read no board preset and no flash geometry, so nothing here settles a cell" gate_registry = "assurance-gate: SEC-FIDO-004 NoAccessibleSecretWithoutGate MODELLED-ONLY cfgs=39 mut=2 co=2 kani=0 fuzz=0 rust=2 test=0 — and every one of those numbers has a reading in this file. `rust` counts files that mention the name, not owners; `mut` counts the two invariant-isolating configurations, both of which fall on the same clause; `co` counts two code twins that patch one file between them; `kani` is correct rather than unfilled" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-005.toml b/assurance/bundle/SEC-FIDO-005.toml index 5e33f8c92..851d9a252 100644 --- a/assurance/bundle/SEC-FIDO-005.toml +++ b/assurance/bundle/SEC-FIDO-005.toml @@ -356,7 +356,7 @@ m_matrix_cells = "docs/assurance-matrix.md:77 claims this property on four cells gate_ghost = "ghost-gate: 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle and transcribed because the roster rule demands every gate line whether or not this property moved it; the ghost register is about a different invariant entirely, and this property records no `viol` anywhere" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the token axes, and not one of this property's owner sites is in any of them" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93. Three of the five have BOTH arms checking this invariant — AlwaysUvShipped through AlwaysUv.cfg, WidePerms through PermWide.cfg and ForceChangeModelled through ForceChange.cfg — which is more than either existing bundle can say, and is also exactly what result.b_asserted_arms says is worth little here. Transcribed at the moment of writing and VOLATILE, and that prediction has now been paid twice over: this line moved twice while the bundle was being written, because formal/ was under concurrent edit and every generated configuration is a denominator in it, and it moved again afterwards when `RekeyOrderModelled` entered the registry and two `Historical_Boot*` configurations entered the roster" -gate_matrix = "matrix-gate: 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle, and the per-property half of that row is in result.m_matrix_cells rather than here, because a number this line does not derive does not belong on it" +gate_matrix = "matrix-gate: 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle, and the per-property half of that row is in result.m_matrix_cells rather than here, because a number this line does not derive does not belong on it" gate_registry = "assurance-gate: SEC-FIDO-005 NoUnmanageableCredential MODELLED-ONLY cfgs=40 mut=3 co=3 kani=0 fuzz=0 rust=3 test=0 — and this bundle moves none of them. `cfgs=40` is still the finding rather than the evidence, for one reason now instead of two: twenty-nine of the forty are mutants of some other property that merely CHECK this one. The other reason is gone — one of the forty WAS a byte-identical copy of another and is not any more, so the forty are forty distinct configurations and the number did not have to move for the meaning to. `rust=3` counts files and not tags, so tagging bump_rp would move nothing and tagging reset.rs would move it by one" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-006.toml b/assurance/bundle/SEC-FIDO-006.toml index 1975d6735..d5547341e 100644 --- a/assurance/bundle/SEC-FIDO-006.toml +++ b/assurance/bundle/SEC-FIDO-006.toml @@ -370,7 +370,7 @@ readme_stale = "formal/README.md:881 publishes three run counts for the three So gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle, and transcribed because the roster rule demands every gate line whether or not this property moved it. The ghost records a different invariant: no action in this module writes ResetNeverWeakensSurvivingState into `viol`, which is correct, because this property is a state predicate and not a step one" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the pinUvAuthToken axes and none of this property's owners is in it; the reset family's own ledger is `formal/comutants.toml` and the tag grep, neither of which is a site ledger" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — RE-DERIVED, and this time the roster did move. The line used to say all four denominators were unchanged between the two tiers, and `RekeyOrderModelled` plus the two `Historical_Boot*` configurations have since made that sentence false, which is the whole reason this leaf is derived rather than kept. The one that matters here is still the first: both arms of AlwaysUvShipped check this invariant, and the TRUE arm is the one on which its second clause cannot fail" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check, a bounded proof and a review read no board preset and no flash geometry, so nothing here settles a cell" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check, a bounded proof and a review read no board preset and no flash geometry, so nothing here settles a cell" gate_registry = "assurance-gate: SEC-FIDO-006 BOUNDED, cfgs=40 mut=3 co=3 kani=1 fuzz=1 rust=3 test=1 — `test=1` is the only non-zero runtime column of the eleven and it counts a FILE, not a board run; `kani=1` counts harnesses whose name contains the snake form of the invariant, which the three clause harnesses do not, so the parent's proof column omits three of the four harnesses that are about it. Every clause row derives its own line beneath this one" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-006A.toml b/assurance/bundle/SEC-FIDO-006A.toml index 51760bb9e..a571069ee 100644 --- a/assurance/bundle/SEC-FIDO-006A.toml +++ b/assurance/bundle/SEC-FIDO-006A.toml @@ -309,7 +309,7 @@ t_hil = "`test = 1` counts tests/29_reset_power_cut.py, whose doc at `tests/29_r gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle, and transcribed because the roster rule asks for every gate line whether or not this property moved it. The ghost records a different invariant, correctly: this clause is a state predicate and no action writes its name into the violation set" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — AND THIS ONE MOVED WHILE THIS BUNDLE WAS BEING WRITTEN. The volatile pair read one lower on each side earlier in the same session, before another agent's uncommitted edit landed. The ledger owns the pinUvAuthToken axes and none of this clause's owners is in it; it is transcribed as a live reading. RE-DERIVED ON INTEGRATION and unchanged: the pair the integrator read from `bundle_gate.gate_corpus()` is the one above, and the eight existing bundles were moved up to it rather than this one down" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — derived over a working tree carrying other agents' uncommitted configurations, so these denominators move with the roster, and they have: `RekeyOrderModelled` is the fifth entry and it arrived after this bundle was written. None of the five is an antecedent of this clause: unlike its sibling, this conjunct is falsifiable on both arms of AlwaysUvShipped, because its consequent is written FALSE by the PIN disjunct of the gate sweep on every arm" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check, a bounded proof and two reviews read no board preset and no flash geometry, so nothing here settles a cell" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check, a bounded proof and two reviews read no board preset and no flash geometry, so nothing here settles a cell" gate_registry = " SEC-FIDO-006A ResetKeepsThePinGate BOUNDED cfgs=2 mut=1 co=1 kani=1 fuzz=1 rust=1 test=1 — and two of those non-zero columns count files whose oracles cannot name this clause, while `co` counts a patch the neighbouring property counts too" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-006B.toml b/assurance/bundle/SEC-FIDO-006B.toml index 54f024ab9..1640ad128 100644 --- a/assurance/bundle/SEC-FIDO-006B.toml +++ b/assurance/bundle/SEC-FIDO-006B.toml @@ -290,7 +290,7 @@ x_no_gate_row = "and no row of the merge gate builds this column at all: `grep - gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle. The ghost records a different invariant, and correctly: no action writes this clause's name into `viol`, because it is a state predicate over a snapshot and not a step predicate over an authorization" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the pinUvAuthToken axes and none of this clause's owners is in it. THIS LINE MOVED WHILE THIS BUNDLE WAS BEING WRITTEN: an earlier derivation the same morning read volatile=ten-over-nine, and another agent's edits to assurance/token_refinement.toml moved it — a `[[volatile_writer]]` for `crates/rsk-device/src/ctap.rs`'s `new`, which replaces the borrowed session state on every power-up. RE-DERIVED ON INTEGRATION and unchanged" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — the first pair is this clause's own scope constant, and the split is the finding above read off a gate: on every configuration in the smaller arm this clause is vacuous, and on none of them is its mutant armed. The denominators move with the roster, so the integrator re-derives — and they moved again when `RekeyOrderModelled` took the registry to five, which is what re-derived this line" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check, a bounded proof and two reviews read no board preset and no flash geometry, so nothing here settles a cell. The firmware-always-uv column is one of the gap cells for this row, and assurance/configurations.toml carries the question that says why" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check, a bounded proof and two reviews read no board preset and no flash geometry, so nothing here settles a cell. The firmware-always-uv column is one of the gap cells for this row, and assurance/configurations.toml carries the question that says why" gate_registry = "assurance-gate: SEC-FIDO-006B ResetKeepsTheAlwaysUvGate BOUNDED, cfgs=2 mut=1 co=1 kani=1 fuzz=1 rust=1 test=1 — the model column counts one green base and one red clause row, the proof column counts one harness, and each runtime column counts a FILE that names the invariant rather than a run that passed" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-006C.toml b/assurance/bundle/SEC-FIDO-006C.toml index 64afd5f56..6f9a7049c 100644 --- a/assurance/bundle/SEC-FIDO-006C.toml +++ b/assurance/bundle/SEC-FIDO-006C.toml @@ -332,7 +332,7 @@ x_ladder_reading = "so the four rungs are not four checks of one sentence. The m gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle, and transcribed because the roster rule demands every gate line whether or not this property moved it. The ghost records a different invariant; no action in this module writes this clause into `viol`, which is correct, because it is a state predicate and not a step one" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the pinUvAuthToken axes and no owner of this clause is in it. DERIVED OVER A WORKING TREE CARRYING ANOTHER AGENT'S UNCOMMITTED `assurance/token_refinement.toml`, which is why the volatile pair here differs from the one every existing bundle transcribes and why the gate was red on all of them; RE-DERIVED ON INTEGRATION and unchanged, and the eight were moved up to this reading" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — derived over a working tree carrying other agents' uncommitted configurations, so these denominators move with the roster and must be re-derived; `RekeyOrderModelled` is the entry that moved them since. None of the five constants appears in this clause's antecedent or consequent: unlike its sibling, this conjunct is falsifiable on both arms of every one of them, and the two that live in RSKeyBootHardening are assigned by neither configuration that checks this clause" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check, a bounded proof and two reviews read no board preset and no flash geometry, so nothing here settles a cell. Two build columns would change what this clause is ABOUT rather than how it is checked, and each has an assumption row instead of a cell" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check, a bounded proof and two reviews read no board preset and no flash geometry, so nothing here settles a cell. Two build columns would change what this clause is ABOUT rather than how it is checked, and each has an assumption row instead of a cell" gate_registry = "assurance-gate: SEC-FIDO-006C ResetKeepsTheBackupSeal BOUNDED cfgs=2 mut=1 co=1 kani=1 fuzz=1 rust=1 test=1 — `co=1` is the column the roadmap records as a zero, and it is the credit resolved through the companion rule; `rust=1` counts a FILE holding one tag; `test=1` counts a FILE that names the invariant and not a board run" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-007.toml b/assurance/bundle/SEC-FIDO-007.toml index 0bb533de9..f11971a63 100644 --- a/assurance/bundle/SEC-FIDO-007.toml +++ b/assurance/bundle/SEC-FIDO-007.toml @@ -387,7 +387,7 @@ r_column = "the `rust` column is not a count of tags: assurance_gate.grep_word m gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle, and transcribed because the roster rule demands every gate line whether or not this property moved it" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the token axes, not the seed ones, so none of the owners this bundle enumerates is in it" gate_assumption = "5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — re-derived, and it REFUTES what this line used to say. `grep -lw RamNeverOutlivesFlashSeed formal/*.cfg` is Shipped.cfg, AlwaysUv.cfg, ForceChange.cfg, PermWide.cfg and Solo_RamNeverOutlivesFlashSeed.cfg, and across those five all three model constants now have BOTH arms run against this property — `AlwaysUvShipped` through Shipped.cfg and AlwaysUv.cfg, `WidePerms` through PermWide.cfg, `ForceChangeModelled` through ForceChange.cfg. The earlier wording said the two scope constants were FALSE in every configuration that checks it, which was true when it was typed and is not now. `PowerOnClearsScratch2` and `RekeyOrderModelled` are the two whose arms this property never sees, because both live in RSKeyBootHardening — the second joined the registry after this line was first written, and is why the total is five rather than four" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check and a host-test slice read no board preset and no flash geometry, so nothing here settles a cell" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check and a host-test slice read no board preset and no flash geometry, so nothing here settles a cell" gate_registry = "SEC-FIDO-007 RamNeverOutlivesFlashSeed MODELLED-ONLY cfgs=5 mut=1 co=1 kani=0 fuzz=0 rust=2 test=0 — `rust` moved to two when the ONE producer of the antecedent, `vendor.rs::unlock`, was tagged; it had carried only the consequent's owner. `cfgs` counts configurations CHECKING the invariant rather than evidence about it" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/bundle/SEC-FIDO-008.toml b/assurance/bundle/SEC-FIDO-008.toml index bde48ad3f..00133897e 100644 --- a/assurance/bundle/SEC-FIDO-008.toml +++ b/assurance/bundle/SEC-FIDO-008.toml @@ -388,7 +388,7 @@ r_citations = "every `file:line` in this bundle was verified by content rather t gate_ghost = "ghost-gate: ok — 21 action(s) record NoAuthorizationBypass over 24 route(s), consulting 11 guard(s) — unchanged by this bundle, and transcribed because the roster rule demands every gate line whether or not this property moved it. assurance/ghost_actions.toml is about that invariant and no other; this one has no ghost at all, which is why its counterexample carries an empty `viol`" gate_ledger = "token-refinement-gate: GREEN keys=2 api=11 volatile=11/10 persistent=14/5 outcomes=7/6 walk=4 softlock=12 window=2 testonly=1 — the ledger owns the token AXES and this property owns the token's LIFETIME against a flash record, so none of the nine owners enumerated above is in it" gate_assumption = "assumption-gate: 5 standing assumption(s) AlwaysUvShipped TRUE=5 FALSE=91 ForceChangeModelled TRUE=3 FALSE=93 PowerOnClearsScratch2 TRUE=11 FALSE=4 RekeyOrderModelled TRUE=2 FALSE=13 WidePerms TRUE=3 FALSE=93 — three of the five take both arms against this property and the other two take neither, which `b_all_arms` states configuration by configuration. Three of the eight counts this line used to carry moved WHILE THIS BUNDLE WAS BEING WRITTEN, on a working tree another agent was adding configurations to, and there are ten of them now: the arms are counted over every file the glob returns, so a transcribed count here is stale the moment a sibling's mutant family lands — `RekeyOrderModelled` and the two `Historical_Boot*` configurations landed exactly that way — and `build.tree_state` is where that is recorded rather than hidden" -gate_matrix = "matrix-gate: ok — 40 P0-family properties × 31 build configurations = 1240 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 954 gap) — UNCHANGED by this bundle: a model check and a source review read no board preset and no flash geometry, so nothing here settles a cell" +gate_matrix = "matrix-gate: ok — 40 P0-family properties × 32 build configurations = 1280 cells (37 covered, 143 equivalent, 0 conditional, 106 out-of-scope, 994 gap) — UNCHANGED by this bundle: a model check and a source review read no board preset and no flash geometry, so nothing here settles a cell" gate_registry = "assurance-gate: SEC-FIDO-008 NoLiveTokenWithoutPinRecord MODELLED-ONLY cfgs=5 mut=1 co=1 kani=0 fuzz=0 rust=1 test=0 — the weakest vector of the eleven, and identical to the counterpart's but for `rust`, which reads one file here against two there. Nothing in this bundle moves it: every verdict transcribed is the recorded tier's, and the one column a new tag would move is the untagged reset half that `owners_tag_reading` names" # ---- 6. Raw artifact or log -------------------------------------------------- diff --git a/assurance/configurations.toml b/assurance/configurations.toml index 27334c5de..ce3091c51 100644 --- a/assurance/configurations.toml +++ b/assurance/configurations.toml @@ -974,3 +974,17 @@ in `check.sh` builds `firmware` on it at all. The board loop builds `rsk-wipe`, and both `BOARD=` firmware rows pin `waveshare-one`. So the column is real and \ WHOLLY UNEXERCISED, and what is left to decide is whether the tree should build \ it, or whether it should be a knob of `firmware-display` instead.""" + +[[question]] +column = "panel" +owner = "contributor" +settled_by = "absence" +text = """ +The status-only screen: `rsk-ui` for its fonts and `firmware/src/panel.rs` over \ +SPI1. It wraps the BOOTSEL button in a `UserPresence` impl that paints a banner \ +for the length of the wait and forwards the decision unchanged — no `Confirm` \ +reaches the panel, `shows_confirm` keeps its false default, and there is no PIN \ +surface at all. Is the delta for every P0-family row genuinely out-of-scope, or \ +does a wrapper on the presence path — beside a screen that reads the build \ +counter, the chip serial, the attach flag and uptime — put a new path under an \ +authorisation row?""" diff --git a/assurance/platform.toml b/assurance/platform.toml index eb3efd252..e082368d4 100644 --- a/assurance/platform.toml +++ b/assurance/platform.toml @@ -382,8 +382,8 @@ covers = ["unsafe:firmware/src/main.rs#block:heap-init-core-ptr-addr_of_mut-heap [[assumption]] id = "PLAT-UNSAFE-004" class = "build-configuration" -statement = "Each of the eight build-selected GPIOs handed to `AnyPin::steal` has exactly one owner at runtime: no two of them name the same pad, none of them is a pad another driver claims, and the LED data pin a host can re-aim cannot be pointed at any of them." -discharge = "The compile-time asserts in firmware/build.rs and the boot-time filter in firmware/src/main.rs, read TOGETHER with the presence button's own steal in firmware/src/presence.rs and against the five board files under firmware/boards. The asserts reject a build that collides LED_POWER_PIN, USR_LED_PIN, WAKE_PIN or the four panel control lines with each other, with the LED data pin, or with the hard-wired SPI1 and I2C1 lines; the filter drops a host-written data pin naming one of them back to the build default. What is owed is the PAIRWISE table: that the assert set covers every pair of the eight rather than the pairs somebody thought of. One uncovered pair refutes it." +statement = "Each of the eleven build-selected GPIOs handed to `AnyPin::steal` has exactly one owner at runtime: no two of them name the same pad, none of them is a pad another driver claims, and the LED data pin a host can re-aim cannot be pointed at any of them." +discharge = "The compile-time asserts in firmware/build.rs and the boot-time filter in firmware/src/main.rs, read TOGETHER with the presence button's own steal in firmware/src/presence.rs and against the five board files under firmware/boards. The asserts reject a build that collides LED_POWER_PIN, USR_LED_PIN, WAKE_PIN, the display's four panel control lines or the status panel's CS/DC/backlight pins with each other, with the LED data pin, or with the hard-wired SPI1 and I2C1 lines; the filter drops a host-written data pin naming one of them back to the build default. What is owed is the PAIRWISE table: that the assert set covers every pair of the eleven rather than the pairs somebody thought of. One uncovered pair refutes it." discharge_owner = "contributor" status = "pending" failure_direction = "memory-safety and availability: two owners driving one pad is aliased ownership of a peripheral, and the loser is a button, an LED rail or a panel control line" @@ -397,6 +397,9 @@ covers = [ "unsafe:firmware/src/main.rs#block:embassy_rp-gpio-anypin-steal-build_display_dc", "unsafe:firmware/src/main.rs#block:embassy_rp-gpio-anypin-steal-build_display_rst", "unsafe:firmware/src/main.rs#block:embassy_rp-gpio-anypin-steal-build_display_tp_rst", + "unsafe:firmware/src/main.rs#block:embassy_rp-gpio-anypin-steal-build_panel_cs", + "unsafe:firmware/src/main.rs#block:embassy_rp-gpio-anypin-steal-build_panel_dc", + "unsafe:firmware/src/main.rs#block:embassy_rp-gpio-anypin-steal-build_panel_bl_pin", ] [[assumption]] @@ -575,7 +578,7 @@ supports = ["SEC-FIDO-001"] id = "PLAT-BUILD-003" class = "build-configuration" statement = "The `display` build implements the one-hold-one-ceremony latch SOMEWHERE ELSE: it compiles `ButtonWait` out entirely and the panel's own release debounce takes over, so the model keeps a defence that build has in a different place rather than one it does not have." -discharge = "The `firmware-display` and `waveshare-touch-lcd` matrix cells, which are `gap` for this property today. `firmware/src/presence.rs:99-101` gates the button backend on `not(feature = \"display\")` and `:163-166` swaps `Presence` to `crate::display::TouchPresence`; `crates/rsk-display/src/presence.rs:45-48` clears the stale cancel in the panel's OWN loop, not in `ButtonWait::wait`, and `crates/rsk-display/src/lib.rs:131-134` gives the trait a no-op `set_up_pending`. `formal/RSKeySecurityState.tla:473-477` states the direction: NARROWER than the display build. No configuration and no harness in the tree runs the display build's version of this rule." +discharge = "The `firmware-display` and `waveshare-touch-lcd` matrix cells, which are `gap` for this property today. `firmware/src/presence.rs:103-105` gates the button backend on `not(feature = \"display\")` and `:167-170` swaps `Presence` to `crate::display::TouchPresence`; `crates/rsk-display/src/presence.rs:45-48` clears the stale cancel in the panel's OWN loop, not in `ButtonWait::wait`, and `crates/rsk-display/src/lib.rs:131-134` gives the trait a no-op `set_up_pending`. `formal/RSKeySecurityState.tla:473-477` states the direction: NARROWER than the display build. No configuration and no harness in the tree runs the display build's version of this rule." # `contributor` and not the `maintainer` the proposal wrote. The route is the two # matrix cells and the display build's own ceremony code, all of it host-testable # in `crates/rsk-display`; the glass itself is PLAT-DISPLAY-001's, and a second @@ -590,7 +593,7 @@ supports = ["SEC-FIDO-002"] id = "PLAT-BUILD-004" class = "build-configuration" statement = "On the four `no-touch` images no presence DECISION is produced at all — the arbiter is still compiled in, but nothing can reach the wait that would consume one — which is why this property's cells there are `out-of-scope` and not `gap`." -discharge = "Reading the cfg arms, and it is decided rather than open. `firmware/src/presence.rs:202-258`: `request` and `request_ceremony` return `Presence::Confirmed` unconditionally under `feature = \"no-touch\"`, `poll_pressed` returns `false`, and `ButtonPresence::wait` is `cfg(not(feature = \"no-touch\"))` so `ButtonWait::wait` — the sole caller of `set_up_pending(true)` on a non-display build — is never called. `Arbiter` itself, and the three `Refines`-tagged functions on it, ARE in those images; what is absent is the decision, not the arbitration code. `firmware/Cargo.toml` says \"Never ship a no-touch build\" and `release-build.yml` refuses to release one." +discharge = "Reading the cfg arms, and it is decided rather than open. `firmware/src/presence.rs:209-265`: `request` and `request_ceremony` return `Presence::Confirmed` unconditionally under `feature = \"no-touch\"`, `poll_pressed` returns `false`, and `ButtonPresence::wait` is `cfg(not(feature = \"no-touch\"))` so `ButtonWait::wait` — the sole caller of `set_up_pending(true)` on a non-display build — is never called. `Arbiter` itself, and the three `Refines`-tagged functions on it, ARE in those images; what is absent is the decision, not the arbitration code. `firmware/Cargo.toml` says \"Never ship a no-touch build\" and `release-build.yml` refuses to release one." discharge_owner = "contributor" status = "pending" failure_direction = "coverage in the reading direction: an `out-of-scope` cell that turned out to be a `gap` would be a claim quietly not made where the reader thinks it was refused" @@ -878,8 +881,8 @@ supports = ["SEC-FIDO-006", "SEC-FIDO-006A", "SEC-FIDO-006B", "SEC-FIDO-006C"] [[assumption]] id = "PLAT-TRNG-001" class = "build-configuration" -statement = "The RP2350 TRNG's three health checks are CONTINUOUS, this image leaves all three armed, and they fail CLOSED: `embassy_rp::trng::Config::default()` clears every bypass bit and `Trng::new` writes them into TRNG_DEBUG_CONTROL, firmware/src/main.rs:609 edits `sample_count` and nothing else, and on a failed check the part presents no result and every EHR_DATA register reads zero. So a source that has DIED cannot be drawn from: at boot that stalls before the USB pull-up goes up, and at the 64 KiB DRBG reseed (firmware/src/handler.rs:78) it wedges or halts the operation in flight. That the surviving source has ENTROPY is a DIFFERENT claim and PLAT-TRNG-002's, not this row's: three continuous checks are a dead-source detector, never an entropy estimator." -discharge = "READ on 2026-09-05, and it is the reading half of a route that used to carry three claims of very different reachability under one id. FOUR READINGS. (1) ALL THREE CHECKS ARE ARMED, and the image WRITES that rather than inheriting a reset value: `embassy_rp::trng::Config::default()` sets `disable_autocorrelation_test`, `disable_crngt_test` and `disable_von_neumann_balancer` all `false`, and `Trng::new`'s `initialize_rng` writes those three bits into TRNG_DEBUG_CONTROL (0x138) on every construction. firmware/src/main.rs:601 takes that default, firmware/src/main.rs:609 edits `sample_count` alone, and firmware/src/main.rs:610 is the tree's ONLY `Trng::new`, and no first-party Rust assigns any of the three bypass fields: `git grep -E 'disable_autocorrelation_test|disable_crngt_test|disable_von_neumann_balancer' -- 'firmware/*.rs' 'crates/*.rs' 'tools/*.rs'` exits 1. THE THREE NAMES AND THE PATHSPEC ARE BOTH THE POINT, not tidiness. Unscoped, that grep answers this row's own prose and the page generated from it, so a claim about what the tree contains would be falsified by writing it down — PLAT-TIMER-001 records that trap one row over. And `disable_` alone over the same pathspec is NOT empty, which is why the command is the three field names and not the prefix — and the count is worth getting right, because the first version of this sentence got it wrong: FOUR hits in TWO files, two test names in crates/rsk-fido/src/vendor_tests.rs and two `disable_raw_mode` calls in tools/tui/src/main.rs, because git's pathspec matching lets `tools/*.rs` cross a `/`. The revision is pinned in Cargo.lock: embassy-rp 0.10.0 at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (2) THEY ARE CONTINUOUS, AND THERE IS NO START-UP TEST. RNG_ISR (0x104, RP2350 datasheet of 29 July 2025, p.1217) carries VN_ERR — 32 identical consecutive bits — CRNGT_ERR — two equal consecutive 16-bit blocks — and AUTOCORR_ERR, and each is evaluated as a block is produced rather than once at reset — AUTOCORR_ERR latching after four failures in a row rather than on the first, which the driver's own comment quotes from the register description. That datasheet describes no start-up test and no Adaptive Proportion Test, which is worth writing down because SP800-90B's health-test vocabulary makes a reader expect both: what this part gives is a per-block continuous test and nothing else. (3) THEY FAIL CLOSED AND LOUDLY, which is the half the row this was split from had backwards. §12.12.3 p.1213: on failure no results are presented and the EHR_DATA registers all read as 0, so TRNG_VALID.ehr_valid stays low and no byte reaches the caller; AUTOCORR_ERR is LATCHED — in the datasheet's own words the RNG then ceases functioning until the next reset — and the interrupt-clear register does not lift it: what the driver does instead is assert TRNG_SW_RESET and re-initialise, which is the loop below and not a recovery. This firmware's boot draw is the BLOCKING one: firmware/src/handler.rs:63 takes the `Trng` and firmware/src/handler.rs:65 fills 48 bytes through `blocking_fill_bytes`, whose not-valid branch soft-resets and re-samples on AUTOCORR_ERR and panics on anything else. Either way the boot does not finish — it is the 30-105 s stall firmware/src/main.rs:602-608 records, taken before the bus pull-up (firmware/src/usb_attach.rs:8) — and a device that never enumerates is not a device handing out predictable keys. AND THE BOOT IS NOT THE ONLY DRAW, which the first version of this reading missed: firmware/src/handler.rs:78 calls the same `blocking_fill_bytes` again on every `RESEED_INTERVAL` (firmware/src/handler.rs:57, 64 KiB of DRBG output), so a source that dies AFTER boot meets the same loop on an ENUMERATED device, inside whatever operation asked for the bytes. Loud in a second way — a wedged command or a `panic-halt` freeze rather than a device that never appears — and still not a quiet weak key, which is the claim. (4) WHAT `sample_count = 1000` DOES, AND WHAT IT DOES NOT. SAMPLE_CNT1 (0x130, p.1218) is the number of rng_clk cycles BETWEEN two consecutive ROSC samples, so 25 -> 1000 buys DECORRELATION and costs generation time; it does not change the 192 bits an EHR block carries and it adds no entropy to any of them. Why it was raised is on record and was measured on hardware at `2cdcc282e9605d601631b4b06727b2f746325727`: at 25 the autocorrelation check failed on nearly every block on one Waveshare unit and the driver soft-reset in a loop, an inverter-chain change did NOT help, and 1000 gives a seed in about a second and a half. THREE THINGS THIS DOES NOT SETTLE. That the surviving source has ENTROPY — the checks catch a source that has DIED and a merely DEGRADED one passes all three, which is the single failure this row's statement deliberately leaves open and PLAT-TRNG-002's whole subject. WHICH loud arm a CRNGT or VN failure takes: `blocking_wait_for_successful_generation` branches on `autocorr_err` alone and panics otherwise, and whether the part drops TRNG_BUSY with `ehr_valid` low on those two is a clause this reading does not have — a stall and a `panic-halt` freeze are both loud and neither is a quiet weak key, so the claim survives either answer while the answer stays unread. And NOTHING IN THIS TREE BACKS THE FAIL-CLOSED CLAUSE: the driver's `read_ehr_registers_into_array` copies EHR_DATA0 through EHR_DATA5 with no zero check and firmware/src/handler.rs:63 does not inspect the seed it gets, so `no byte reaches the caller` rests on the datasheet's promise that `ehr_valid` stays low and on no code here — a part that ever raised that flag over zeroed result registers would seed the DRBG with 48 zero bytes and nothing would say so. And the CORNERS: everything above is about what this image configures at desk conditions, and temperature, supply and process are PLAT-TRNG-003's. TWO THINGS THE READING FOUND AND THIS CHANGE REPAIRED, because they were firmware comments rather than registry entries and they were two copies of ONE claim: firmware/src/handler.rs:44 told the reader the block was drawn only through a working ROSC config with the chain at zero, and firmware/src/handler.rs:61 repeated it as a draw through the ROSC config the caller set on the `Trng`. No such configuration ever existed on this branch: `2cdcc282e9605d601631b4b06727b2f746325727` replaced the `inverter_chain_length = InverterChainLength::None` assignment with the `sample_count = 1000` one and dropped the `InverterChainLength` import, and left both sentences standing — so from 2026-06-18 the shipped image ran the driver's DEFAULT chain length while its own doc comment called that setting catastrophic, and `git grep inverter_chain_length` answered with no Rust at all. Both lines now say what the code does — the draw finishes because of `sample_count`, and the chain length is the driver's — which is what that commit's own message already recorded when it wrote that an inverter-chain change did NOT help. THE BEHAVIOUR WAS NEVER WRONG AND THE EXPLANATION OF IT WAS, for eleven weeks, which is why this belongs in a reading's discharge rather than in a fix: nothing about the image changed when the sentences did." +statement = "The RP2350 TRNG's three health checks are CONTINUOUS, this image leaves all three armed, and they fail CLOSED: `embassy_rp::trng::Config::default()` clears every bypass bit and `Trng::new` writes them into TRNG_DEBUG_CONTROL, firmware/src/main.rs:658 edits `sample_count` and nothing else, and on a failed check the part presents no result and every EHR_DATA register reads zero. So a source that has DIED cannot be drawn from: at boot that stalls before the USB pull-up goes up, and at the 64 KiB DRBG reseed (firmware/src/handler.rs:78) it wedges or halts the operation in flight. That the surviving source has ENTROPY is a DIFFERENT claim and PLAT-TRNG-002's, not this row's: three continuous checks are a dead-source detector, never an entropy estimator." +discharge = "READ on 2026-09-05, and it is the reading half of a route that used to carry three claims of very different reachability under one id. FOUR READINGS. (1) ALL THREE CHECKS ARE ARMED, and the image WRITES that rather than inheriting a reset value: `embassy_rp::trng::Config::default()` sets `disable_autocorrelation_test`, `disable_crngt_test` and `disable_von_neumann_balancer` all `false`, and `Trng::new`'s `initialize_rng` writes those three bits into TRNG_DEBUG_CONTROL (0x138) on every construction. firmware/src/main.rs:650 takes that default, firmware/src/main.rs:658 edits `sample_count` alone, and firmware/src/main.rs:659 is the tree's ONLY `Trng::new`, and no first-party Rust assigns any of the three bypass fields: `git grep -E 'disable_autocorrelation_test|disable_crngt_test|disable_von_neumann_balancer' -- 'firmware/*.rs' 'crates/*.rs' 'tools/*.rs'` exits 1. THE THREE NAMES AND THE PATHSPEC ARE BOTH THE POINT, not tidiness. Unscoped, that grep answers this row's own prose and the page generated from it, so a claim about what the tree contains would be falsified by writing it down — PLAT-TIMER-001 records that trap one row over. And `disable_` alone over the same pathspec is NOT empty, which is why the command is the three field names and not the prefix — and the count is worth getting right, because the first version of this sentence got it wrong: FOUR hits in TWO files, two test names in crates/rsk-fido/src/vendor_tests.rs and two `disable_raw_mode` calls in tools/tui/src/main.rs, because git's pathspec matching lets `tools/*.rs` cross a `/`. The revision is pinned in Cargo.lock: embassy-rp 0.10.0 at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (2) THEY ARE CONTINUOUS, AND THERE IS NO START-UP TEST. RNG_ISR (0x104, RP2350 datasheet of 29 July 2025, p.1217) carries VN_ERR — 32 identical consecutive bits — CRNGT_ERR — two equal consecutive 16-bit blocks — and AUTOCORR_ERR, and each is evaluated as a block is produced rather than once at reset — AUTOCORR_ERR latching after four failures in a row rather than on the first, which the driver's own comment quotes from the register description. That datasheet describes no start-up test and no Adaptive Proportion Test, which is worth writing down because SP800-90B's health-test vocabulary makes a reader expect both: what this part gives is a per-block continuous test and nothing else. (3) THEY FAIL CLOSED AND LOUDLY, which is the half the row this was split from had backwards. §12.12.3 p.1213: on failure no results are presented and the EHR_DATA registers all read as 0, so TRNG_VALID.ehr_valid stays low and no byte reaches the caller; AUTOCORR_ERR is LATCHED — in the datasheet's own words the RNG then ceases functioning until the next reset — and the interrupt-clear register does not lift it: what the driver does instead is assert TRNG_SW_RESET and re-initialise, which is the loop below and not a recovery. This firmware's boot draw is the BLOCKING one: firmware/src/handler.rs:63 takes the `Trng` and firmware/src/handler.rs:65 fills 48 bytes through `blocking_fill_bytes`, whose not-valid branch soft-resets and re-samples on AUTOCORR_ERR and panics on anything else. Either way the boot does not finish — it is the 30-105 s stall firmware/src/main.rs:651-657 records, taken before the bus pull-up (firmware/src/usb_attach.rs:8) — and a device that never enumerates is not a device handing out predictable keys. AND THE BOOT IS NOT THE ONLY DRAW, which the first version of this reading missed: firmware/src/handler.rs:78 calls the same `blocking_fill_bytes` again on every `RESEED_INTERVAL` (firmware/src/handler.rs:57, 64 KiB of DRBG output), so a source that dies AFTER boot meets the same loop on an ENUMERATED device, inside whatever operation asked for the bytes. Loud in a second way — a wedged command or a `panic-halt` freeze rather than a device that never appears — and still not a quiet weak key, which is the claim. (4) WHAT `sample_count = 1000` DOES, AND WHAT IT DOES NOT. SAMPLE_CNT1 (0x130, p.1218) is the number of rng_clk cycles BETWEEN two consecutive ROSC samples, so 25 -> 1000 buys DECORRELATION and costs generation time; it does not change the 192 bits an EHR block carries and it adds no entropy to any of them. Why it was raised is on record and was measured on hardware at `2cdcc282e9605d601631b4b06727b2f746325727`: at 25 the autocorrelation check failed on nearly every block on one Waveshare unit and the driver soft-reset in a loop, an inverter-chain change did NOT help, and 1000 gives a seed in about a second and a half. THREE THINGS THIS DOES NOT SETTLE. That the surviving source has ENTROPY — the checks catch a source that has DIED and a merely DEGRADED one passes all three, which is the single failure this row's statement deliberately leaves open and PLAT-TRNG-002's whole subject. WHICH loud arm a CRNGT or VN failure takes: `blocking_wait_for_successful_generation` branches on `autocorr_err` alone and panics otherwise, and whether the part drops TRNG_BUSY with `ehr_valid` low on those two is a clause this reading does not have — a stall and a `panic-halt` freeze are both loud and neither is a quiet weak key, so the claim survives either answer while the answer stays unread. And NOTHING IN THIS TREE BACKS THE FAIL-CLOSED CLAUSE: the driver's `read_ehr_registers_into_array` copies EHR_DATA0 through EHR_DATA5 with no zero check and firmware/src/handler.rs:63 does not inspect the seed it gets, so `no byte reaches the caller` rests on the datasheet's promise that `ehr_valid` stays low and on no code here — a part that ever raised that flag over zeroed result registers would seed the DRBG with 48 zero bytes and nothing would say so. And the CORNERS: everything above is about what this image configures at desk conditions, and temperature, supply and process are PLAT-TRNG-003's. TWO THINGS THE READING FOUND AND THIS CHANGE REPAIRED, because they were firmware comments rather than registry entries and they were two copies of ONE claim: firmware/src/handler.rs:44 told the reader the block was drawn only through a working ROSC config with the chain at zero, and firmware/src/handler.rs:61 repeated it as a draw through the ROSC config the caller set on the `Trng`. No such configuration ever existed on this branch: `2cdcc282e9605d601631b4b06727b2f746325727` replaced the `inverter_chain_length = InverterChainLength::None` assignment with the `sample_count = 1000` one and dropped the `InverterChainLength` import, and left both sentences standing — so from 2026-06-18 the shipped image ran the driver's DEFAULT chain length while its own doc comment called that setting catastrophic, and `git grep inverter_chain_length` answered with no Rust at all. Both lines now say what the code does — the draw finishes because of `sample_count`, and the chain length is the driver's — which is what that commit's own message already recorded when it wrote that an inverter-chain change did NOT help. THE BEHAVIOUR WAS NEVER WRONG AND THE EXPLANATION OF IT WAS, for eleven weeks, which is why this belongs in a reading's discharge rather than in a fix: nothing about the image changed when the sentences did." discharge_owner = "contributor" status = "discharged" evidence = [ @@ -916,7 +919,7 @@ failure_direction = "coverage, and saying so is the correction this split makes: id = "PLAT-TRNG-002" class = "trng" statement = "The RAW ring-oscillator source clears the floor the shipped whitening needs — at least half a bit of min-entropy per raw bit, measured AT THE SAMPLE SPACING THE PRODUCT USES, on the die this project owns and at desk conditions. HALF A BIT IS SUFFICIENT AND NOT NECESSARY, and which way round is the point rather than a hedge: a von Neumann debiaser emits at most one bit per two raw bits, so at 0.5 bits per raw bit every whitened bit is backed by a full bit of input min-entropy AT EVERY RATE THAT BALANCER CAN ACHIEVE, and at a slower real rate by more. Below the floor the BOUND stops holding, not the device — von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it and what actually threatens it is CORRELATION between samples, which is what `sample_count` buys (PLAT-TRNG-001) and what the non-IID estimators are chosen to see. Neither of the two things this tree already has can answer it: the three continuous checks PLAT-TRNG-001 reads detect a source that has DIED and pass a degraded one, and a statistical test of the WHITENED output passes over a stuck source, which is what whitening is for. What rests on the floor is the 48-byte DRBG seed firmware/src/handler.rs:64 declares and firmware/src/handler.rs:65 fills at every boot, and every key the device mints afterwards." -discharge = "A BENCH IMAGE AND SP800-90B, and the route is REACHABLE — the first thing this split corrects, because the row it came from said the raw source needs an image `this tree does not ship` and left a reader picturing an exotic rig. The raw pre-whitening samples are readable in software on a stock part: TRNG_DEBUG_CONTROL (0x138, RP2350 datasheet of 29 July 2025, p.1219) exposes AUTO_CORRELATE_BYPASS, TRNG_CRNGT_BYPASS and VNC_BYPASS, and that datasheet's own bootrom listing at §12.12.4.1 p.1214 sets `sample_cnt1 = 0` and `trng_debug_control = -1u` and reads EHR_DATA to stream raw TRNG ROSC samples — shipping pico-sdk does the same in `src/rp2_common/pico_rand/rand.c`. THE RUN, in assurance/board/PLAT-TRNG-002.toml: that bypass configuration on one board, at least 1,000,000 consecutive raw samples captured and hashed into the record, then the SP800-90B §6.3 non-IID estimator suite over them. AT THE SHIPPED `SAMPLE_CNT1`, AND THAT IS A CORRECTION rather than a detail: the bootrom listing sets the sample period to 0 because it wants throughput, and 0 is the MOST correlated setting this part offers, while the product spaces its samples 1000 cycles apart. A verdict taken at 0 would be about a stream the device never draws, and this row's own FAIL is gate-forced to `refuted` — so the criterion is read off the 1000 capture and the 0 capture is kept beside it as the correlated control, which is also what makes the pair say whether the spacing bought anything. AND A SECOND HALF THE SAME SESSION CAN TAKE: AUTOCORR_STATISTIC (0x134, p.1218) read with the checks re-enabled at `sample_count` 25 and at the shipped 1000 on the same unit, which is the only way this project can put a RATE behind firmware/src/main.rs:609 — what stands behind it today is one wall-clock observation of a boot on one Waveshare unit at `2cdcc282e9605d601631b4b06727b2f746325727`, and a boot time is not a health-check failure rate. WHY THE CRITERION NAMES NO DATASHEET FIGURE, said here because the row this was split from named one and there is none. §12.12.1 p.1212 makes a bare compliance assertion and gives `approximately 7.5 kb/s of entropy`, which is a RATE and not min-entropy per bit; §12.12.2 p.1213 records that `software does not configure the ROSC length and sample count settings provided by the Arm characterisation procedure`, so the vendor declined the procedure that would have produced one; and the part holds no NIST ESV or ENT listing. `PASS = min-entropy at or above the datasheet figure` was therefore a clause no passing world could walk. The record names its own threshold and justifies it there instead, which is the whole of what changed. WHAT THIS RUN DOES NOT REACH, said because a PASS read as covering it would be read wrong: one die, one desk, one temperature and one supply — the corners are PLAT-TRNG-003's — and the whitening itself, since the capture bypasses it, so this measures the SOURCE and says nothing about how many raw bits the shipped path spends per EHR block." +discharge = "A BENCH IMAGE AND SP800-90B, and the route is REACHABLE — the first thing this split corrects, because the row it came from said the raw source needs an image `this tree does not ship` and left a reader picturing an exotic rig. The raw pre-whitening samples are readable in software on a stock part: TRNG_DEBUG_CONTROL (0x138, RP2350 datasheet of 29 July 2025, p.1219) exposes AUTO_CORRELATE_BYPASS, TRNG_CRNGT_BYPASS and VNC_BYPASS, and that datasheet's own bootrom listing at §12.12.4.1 p.1214 sets `sample_cnt1 = 0` and `trng_debug_control = -1u` and reads EHR_DATA to stream raw TRNG ROSC samples — shipping pico-sdk does the same in `src/rp2_common/pico_rand/rand.c`. THE RUN, in assurance/board/PLAT-TRNG-002.toml: that bypass configuration on one board, at least 1,000,000 consecutive raw samples captured and hashed into the record, then the SP800-90B §6.3 non-IID estimator suite over them. AT THE SHIPPED `SAMPLE_CNT1`, AND THAT IS A CORRECTION rather than a detail: the bootrom listing sets the sample period to 0 because it wants throughput, and 0 is the MOST correlated setting this part offers, while the product spaces its samples 1000 cycles apart. A verdict taken at 0 would be about a stream the device never draws, and this row's own FAIL is gate-forced to `refuted` — so the criterion is read off the 1000 capture and the 0 capture is kept beside it as the correlated control, which is also what makes the pair say whether the spacing bought anything. AND A SECOND HALF THE SAME SESSION CAN TAKE: AUTOCORR_STATISTIC (0x134, p.1218) read with the checks re-enabled at `sample_count` 25 and at the shipped 1000 on the same unit, which is the only way this project can put a RATE behind firmware/src/main.rs:658 — what stands behind it today is one wall-clock observation of a boot on one Waveshare unit at `2cdcc282e9605d601631b4b06727b2f746325727`, and a boot time is not a health-check failure rate. WHY THE CRITERION NAMES NO DATASHEET FIGURE, said here because the row this was split from named one and there is none. §12.12.1 p.1212 makes a bare compliance assertion and gives `approximately 7.5 kb/s of entropy`, which is a RATE and not min-entropy per bit; §12.12.2 p.1213 records that `software does not configure the ROSC length and sample count settings provided by the Arm characterisation procedure`, so the vendor declined the procedure that would have produced one; and the part holds no NIST ESV or ENT listing. `PASS = min-entropy at or above the datasheet figure` was therefore a clause no passing world could walk. The record names its own threshold and justifies it there instead, which is the whole of what changed. WHAT THIS RUN DOES NOT REACH, said because a PASS read as covering it would be read wrong: one die, one desk, one temperature and one supply — the corners are PLAT-TRNG-003's — and the whitening itself, since the capture bypasses it, so this measures the SOURCE and says nothing about how many raw bits the shipped path spends per EHR block." discharge_owner = "maintainer" status = "pending" failure_direction = "security, and the size of the claim is what a `fail` here must not overstate, since the gate forces one to `refuted`: the floor is SUFFICIENT AND NOT NECESSARY, so an estimate below it WITHDRAWS the counting bound standing behind the 48-byte seed rather than establishing a weak one — a real source at h = 0.25 sits at a balancer rate near 0.13 and still carries a full bit into every whitened bit, and nobody here has measured that rate. What a `fail` establishes is exactly that much: the argument behind every key, nonce, blinding factor and pinUvAuthToken drawn after boot is gone, and the measurement that would settle whether the secrets themselves are weak is one this project has never taken. That open half is the serious one, because PLAT-TRNG-001's three continuous checks pass a degraded source by construction — a source that IS weak is noticed by nothing on the device, which is what separates this row from that one, whose failure stalls the boot — and the observable, if there is one, is a host that guesses a credential" @@ -1023,7 +1026,7 @@ depends_on = ["PLAT-TRNG-002"] id = "PLAT-TIMER-001" class = "build-configuration" statement = "The timebase every timeout is measured against is a 1 MHz 64-bit tick count that no run can wrap: `embassy_time::Instant` is a `u64` of ticks, `embassy-rp`'s driver reads RP2350 `TIMER0`'s NATIVE 64-bit counter rather than extending a 32-bit one in software, and the image pins the rate at 1 MHz — so the roll-over the timeout arithmetic would have to survive is 2^64 microseconds, about 584,542 years. That the counter also ADVANCES is a DIFFERENT claim and PLAT-TIMER-003's, not this row's." -discharge = "READ on 2026-09-05, and it is the first half of the route this row used to carry; the second half is PLAT-TIMER-002 because no run walks the one written here. FOUR READINGS AND ONE DIVISION. (1) `embassy_time::Instant` is `ticks: u64` and nothing narrows it. (2) The driver reads the silicon's own 64-bit value: `now()` takes `timerawh`, then `timerawl`, then `timerawh` again and retries while the two high reads disagree, concatenating `(hi as u64) shifted 32 | lo`. That is a TORN-READ guard over a hardware 64-bit counter, not a software roll-over extension — the halves are joined, never accumulated, and the SVD the PAC is generated from marks TIMERAWH and TIMERAWL read-only. The revision is pinned in Cargo.lock: embassy-rp 0.10.0, embassy-time 0.5.1 and embassy-time-driver 0.2.2 all at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (3) The rate is 1 MHz twice over. firmware/Cargo.toml:13 enables embassy-rp's `time-driver`, whose feature body is `embassy-time-driver?/tick-hz-1_000_000`; no manifest in this tree names a `tick-hz-*` feature of its own — `git grep tick-hz -- '*Cargo.toml'` exits 1 — so the driver crate's own no-feature fallback, `TICK_HZ = 1_000_000`, would give the same answer. firmware/src/presence.rs:137 asserts it at COMPILE time. TWO THINGS ABOUT THAT READING ARE NOT WHAT THEY LOOK LIKE, and both are here because nothing else in the tree says them. The grep is SCOPED because the unscoped one falsifies itself: this sentence used to say that `git grep tick-hz` over the tree answers nothing else, which was true of the tree it was written against and false of the tree it now sits in — three hits, every one of them this row's own prose and the page generated from it. A claim about what the tree contains cannot be spelled so that writing it changes the answer. And the citation to the manifest is INVISIBLE to the citation gate, so that gate's green says nothing about it: scripts/citation_gate.py:556 sets `EXTS` to `rs|sh|txt|py`, and no `.toml` line reference anywhere in this tree is therefore pinned or re-checked. A pre-existing class with other members — assurance/properties.toml:101, assurance/threat_clauses.toml:216-218 and formal/runs.toml:66 among them — which this row names rather than closes; read that line by CONTENT. (4) The boot configuration is the default one: firmware/src/main.rs:524 takes `embassy_rp::config::Config::default()` and edits only the XOSC startup delay, so the clocks are `ClockConfig::crystal(12_000_000)` with `ref_clk` from XOSC at div 1, hence clk_ref 12 MHz, and embassy's RP2350 arm writes `TICKS.timer0_cycles = clk_ref / 1_000_000`, which is 12. THE DIVISION: 2^64 ticks at one per microsecond is 18446744073709.55 s, 213503982.33 days, 584,542.05 years of 365.25 days. The route this row used to demand is refused by arithmetic and not by cost. THREE THINGS THIS DOES NOT SETTLE, and one of them is owned by nothing. The compile-time pin is not on every arm: firmware/src/presence.rs:137 sits under `cfg(all(not(feature = \"no-touch\"), not(feature = \"display\")))`, so the display build and the no-touch builds rest on the feature alone. That the counter ADVANCES is unread here and is PLAT-TIMER-003's, and this sentence read `unregistered anywhere` until that row was opened on 2026-09-05: it was true, and it sat beside a `supports` over three liveness properties that need progress and not rate, which is the defect the third row exists to close. Nothing in this tree's CODE would still notice a stopped tick — no assert, test or proof covers it — and the honest instrument remains a recording of an alarm-driven cadence, since a counter that stopped would freeze that render permanently; what changed is that PLAT-TIMER-003 asserts on it, over the recording PLAT-TIMER-002's run already produces. And the counter is NOT the read-only part an earlier reading of this row took it for: RP2350 TIMER0 at 0x400b0000 exposes TIMEHW and TIMELW as WRITE-ONLY halves of the same 64-bit value (the SVD's own words are `to change time write to timelw before timehw`), beside PAUSE, LOCKED and SOURCE. So the 64-bit wrap is unreachable by a RUN and reachable by a WRITE; what this tree has is no user of any of them, in the firmware or in embassy-rp." +discharge = "READ on 2026-09-05, and it is the first half of the route this row used to carry; the second half is PLAT-TIMER-002 because no run walks the one written here. FOUR READINGS AND ONE DIVISION. (1) `embassy_time::Instant` is `ticks: u64` and nothing narrows it. (2) The driver reads the silicon's own 64-bit value: `now()` takes `timerawh`, then `timerawl`, then `timerawh` again and retries while the two high reads disagree, concatenating `(hi as u64) shifted 32 | lo`. That is a TORN-READ guard over a hardware 64-bit counter, not a software roll-over extension — the halves are joined, never accumulated, and the SVD the PAC is generated from marks TIMERAWH and TIMERAWL read-only. The revision is pinned in Cargo.lock: embassy-rp 0.10.0, embassy-time 0.5.1 and embassy-time-driver 0.2.2 all at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (3) The rate is 1 MHz twice over. firmware/Cargo.toml:13 enables embassy-rp's `time-driver`, whose feature body is `embassy-time-driver?/tick-hz-1_000_000`; no manifest in this tree names a `tick-hz-*` feature of its own — `git grep tick-hz -- '*Cargo.toml'` exits 1 — so the driver crate's own no-feature fallback, `TICK_HZ = 1_000_000`, would give the same answer. firmware/src/presence.rs:141 asserts it at COMPILE time. TWO THINGS ABOUT THAT READING ARE NOT WHAT THEY LOOK LIKE, and both are here because nothing else in the tree says them. The grep is SCOPED because the unscoped one falsifies itself: this sentence used to say that `git grep tick-hz` over the tree answers nothing else, which was true of the tree it was written against and false of the tree it now sits in — three hits, every one of them this row's own prose and the page generated from it. A claim about what the tree contains cannot be spelled so that writing it changes the answer. And the citation to the manifest is INVISIBLE to the citation gate, so that gate's green says nothing about it: scripts/citation_gate.py:556 sets `EXTS` to `rs|sh|txt|py`, and no `.toml` line reference anywhere in this tree is therefore pinned or re-checked. A pre-existing class with other members — assurance/properties.toml:101, assurance/threat_clauses.toml:216-218 and formal/runs.toml:66 among them — which this row names rather than closes; read that line by CONTENT. (4) The boot configuration is the default one: firmware/src/main.rs:573 takes `embassy_rp::config::Config::default()` and edits only the XOSC startup delay, so the clocks are `ClockConfig::crystal(12_000_000)` with `ref_clk` from XOSC at div 1, hence clk_ref 12 MHz, and embassy's RP2350 arm writes `TICKS.timer0_cycles = clk_ref / 1_000_000`, which is 12. THE DIVISION: 2^64 ticks at one per microsecond is 18446744073709.55 s, 213503982.33 days, 584,542.05 years of 365.25 days. The route this row used to demand is refused by arithmetic and not by cost. THREE THINGS THIS DOES NOT SETTLE, and one of them is owned by nothing. The compile-time pin is not on every arm: firmware/src/presence.rs:141 sits under `cfg(all(not(feature = \"no-touch\"), not(feature = \"display\")))`, so the display build and the no-touch builds rest on the feature alone. That the counter ADVANCES is unread here and is PLAT-TIMER-003's, and this sentence read `unregistered anywhere` until that row was opened on 2026-09-05: it was true, and it sat beside a `supports` over three liveness properties that need progress and not rate, which is the defect the third row exists to close. Nothing in this tree's CODE would still notice a stopped tick — no assert, test or proof covers it — and the honest instrument remains a recording of an alarm-driven cadence, since a counter that stopped would freeze that render permanently; what changed is that PLAT-TIMER-003 asserts on it, over the recording PLAT-TIMER-002's run already produces. And the counter is NOT the read-only part an earlier reading of this row took it for: RP2350 TIMER0 at 0x400b0000 exposes TIMEHW and TIMELW as WRITE-ONLY halves of the same 64-bit value (the SVD's own words are `to change time write to timelw before timehw`), beside PAUSE, LOCKED and SOURCE. So the 64-bit wrap is unreachable by a RUN and reachable by a WRITE; what this tree has is no user of any of them, in the firmware or in embassy-rp." discharge_owner = "contributor" status = "discharged" evidence = [ @@ -1060,8 +1063,8 @@ depends_on = ["PLAT-TIMER-003"] [[assumption]] id = "PLAT-TIMER-003" class = "timers" -statement = "The timebase ADVANCES: RP2350 `TIMER0`'s counter keeps counting for as long as the board has power, so every deadline armed against it is eventually reached and every spin on `Instant::now()` eventually ends. This is the condition the three liveness properties rest on and PLAT-TIMER-001's RATE reading is not: `EveryOpQuiesces`, `EveryWaitReleases` and `EveryWalkCloses` are EVENTUALLY claims, so a tick twelve times slow still releases every wait and closes every walk twelve times later, and only a tick that STOPS falsifies them. Nothing in this tree asserts it — firmware/src/presence.rs:137 pins the RATE at compile time and on one build arm of three, and no assert, test or proof anywhere covers progress." -discharge = "PLAT-TIMER-002'S RECORDING, READ FOR A DIFFERENT QUESTION, and shared deliberately rather than thriftily: the alternative is a second unattended two-hour run producing the same series. THE INSTRUMENT, AND WHY IT WITNESSES PROGRESS — per backend, because the first version of this sentence gave one mechanism to three and only one of them has it. What all three share is the STEP: each advances one step per `Timer` await and per nothing else — firmware/src/led.rs:618 (ws2812, the default), firmware/src/led.rs:727 (pimoroni) and firmware/src/led.rs:692 (the gpio soft-PWM's 2 ms period). WHAT DIFFERS IS WHAT THE STEP PAINTS. Only ws2812's frame is a function of the loop's own counter: firmware/src/led.rs:589 declares it, firmware/src/led.rs:596 advances it once per await, firmware/src/led.rs:607 is the only thing that reads it, and firmware/src/led.rs:536's `effect_sparkle` re-rolls it every eight steps. GPIO AND PIMORONI TAKE THEIRS FROM THE CLOCK: both call `Blinker::tick` (firmware/src/led.rs:353) — at firmware/src/led.rs:695 and firmware/src/led.rs:719 — which compares `Instant::now()` against a stored phase end at firmware/src/led.rs:361 and flips the phase when it is reached. THE CONCLUSION SURVIVES THE CORRECTION, and twice over rather than by luck: a stopped counter never returns the await, so no backend takes a step at all; and a clock-driven frame that somehow did step would read a frozen `Instant::now()` and paint the same colour anyway. So on all three a frame that CHANGES is a witness that the await returned, and an await returns only when the counter reached the value `set_alarm` armed; a stopped counter holds the last frame for ever and nothing on the device repairs it. THE CHAIN, named so that a reader can see what produces the observable and what reads it: XOSC at 12 MHz drives clk_ref, which firmware/src/main.rs:524's default `ClockConfig` selects; clk_ref drives the TICKS block embassy enables with `timer0_cycles` = 12; that block increments TIMER0's counter once per microsecond; the counter reaches ALARM0; `TIMER0_IRQ_0` runs `check_alarm`; the queue wakes the render task spawned at firmware/src/main.rs:1022, firmware/src/main.rs:960 or firmware/src/main.rs:968; the task writes a frame; and a camera or a photodiode is what reads it. No host, no finger and no command appears anywhere in that chain. WHY NO READING DOES IT, and the trait contract is the part that surprises: `Driver::now`'s own documentation promises monotonic in the sense of `a greater or equal value than earlier calls`, which a stopped counter satisfies exactly — the contract does not contain this claim, so reading it cannot establish it. The hardware guarantee is a datasheet clause about the very silicon a run would be taken on. And the last in-tree reading — that no code writes TIMER0's PAUSE, SOURCE, LOCKED or the write-only TIMEHW/TIMELW halves, in this firmware or in embassy-rp — already sits inside PLAT-TIMER-001, and it is a reading of what the image does NOT do, which cannot establish what the silicon DOES. Of those four only PAUSE stops the count: SOURCE swaps the tick for clk_sys cycles, which is a RATE change and PLAT-TIMER-001's axis, and LOCKED only freezes write access. What no reading reaches at all is the clock underneath — a dead XOSC, or a TICKS block that quits, leaves every register this tree can name unwritten and the counter still. WHAT THE SHARED RECORDING SETTLES HERE AND NOT THERE, because two rows on one recording is where a reader stops telling them apart: this row reads the series for a stall that NEVER ENDS, PLAT-TIMER-002 for a stall of the order of one period that DOES end, and the verdicts do not move together. A stall that resumes is this row's PASS and that row's FAIL. A series that stops and never resumes is this row's FAIL and leaves that row INCONCLUSIVE rather than joining it. The one arm they share is a freeze beginning less than a period before the recording ends, inconclusive for both, and assurance/board/PLAT-TIMER-003.toml makes it a condition on the reading rather than only on the length." +statement = "The timebase ADVANCES: RP2350 `TIMER0`'s counter keeps counting for as long as the board has power, so every deadline armed against it is eventually reached and every spin on `Instant::now()` eventually ends. This is the condition the three liveness properties rest on and PLAT-TIMER-001's RATE reading is not: `EveryOpQuiesces`, `EveryWaitReleases` and `EveryWalkCloses` are EVENTUALLY claims, so a tick twelve times slow still releases every wait and closes every walk twelve times later, and only a tick that STOPS falsifies them. Nothing in this tree asserts it — firmware/src/presence.rs:141 pins the RATE at compile time and on one build arm of three, and no assert, test or proof anywhere covers progress." +discharge = "PLAT-TIMER-002'S RECORDING, READ FOR A DIFFERENT QUESTION, and shared deliberately rather than thriftily: the alternative is a second unattended two-hour run producing the same series. THE INSTRUMENT, AND WHY IT WITNESSES PROGRESS — per backend, because the first version of this sentence gave one mechanism to three and only one of them has it. What all three share is the STEP: each advances one step per `Timer` await and per nothing else — firmware/src/led.rs:618 (ws2812, the default), firmware/src/led.rs:727 (pimoroni) and firmware/src/led.rs:692 (the gpio soft-PWM's 2 ms period). WHAT DIFFERS IS WHAT THE STEP PAINTS. Only ws2812's frame is a function of the loop's own counter: firmware/src/led.rs:589 declares it, firmware/src/led.rs:596 advances it once per await, firmware/src/led.rs:607 is the only thing that reads it, and firmware/src/led.rs:536's `effect_sparkle` re-rolls it every eight steps. GPIO AND PIMORONI TAKE THEIRS FROM THE CLOCK: both call `Blinker::tick` (firmware/src/led.rs:353) — at firmware/src/led.rs:695 and firmware/src/led.rs:719 — which compares `Instant::now()` against a stored phase end at firmware/src/led.rs:361 and flips the phase when it is reached. THE CONCLUSION SURVIVES THE CORRECTION, and twice over rather than by luck: a stopped counter never returns the await, so no backend takes a step at all; and a clock-driven frame that somehow did step would read a frozen `Instant::now()` and paint the same colour anyway. So on all three a frame that CHANGES is a witness that the await returned, and an await returns only when the counter reached the value `set_alarm` armed; a stopped counter holds the last frame for ever and nothing on the device repairs it. THE CHAIN, named so that a reader can see what produces the observable and what reads it: XOSC at 12 MHz drives clk_ref, which firmware/src/main.rs:573's default `ClockConfig` selects; clk_ref drives the TICKS block embassy enables with `timer0_cycles` = 12; that block increments TIMER0's counter once per microsecond; the counter reaches ALARM0; `TIMER0_IRQ_0` runs `check_alarm`; the queue wakes the render task spawned at firmware/src/main.rs:1071, firmware/src/main.rs:1009 or firmware/src/main.rs:1017; the task writes a frame; and a camera or a photodiode is what reads it. No host, no finger and no command appears anywhere in that chain. WHY NO READING DOES IT, and the trait contract is the part that surprises: `Driver::now`'s own documentation promises monotonic in the sense of `a greater or equal value than earlier calls`, which a stopped counter satisfies exactly — the contract does not contain this claim, so reading it cannot establish it. The hardware guarantee is a datasheet clause about the very silicon a run would be taken on. And the last in-tree reading — that no code writes TIMER0's PAUSE, SOURCE, LOCKED or the write-only TIMEHW/TIMELW halves, in this firmware or in embassy-rp — already sits inside PLAT-TIMER-001, and it is a reading of what the image does NOT do, which cannot establish what the silicon DOES. Of those four only PAUSE stops the count: SOURCE swaps the tick for clk_sys cycles, which is a RATE change and PLAT-TIMER-001's axis, and LOCKED only freezes write access. What no reading reaches at all is the clock underneath — a dead XOSC, or a TICKS block that quits, leaves every register this tree can name unwritten and the counter still. WHAT THE SHARED RECORDING SETTLES HERE AND NOT THERE, because two rows on one recording is where a reader stops telling them apart: this row reads the series for a stall that NEVER ENDS, PLAT-TIMER-002 for a stall of the order of one period that DOES end, and the verdicts do not move together. A stall that resumes is this row's PASS and that row's FAIL. A series that stops and never resumes is this row's FAIL and leaves that row INCONCLUSIVE rather than joining it. The one arm they share is a freeze beginning less than a period before the recording ends, inconclusive for both, and assurance/board/PLAT-TIMER-003.toml makes it a condition on the reading rather than only on the length." discharge_owner = "maintainer" status = "pending" failure_direction = "split, and the security half is the one PLAT-TIMER-002 does not have. A stopped counter WEDGES everything that awaits a `Timer` — the LED render, the worker's 16 ms button poll (firmware/src/worker.rs:307), and any open presence wait, whose crates/rsk-device/src/presence.rs:215 busy poll then compares a frozen `now_us` against its timeout for ever — and that is availability, the comparator's direction. But the windows measured on command ARRIVAL do not wedge, they stop counting: the pinUvAuthToken's rolling and absolute caps are `saturating_sub` over a millisecond count that no longer grows, so a grant minted before the stop never retires, while the USB stack, which is interrupt-driven rather than timer-driven, goes on answering. A live grant that outlives its cap is security, and silently. Not an ambiguity but a rule: what AWAITS a timer wedges, what SUBTRACTS two clock readings freezes open" @@ -1145,7 +1148,7 @@ supports = ["SEC-FIDO-002"] id = "PLAT-PRES-002" class = "model-abstraction" statement = "No API in the tree can originate a cancel from `SCOPE_CCID` or from an on-panel flow, so a CCID or panel ceremony is uncancellable by ABSENCE of a producer rather than by a guard anything checks." -discharge = "A whole-tree measurement, and it is a grep rather than a proof: `git grep -n 'cancel_requested.store(true'` is exactly two sites, `crates/rsk-device/src/presence.rs:120` (`request_cancel`, gated on `SCOPE_FIDO`) and `:131` (`cancel_otp_wait`, gated on `SCOPE_OTP`), and the only other writer is `set_cancel_requested`, whose six production call sites (`crates/rsk-device/src/presence.rs:196, :233` and `crates/rsk-display/src/pin.rs:317, :441` + `crates/rsk-display/src/presence.rs:47, :58`) all pass a literal `false`, the two `cfg(feature = \"display\")` forwarders at `firmware/src/presence.rs:269` and `firmware/src/display.rs:198` reaching only those; the one site that passes `true` is `crates/rsk-device/src/presence_kani.rs:158`, a harness. The Kani cancel harness rests on this: `board.attempted[SCOPE_CCID]` stays false because the harness raises only the two cancels that exist, so 'a CCID wait cannot be cancelled at all' is the harness's construction and not its conclusion. A third writer added anywhere in the tree falsifies it with every harness still green." +discharge = "A whole-tree measurement, and it is a grep rather than a proof: `git grep -n 'cancel_requested.store(true'` is exactly two sites, `crates/rsk-device/src/presence.rs:120` (`request_cancel`, gated on `SCOPE_FIDO`) and `:131` (`cancel_otp_wait`, gated on `SCOPE_OTP`), and the only other writer is `set_cancel_requested`, whose six production call sites (`crates/rsk-device/src/presence.rs:196, :233` and `crates/rsk-display/src/pin.rs:317, :441` + `crates/rsk-display/src/presence.rs:47, :58`) all pass a literal `false`, the two `cfg(feature = \"display\")` forwarders at `firmware/src/presence.rs:276` and `firmware/src/display.rs:198` reaching only those; the one site that passes `true` is `crates/rsk-device/src/presence_kani.rs:158`, a harness. The Kani cancel harness rests on this: `board.attempted[SCOPE_CCID]` stays false because the harness raises only the two cancels that exist, so 'a CCID wait cannot be cancelled at all' is the harness's construction and not its conclusion. A third writer added anywhere in the tree falsifies it with every harness still green." discharge_owner = "contributor" status = "pending" failure_direction = "security: a new cancel producer is invisible to the harness that reads as covering CCID, and the negative obligation for two of the four owners rests on it" @@ -1526,7 +1529,7 @@ supports = ["SEC-STORE-002"] [[assumption]] id = "PLAT-STORE-003" class = "model-abstraction" -statement = "GIVEN a medium that tears the way PLAT-FLASH-001 asserts, a reboot recovers the committed store: after any number of power cuts every key whose write or remove returned cleanly reads back exactly, and only the operation the cut interrupted is ambiguous — its old value or its new one, never a third older one. The `GIVEN` is the whole narrowing and it is what makes this row dischargeable by a mock; the medium half is PLAT-FLASH-001's and is not restated here. firmware/src/main.rs:555 runs `Fs::scan` once at boot, and every later absence answer is that one walk's." +statement = "GIVEN a medium that tears the way PLAT-FLASH-001 asserts, a reboot recovers the committed store: after any number of power cuts every key whose write or remove returned cleanly reads back exactly, and only the operation the cut interrupted is ambiguous — its old value or its new one, never a third older one. The `GIVEN` is the whole narrowing and it is what makes this row dischargeable by a mock; the medium half is PLAT-FLASH-001's and is not restated here. firmware/src/main.rs:604 runs `Fs::scan` once at boot, and every later absence answer is that one walk's." discharge = "A run record naming a verdict, which is the half that does not exist. The CONTRACT is asserted already: fuzz/fuzz_targets/kv_durability.rs states it in as many words in its header and drives one `MapStorage` partition directly, and fuzz/fuzz_targets/power_cut.rs torments the whole rsk-fs stack over the same power-cuttable mock. Both are on the weekly roster — scripts/fuzz-all.sh:41 takes it from `cargo fuzz list` rather than a hand-written table, so neither can silently fall off it. What nothing does is READ those verdicts: `kv_durability` appears in no workflow, no docs page, no script and no assurance file, so its result is a green shard nothing cites. Why this row is `model-abstraction` and not `flash`, decided by measurement rather than by taxonomy: the class is the whole difference between a discharge that must name silicon and one that need not. Marked `discharged` with `evidence = [\"fuzz/fuzz_targets/kv_durability.rs\"]` this row is exit 0 under `model-abstraction` and exit 1 under `flash` — `PLAT-STORE-003: a flash discharge records no 'board_revision'`. As the statement stood before the `GIVEN`, `flash` was the honest class and the row was mis-classed. Narrowing the statement is the better repair: classing it `flash` would oblige a stepping for a defect that sits ABOVE the medium, which is exactly the mistake PLAT-STORE-001 exists to undo — a software question parked behind hardware that cannot answer it — and this row's `discharge_owner` is `contributor`, who under AGENTS.md may not produce a board revision at all. What the narrowing does NOT buy, in the reading this row carried until the hole was closed: `model-abstraction` accepted any path that existed, so `evidence = [\"README.md\"]` on this row was also exit 0, measured. That half is now `check_evidence`'s `PROSE_PAGE` rule and it belongs to every row of the class rather than to this one: a `.md` that no generator both CLAIMS and MARKS is refused as evidence anywhere in the registry, whatever its case, and so is a directory and any spelling git's listing does not have — `docs`, `README.MD`, and a generator-claimed page with its own header line deleted, were each exit 0 after the rule's first version, measured. So is a page rendered FROM this registry, which that version admitted through its own carve-out: `evidence = [\"docs/platform-assumptions.md\"]` on this row was exit 0 while that page prints this row's `discharge` verbatim. What the rule still does not buy is relevance: an artifact of legal shape and no bearing on the claim, `deny.toml`, is exit 0 on this row today, measured, and only a reader stops that. Nor does it stop a `*_gate.py` DECLARING a page it does not write, because nothing here runs a generator — that move now costs a second edit, since the page must carry the marker too." discharge_owner = "contributor" status = "pending" diff --git a/docs/assurance-matrix.md b/docs/assurance-matrix.md index ce4787401..e003c581a 100644 --- a/docs/assurance-matrix.md +++ b/docs/assurance-matrix.md @@ -23,7 +23,7 @@ These are the **committed** configurations, not the buildable ones. Every `mkFir ## The axes - **19 flake packages** (`nix/firmware.nix`), of which **14** are published by `.github/workflows/release-build.yml` — the published set is a named subset of the matrix, never the matrix. -- **6 orthogonal cargo features** (`firmware/Cargo.toml`) that no flake package expresses. +- **7 orthogonal cargo features** (`firmware/Cargo.toml`) that no flake package expresses. - **6 board presets** (`firmware/boards/*.toml`): `BOARD=` sets the same knobs the flake arguments do, and no cargo feature. ## Columns @@ -57,59 +57,60 @@ The last cell is DERIVED, not declared: the per-crate cargo-feature closure this | 23 | `fido-conformance` | feature | n/a | `fido-conformance` | — | `firmware` +`fido-conformance`, `rsk-fido` +`fido-conformance` +`strict-up` | | 24 | `ea-conformance-rpid` | feature | n/a | `ea-conformance-rpid` | — | `firmware` +`ea-conformance-rpid`, `rsk-fido` +`ea-conformance-rpid` +`fido-conformance` +`strict-up` | | 25 | `largeblob-ext` | feature | n/a | `largeblob-ext` | — | `firmware` +`largeblob-ext`, `rsk-fido` +`largeblob-ext` | -| 26 | `abrobot-16m` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=16`, `led.kind=ws2812`, `led.max_leds=4`, `led.order=grb`, `led.pin=16`, `presence.active_high=False`, `presence.pin=23`, `presence.source=gpio`, `usb.vidpid=RSKey` | — | -| 27 | `abrobot-4m` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=4`, `led.kind=ws2812`, `led.max_leds=4`, `led.order=grb`, `led.pin=16`, `presence.active_high=False`, `presence.pin=23`, `presence.source=gpio`, `usb.vidpid=RSKey` | — | -| 28 | `seeed-xiao` | board | n/a | — | `flash.kvmain_kb=896`, `flash.size_mb=2`, `led.kind=ws2812`, `led.order=grb`, `led.pin=22`, `led.power_pin=23`, `presence.source=bootsel`, `usb.vidpid=RSKey`, `usr_led.active_high=False`, `usr_led.pin=25` | — | -| 29 | `tenstar-usb` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=16`, `led.kind=ws2812`, `led.order=grb`, `led.pin=22`, `presence.active_high=False`, `presence.pin=15`, `presence.source=gpio`, `usb.vidpid=RSKey` | — | -| 30 | `waveshare-one` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=4`, `led.kind=ws2812`, `led.order=rgb`, `led.pin=16`, `presence.source=bootsel`, `usb.vidpid=RSKey` | — | -| 31 | `waveshare-touch-lcd` | board | n/a | — | `display.bl_pin=16`, `display.bl_pwm_channel=A`, `display.bl_pwm_slice=0`, `display.cs=13`, `display.dc=14`, `display.rst=15`, `display.spi_freq_hz=80000000`, `display.tp_rst=17`, `display.wake_active_high=False`, `display.wake_pin=25`, `flash.kvmain_kb=1408`, `flash.size_mb=16`, `led.kind=none`, `presence.source=bootsel`, `usb.vidpid=RSKey` | — | +| 26 | `panel` | feature | n/a | `panel` | — | `firmware` +`panel`, `rsk-ui` (added) | +| 27 | `abrobot-16m` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=16`, `led.kind=ws2812`, `led.max_leds=4`, `led.order=grb`, `led.pin=16`, `presence.active_high=False`, `presence.pin=23`, `presence.source=gpio`, `usb.vidpid=RSKey` | — | +| 28 | `abrobot-4m` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=4`, `led.kind=ws2812`, `led.max_leds=4`, `led.order=grb`, `led.pin=16`, `presence.active_high=False`, `presence.pin=23`, `presence.source=gpio`, `usb.vidpid=RSKey` | — | +| 29 | `seeed-xiao` | board | n/a | — | `flash.kvmain_kb=896`, `flash.size_mb=2`, `led.kind=ws2812`, `led.order=grb`, `led.pin=22`, `led.power_pin=23`, `presence.source=bootsel`, `usb.vidpid=RSKey`, `usr_led.active_high=False`, `usr_led.pin=25` | — | +| 30 | `tenstar-usb` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=16`, `led.kind=ws2812`, `led.order=grb`, `led.pin=22`, `presence.active_high=False`, `presence.pin=15`, `presence.source=gpio`, `usb.vidpid=RSKey` | — | +| 31 | `waveshare-one` | board | n/a | — | `flash.kvmain_kb=1408`, `flash.size_mb=4`, `led.kind=ws2812`, `led.order=rgb`, `led.pin=16`, `presence.source=bootsel`, `usb.vidpid=RSKey` | — | +| 32 | `waveshare-touch-lcd` | board | n/a | — | `display.bl_pin=16`, `display.bl_pwm_channel=A`, `display.bl_pwm_slice=0`, `display.cs=13`, `display.dc=14`, `display.rst=15`, `display.spi_freq_hz=80000000`, `display.tp_rst=17`, `display.wake_active_high=False`, `display.wake_pin=25`, `flash.kvmain_kb=1408`, `flash.size_mb=16`, `led.kind=none`, `presence.source=bootsel`, `usb.vidpid=RSKey` | — | ## The matrix -40 rows × 31 columns = 1240 cells. Column numbers are the table above. - -| Property | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| `SEC-FIDO-001` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | -| `SEC-FIDO-002` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | -| `SEC-FIDO-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-004` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-005` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-006A` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-006B` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-006C` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-007` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-FIDO-008` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-SEAM-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | -| `SEC-SEAM-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | -| `SEC-SEAM-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | -| `SEC-SEAM-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-STORE-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-STORE-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-STORE-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-STORE-004` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-STORE-005` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-STORE-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-LAT-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-LAT-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-LAT-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-POL-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-POL-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-POL-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-POL-004` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | -| `SEC-POL-005` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-POL-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-ADM-002` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | -| `SEC-ADM-004` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-DISP-001` | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | -| `SEC-DISP-002` | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | -| `SEC-DISP-003` | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | -| `SEC-BOOT-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-BOOT-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | -| `SEC-TRANS-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | -| `SEC-TRANS-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | -| `SEC-TRANS-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | +40 rows × 32 columns = 1280 cells. Column numbers are the table above. + +| Property | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| `SEC-FIDO-001` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | +| `SEC-FIDO-002` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | +| `SEC-FIDO-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-004` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-005` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-006A` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-006B` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-006C` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-007` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-FIDO-008` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-SEAM-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | +| `SEC-SEAM-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | +| `SEC-SEAM-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | +| `SEC-SEAM-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-STORE-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-STORE-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-STORE-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-STORE-004` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-STORE-005` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-STORE-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-LAT-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-LAT-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-LAT-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-POL-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-POL-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-POL-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-POL-004` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | +| `SEC-POL-005` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-POL-006` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-ADM-002` | cov | oos | gap | gap | gap | oos | oos | oos | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | +| `SEC-ADM-004` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-DISP-001` | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | +| `SEC-DISP-002` | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | +| `SEC-DISP-003` | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | oos | oos | oos | gap | oos | oos | oos | oos | oos | oos | +| `SEC-BOOT-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-BOOT-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | gap | equ | gap | +| `SEC-TRANS-001` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | +| `SEC-TRANS-002` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | +| `SEC-TRANS-003` | cov | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | gap | equ | gap | equ | equ | gap | gap | gap | gap | gap | gap | gap | gap | equ | equ | equ | equ | equ | equ | ## Dispositions @@ -219,6 +220,7 @@ The two after it are the ledger's, and they are Stage 0's last exit bullet: who | `fido-conformance` | 37 | 11 | contributor | `evidence` | Built only for the FIDO Conformance Tool run: it suppresses the EdDSA advertisement. Its "is a conformance-only build in the supported set" half was the same parked ruling the three measurement columns carried and is settled above with them. What is left is narrower AND wider than the sentence used to say, because the closure the page derives names a second feature: `fido-conformance` implies `strict-up`, so this column also demands a touch on EVERY assertion. Two questions then, not one — does any P0-family row depend on the advertised algorithm list, and does `firmware-strict-up`'s transport-arity question apply here too? | | `ea-conformance-rpid` | 37 | 11 | contributor | `evidence` | Adds the conformance tool's RPID to the vendor-facilitated enterprise-attestation list, which is an authorization-relevant allowlist. `SEC-FIDO-001` is about gates; is an EA RPID list one of them? | | `largeblob-ext` | 37 | 11 | contributor | `evidence` | The sharpest orthogonal feature: it serves the CTAP 2.3 `largeBlob` extension INSTEAD OF the 2.1 `largeBlobKey` + `authenticatorLargeBlobs` pair (the spec forbids both), it carries four `check.sh` rows, and it has zero flake packages — so nothing in the release axis would ever have given it a cell. Does the swapped surface move any credential-management or authorization transition the P0-launch models name? | +| `panel` | 40 | 2 | contributor | `absence` | The status-only screen: `rsk-ui` for its fonts and `firmware/src/panel.rs` over SPI1. It wraps the BOOTSEL button in a `UserPresence` impl that paints a banner for the length of the wait and forwards the decision unchanged — no `Confirm` reaches the panel, `shows_confirm` keeps its false default, and there is no PIN surface at all. Is the delta for every P0-family row genuinely out-of-scope, or does a wrapper on the presence path — beside a screen that reads the build counter, the chip serial, the attach flag and uptime — put a new path under an authorisation row? | | `abrobot-16m` | 31 | 0 | contributor | `evidence` | The 16 MB geometry question of `firmware-16mb`, plus a GPIO presence source instead of BOOTSEL. | | `abrobot-4m` | 4 | 0 | maintainer | `evidence` | The narrowest column with anything open, and it is now one group and one question: does a GPIO button on 23 (active-low) deliver the Confirmed/Cancelled semantics the presence model assumes of BOOTSEL — same debounce, same cancellation, same per-transport arbitration? Only the four rows that quantify over a presence decision are still open on it. The four reset clauses used to be held here as reached-through-a-touch; that is a reachability argument and it is withdrawn in the cell above, where applied evenly it would have taken SEC-FIDO-003 out with them. What is left is genuinely about the button: `crates/rsk-device/src/presence.rs` arbitrates the scope and `firmware/src/presence.rs` holds the latch, and both compile identically here — the one arm that moves is the raw sample. | | `seeed-xiao` | 31 | 0 | contributor | `evidence` | 2 MB with `KVMAIN` at 896 K — the `firmware-2mb` question on a board that also gates the LED behind a power pin. | diff --git a/docs/assurance-vector.md b/docs/assurance-vector.md index 8bf093980..6caa4746a 100644 --- a/docs/assurance-vector.md +++ b/docs/assurance-vector.md @@ -151,6 +151,7 @@ The same 40 P0-family rows, counted the other way round: per column rather than | `fido-conformance` | feature | no | 3 | 37 | | `ea-conformance-rpid` | feature | no | 3 | 37 | | `largeblob-ext` | feature | no | 3 | 37 | +| `panel` | feature | no | 0 | 40 | | `abrobot-16m` | board | no | 9 | 31 | | `abrobot-4m` | board | no | 36 | 4 | | `seeed-xiao` | board | no | 9 | 31 | @@ -221,7 +222,7 @@ Three spellings of "not current", which used to sit on two different pages and i | platform | `PLAT-TOOLCHAIN-002` | docs/unsafe.md is the enumeration of the first-party `unsafe` sites: e | | platform | `PLAT-UNSAFE-002` | `SendUsb` is sound: `embassy_usb::UsbDevice` is `!Send` only for the ` | | platform | `PLAT-UNSAFE-003` | `HEAP.init` runs exactly once, over a static buffer nothing else touch | -| platform | `PLAT-UNSAFE-004` | Each of the eight build-selected GPIOs handed to `AnyPin::steal` has e | +| platform | `PLAT-UNSAFE-004` | Each of the eleven build-selected GPIOs handed to `AnyPin::steal` has | | platform | `PLAT-UNSAFE-005` | The two `static mut` prime sieves are single-core-exclusive: `CORE0_SI | | platform | `PLAT-UNSAFE-006` | Each core programs MSPLIM once, on the core that owns that stack, befo | | platform | `PLAT-UNSAFE-007` | The three FFI calls into the vendored ARM assembly pass fully owned, l | diff --git a/docs/build.md b/docs/build.md index e2bb2f5d2..fb83784c3 100644 --- a/docs/build.md +++ b/docs/build.md @@ -41,6 +41,7 @@ flowchart TD | `always-uv` | off | Bake the CTAP 2.1 `alwaysUv` option **on by default**, so the key requires user verification (a PIN — or the `display` build's built-in UV pad) for **every** makeCredential / getAssertion straight out of the box, with no post-flash `ykman fido config toggle-always-uv`. The shipped image leaves alwaysUv off until a platform turns it on; this flag flips the default. It stays runtime-toggleable — `toggleAlwaysUv` writes an override that survives reboots but not an `authenticatorReset`, which restores this compiled default. **Set a PIN after flashing:** with alwaysUv on and no PIN, FIDO operations return `CTAP2_ERR_PUAT_REQUIRED` until one is set — the standard cue for the platform (Windows, Chrome) to prompt for a PIN. While alwaysUv is on the **CTAP1/U2F interface is disabled** (CTAP 2.1 §7.2.4, as on a YubiKey): U2F only proves presence, never verification, so legacy U2F-only logins stop working, and getInfo drops `U2F_V2` from its advertised `versions`; WebAuthn / CTAP2 (passkeys) are unaffected. The one exception §7.2.4 allows is a build "protected by a built-in user verification method": on a `display` build **with a PIN set**, U2F stays available and every register / authenticate collects the PIN on the panel instead of a touch. | | `largeblob-ext` | off | Serve the **CTAP 2.3 §12.4 `largeBlob` extension instead of** the CTAP 2.1 large-blob design. Not additive, and not a choice the firmware could dodge: §12.4 says *"Authenticators MUST NOT support both extensions"*, so enabling it **withdraws** the `largeBlobKey` extension, the `authenticatorLargeBlobs` command (`0x0C`, which then answers `CTAP1_ERR_INVALID_COMMAND`), the `largeBlobs` option and `maxSerializedLargeBlobArray`. Blobs then live **per credential** (discoverable credentials only) instead of in one platform-managed array, and a whole blob rides in the `getAssertion` that reads or writes it — up to 4046 bytes, sealed at rest under the device seed with the credential id as AAD, which the 2.1 design does not need because the platform encrypts that array itself. Off by default, and think before turning it on: the 2.1 pair is what every shipping browser drives today, so a `largeblob-ext` key **loses WebAuthn `largeBlob` support with current clients**. | | `display` | off | **Experimental** trusted-display build for a screen + touch board (Waveshare RP2350-Touch-LCD-2.8). Adds an on-screen **Approve / Deny** that paints the real relying party for every touch (Deny refuses with `OPERATION_DENIED`), an on-device **PIN pad** (built-in user verification (`options.uv`), plus a CCID **pinpad** so GnuPG / OpenSC collect the OpenPGP / PIV PIN on the panel, never over USB), a read-only **Apps** browser (OpenPGP / PIV / OATH state, no PIN), a **Passkeys** manager (delete / rename on-device), and **Settings** (device & FIDO PINs, an optional scrambled PIN pad, on-screen BIP-39 / SLIP-39 recovery export, audit log, factory reset). Entirely `dep:`-gated. A standard key compiles **none** of it, so its image is unaffected. Build it `LED_KIND=none FLASH_SIZE=16M … --features display` (the panel takes GPIO16 for its backlight; a compile-time guard enforces `LED_KIND=none`). Full walkthrough with screenshots: [guides/display.md](guides/display.md). | +| `panel` | off | **Experimental** status-only screen build for a board with an onboard SPI LCD and **no touch** (the reference is ALIENTEK's RP2350A small-system board: a 1.14" ST7789V2, 240×135, on SPI1). It paints the device's own status — the build `bcdDevice`, the chip serial, whether USB is attached, and uptime — and shows a **PRESS THE KEY** banner while a user-presence wait is pending; it adds **no** presence *source* (the BOOTSEL button still decides, the banner renders no operation and no relying party, and `shows_confirm()` stays false), so this is **not** the trusted display and every host ceremony is unchanged. The panel's control pins and SPI clock come from a board file's `[panel]` section, defaulting to that reference board's wiring (`cs`=9, `dc`=8, backlight=25 — active-low, through the board's PNP high-side switch); SCK/MOSI on GPIO10/11 are hard-wired. Entirely `dep:`-gated, so a standard key compiles none of it, and mutually exclusive with `display` (a compile error names both). Build `LED_KIND=none ... --features panel`. | | `strict-config` | off | **Restores the historical strict admin-write authorization.** The DEFAULT build is now the permissive, full-ykman/YubiKey-compatible admin surface: device-config writes (CCID Management `WRITE CONFIG`, FIDO vendor `CONFIG_WRITE`, the CTAPHID `0x43` and OTP-HID `0x15` transport writes) are **ungated**, and Management RESET (device-wide factory reset) plus the OTP-HID `DEVICE_CONFIG` / `SCAN_MAP` / `NDEF` slots are served. `--features strict-config` re-imposes the presence/PIN gates and refuses the ungated transport writes — the historical shipped behavior. This deliberately weakens the DEFAULT threat model: any USB host can rewrite the reported identity with no operator confirmation ([threat-model.md](threat-model.md)). **Not** the runtime flash flag `EF_HARDENED`. Ship it as `firmware-strict-config`. | ## Environment variables diff --git a/docs/platform-assumptions.md b/docs/platform-assumptions.md index 7d787151c..86793aca8 100644 --- a/docs/platform-assumptions.md +++ b/docs/platform-assumptions.md @@ -31,12 +31,12 @@ Two prices this page owes the reader rather than the rule. An arm has to OPEN a | `PLAT-TIMER-002` | planned | The alarm comparator's wrap is invisible to everything above it. PASS = across a run of at least two periods with no host traffic inside the window, the recorded LED never stops changing -- no interval longer than 10 s in which it holds one value, against an ambient cycle of 1 s on the blinking backends and about 2 s on the breathing one -- and the bus capture (or the absence of a host) shows the window was quiet. AND THE BOARD NEITHER REBOOTED NOR LOST ITS SUPPLY: on the bracketed arm no second `EV_BOOT` in the post-window journal and a closing `uptime` accounting for the whole recording, on the charger arm a supply shown not to have dropped. That condition sits on the PASS and not only on the FAIL because the mirror is real: a board that browns out every thirty seconds never reaches a crossing at all, and its recording is a render that never stops changing -- this row's PASS over a run that tested nothing. FAIL = one interval of the order of the period itself, up to 71 min 35 s, in which the LED holds a single value and then resumes. That is what a missed match costs: the deadline is not late by a millisecond, it waits for the counter's low half to bring the same armed value round again, and it is the ONE alarm the whole queue is served by, so what stalls is every timer on the device at once. A FAIL REFUTES THE STATEMENT -- scripts/platform_gate.py:1337 maps `fail` to `refuted` -- so the interval alone is not enough, and two causes that have nothing to do with the comparator produce exactly it. A brown-out that recovers leaves a dead stretch and then repaints the render. A probe that halts a core and resumes it does the same, and TIMER0's DBGPAUSE makes that a DOCUMENTED way to stop this counter -- assurance/board/PLAT-TIMER-003.toml refuses a debugger by name and this row did not. So a FAIL is owed, beside the interval: no debugger attached at any point in the window; a supply shown not to have dropped; and, on the bracketed arm, no second `EV_BOOT` and a closing `uptime` accounting for the whole recording. INCONCLUSIVE = a run shorter than two periods; a recording with a gap in it, because a crossing that cannot be shown to have been watched is not a crossing that was survived; ANY host traffic inside the window, because a request re-arms the comparator and would repair a missed match before the LED could show it, which would make the run an observation of the host rather than of the wrap; a debugger attached at any point, since a halted core stops this counter by design and the run is then about the probe; a supply that dropped, or a second `EV_BOOT` in the post-window journal, or a closing `uptime` materially shorter than the recording -- a reboot, which the second `EV_BOOT` is what names, both repaints the render and restarts the comparator's phase, while a short `uptime` with NO such entry is the counter having stopped, the last arm below; neither the stall nor its absence is this row's to read on either; the charger arm run with no supply record, which has no reboot instrument at all; and an image or an LED configuration with no animation in it -- `LED_KIND=none`, brightness zero, steady mode -- which observes an absent instrument rather than a wrap; and a series that stops and NEVER resumes before the recording ends, which is not this row's FAIL at all -- a comparator whose counter has stopped can be observed neither to miss a match nor to make one, so this row has nothing left to read. WHAT THIS RECORDING SETTLES HERE AND NOT IN assurance/board/PLAT-TIMER-003.toml, which reads the same series for progress. The FAIL above is that row's PASS: a stall of the order of one period that ENDS is a counter that went on counting and brought the armed value round again. The stall that never ends is that row's FAIL and this row's INCONCLUSIVE, above. And the one arm both records answer the same way is a freeze beginning less than a period before the recording ends, inconclusive for both because nothing has yet told the two apart -- which is the second thing the second period buys, beside the one the method already names. | | `PLAT-TIMER-003` | planned | The counter advances for the whole of a bounded window, and the render is what says so. PASS = the recorded series has NO interval in which the sampled LED takes one value and never changes again -- every quiet stretch is followed by more change, right up to the last sample -- AND, on the bracketed arm, the `uptime` of the entry THE CLOSING COMMAND APPENDED is not materially shorter than the wall-clock length of the recording and no second `EV_BOOT` stands in the window, which together rule out a reboot that repainted the render; on the charger arm a supply shown not to have dropped stands in for both. FAIL = the series stops changing and stays stopped to the end of the recording, with at least one full period, 4294.967296 s, recorded after it stops. That is a counter that quit: every task in the queue is waiting on an alarm that can no longer match, and the three properties this row supports -- `EveryOpQuiesces`, `EveryWaitReleases`, `EveryWalkCloses` -- are false of every operation in flight and of every one begun afterwards, each being an EVENTUALLY claim on a device where nothing is ever again. THE SECOND WITNESS IS OWED HERE TOO, and was not until 2026-09-05: this arm sat under a PASS carrying an uptime condition and carried none itself, while a FAIL REFUTES the statement (scripts/platform_gate.py:1337 maps `fail` to `refuted`). So a FAIL needs, beside the frozen series: on the bracketed arm a closing `uptime` materially shorter than the recording and no second `EV_BOOT` saying the board rebooted, and on the charger arm a supply shown not to have dropped. WITHOUT IT THE ARM IS SATISFIED BY A HALTED PROGRAM RATHER THAN A STOPPED COUNTER, which is the nearest thing this tree has to this row's own failure and is reachable in it: firmware/Cargo.toml:75 is `panic-halt = "1"` and nothing arms a watchdog reset anywhere -- `WATCHDOG` appears only as the scratch register firmware/src/pin_lock.rs:54 writes -- so a panicking render task freezes the frame for ever with the counter in perfect health. AND IF THE CLOSING COMMAND DOES NOT COME BACK, that is a reading rather than a gap: a stopped counter wedges everything that awaits a `Timer`, so a device that still ENUMERATES and will not answer the close answers this arm the same way a short `uptime` does. A device that does not enumerate at all is the INCONCLUSIVE arm below instead, because a halted core produces that too and nothing outside the device tells the two apart. INCONCLUSIVE, and every arm of it but the last is about the RUN. That matters because the last one IS about the configuration and could otherwise be read as this row forbidding its own PASS: it does not, because the image `boot_config` names takes none of these arms -- the shipped default compiles a render task, animates it out of the factory `EF_LED_CONF`, needs no debugger and no host, and so produces the PASS above on any supply that holds. The arms: a recording with a gap, since a stretch nobody watched cannot be shown to have been a stretch of change; the device does not enumerate after the window, which is the halted program's other half and an arm of its own rather than a note on the one above -- a panic takes USB down with the render, so the second witness is unreadable at exactly the moment the reading calls for it, and a frozen frame with no journal behind it does not tell a stopped counter from a stopped program; a freeze beginning inside the last period, which is PLAT-TIMER-002's stall and this row's failure with nothing yet to tell them apart; a debugger attached, since DBGPAUSE makes a halted core a documented way to stop this counter and the run would then be about the probe; a supply that dropped, or a SECOND `EV_BOOT` in the post-window journal, since a reboot repaints the render and reads as continued progress -- and it is that entry and not a short `uptime` that names a reboot, because a stopped counter shortens the `uptime` as well and THAT case is this row's FAIL above, not this arm; a recording sampled at or near the ambient cadence, which aliases into a held value; and an image or an LED configuration with no animation in it, which observes an absent instrument. WHAT THE SHARED RECORDING DOES NOT SETTLE HERE, said because a reader who has just read the other record will carry its verdict across. PLAT-TIMER-002's PASS is not this row's: it is satisfied by a counter that stops one second after the recording ends. PLAT-TIMER-002's FAIL is this row's PASS, since a stall that resumes is a counter that went on counting. Only the never-resuming stall is this row's FAIL, and it leaves that row INCONCLUSIVE rather than joining it. | | `PLAT-TOOL-002` | planned | The board's session agrees with the emulator's at every recorded boundary. PASS = no divergence. FAIL = any boundary where the board gates differently, which makes every trace-linked claim a claim about the emulator. INCONCLUSIVE = the session could not be driven to the same boundaries on the board. | -| `PLAT-TRNG-002` | planned | The raw source clears the floor the shipped whitening needs at the sample spacing the product uses, and the floor is THIS RECORD'S OWN rather than a vendor's. WHY HALF A BIT PER RAW BIT, argued here because the record this replaces cited a datasheet figure that does not exist, and argued in the direction a review had to correct once already: a von Neumann debiaser emits AT MOST one bit per two raw bits, so if h is the raw min-entropy per bit and R the balancer's output-per-input rate, the input min-entropy standing behind one whitened bit is h/R, and R is at most one half. At h = 0.5 that is at least one full bit, AT EVERY RATE THE BALANCER CAN ACHIEVE, and at a slower real rate more. SO THE FLOOR IS SUFFICIENT FOR THAT COUNTING BOUND AND IT IS NOT NECESSARY, which is the opposite of what this field said first: below 0.5 the bound stops holding, the device does not, and a real source at h = 0.25 sits at R near 0.13 and still clears one bit per whitened bit. Von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it; what does is CORRELATION between consecutive samples, which is the thing `sample_count` was raised to reduce and the thing the non-IID track is chosen to see. That is why the verdict is read off the capture at the SHIPPED spacing and not off the bootrom's period-0 one. It is deliberately not derived from §12.12.1's `approximately 7.5 kb/s of entropy`, which is a rate and not a per-bit figure. PASS = the SP800-90B §6.3 non-IID estimator suite, run over at least 1,000,000 consecutive raw samples captured with all three bypass bits SET and SAMPLE_CNT1 AT THE SHIPPED 1000, returns a minimum-over-estimators min-entropy of at least 0.5 bits per raw bit, AND the period-0 control capture and the AUTOCORR_STATISTIC readings at `sample_count` 25 and at 1000 were TAKEN over the two equal-time windows `method` names — trip count and block count both — and are recorded in `actual`. THE SECOND CONJUNCT IS A RECORDING OBLIGATION AND NOT A COMPARISON, and that is deliberate rather than loose: on a healthy unit the autocorrelation check may trip zero times at BOTH settings, so a clause demanding a strict decrease from zero would be a route no passing world walks — which is the defect this whole split exists to remove, reintroduced one conjunct further in. What the two numbers are for is the row's prose and not its verdict: a trip rate that does not fall at 1000 says the raise firmware/src/main.rs:609 made bought something other than what its comment claims, and that is worth recording whichever way it comes out. FAIL = the SHIPPED-SPACING capture's estimate comes back below 0.5 bits per raw bit, on the die this project owns and at the conditions it runs at. That refutes this row's statement in the statement's own terms — the floor is what the row claims and the floor is what was missed — and it deliberately claims no more. What it costs the device stays OPEN: the counting bound the floor secures is sufficient and not necessary, so a real balancer rate below one half spends more raw bits per whitened bit and may still carry a full bit into each, and nobody here has measured that rate. It is not a small open question, because every key, nonce, blinding factor and pinUvAuthToken drawn after boot rests on the seed either way and PLAT-TRNG-001's three continuous checks pass such a source by construction. A FAIL here is a demand for the rate measurement, not a proof the device is broken; overstating it is how the record this replaces came to name a figure nobody published. A FAIL is gate-forced to `refuted` (scripts/platform_gate.py:1337 maps `fail` to `refuted`), so it owes two things beside the number: TRNG_DEBUG_CONTROL and SAMPLE_CNT1 READ BACK after the capture and recorded, and the capture's own hash. A bypass write that did not take yields a WHITENED capture, and a whitened capture estimating low is a broken instrument rather than a weak source, which is the nearest thing to this row's failure that is reachable by accident. INCONCLUSIVE = any run that is not about the source at the spacing the statement names, and each arm is one way to get there: a verdict read off the period-0 CONTROL capture rather than the shipped-spacing one, which is a stream the device never draws and the mismatch this record was rewritten to remove; a capture shorter than the length the estimator suite is specified over; a capture taken through the shipped image, or with any bypass bit clear on read-back, which is whitened output and is the very test the statement says cannot answer this; a capture whose EHR_DATA5 read was not last on every block, since that read is what clears the result registers and a stale block repeats itself into the stream; an estimate taken on the IID track over a capture that fails SP800-90B §5's permutation tests, which is the flattering-number shape; a run at a temperature or a supply outside desk conditions, which is PLAT-TRNG-003's question and not this one's; and a result ASSEMBLED FROM MORE THAN ONE BOARD, because this row is one die: a spread across parts is PLAT-TRNG-003's claim and not this one's, so a second board is a second run of this row and never a wider one. | +| `PLAT-TRNG-002` | planned | The raw source clears the floor the shipped whitening needs at the sample spacing the product uses, and the floor is THIS RECORD'S OWN rather than a vendor's. WHY HALF A BIT PER RAW BIT, argued here because the record this replaces cited a datasheet figure that does not exist, and argued in the direction a review had to correct once already: a von Neumann debiaser emits AT MOST one bit per two raw bits, so if h is the raw min-entropy per bit and R the balancer's output-per-input rate, the input min-entropy standing behind one whitened bit is h/R, and R is at most one half. At h = 0.5 that is at least one full bit, AT EVERY RATE THE BALANCER CAN ACHIEVE, and at a slower real rate more. SO THE FLOOR IS SUFFICIENT FOR THAT COUNTING BOUND AND IT IS NOT NECESSARY, which is the opposite of what this field said first: below 0.5 the bound stops holding, the device does not, and a real source at h = 0.25 sits at R near 0.13 and still clears one bit per whitened bit. Von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it; what does is CORRELATION between consecutive samples, which is the thing `sample_count` was raised to reduce and the thing the non-IID track is chosen to see. That is why the verdict is read off the capture at the SHIPPED spacing and not off the bootrom's period-0 one. It is deliberately not derived from §12.12.1's `approximately 7.5 kb/s of entropy`, which is a rate and not a per-bit figure. PASS = the SP800-90B §6.3 non-IID estimator suite, run over at least 1,000,000 consecutive raw samples captured with all three bypass bits SET and SAMPLE_CNT1 AT THE SHIPPED 1000, returns a minimum-over-estimators min-entropy of at least 0.5 bits per raw bit, AND the period-0 control capture and the AUTOCORR_STATISTIC readings at `sample_count` 25 and at 1000 were TAKEN over the two equal-time windows `method` names — trip count and block count both — and are recorded in `actual`. THE SECOND CONJUNCT IS A RECORDING OBLIGATION AND NOT A COMPARISON, and that is deliberate rather than loose: on a healthy unit the autocorrelation check may trip zero times at BOTH settings, so a clause demanding a strict decrease from zero would be a route no passing world walks — which is the defect this whole split exists to remove, reintroduced one conjunct further in. What the two numbers are for is the row's prose and not its verdict: a trip rate that does not fall at 1000 says the raise firmware/src/main.rs:658 made bought something other than what its comment claims, and that is worth recording whichever way it comes out. FAIL = the SHIPPED-SPACING capture's estimate comes back below 0.5 bits per raw bit, on the die this project owns and at the conditions it runs at. That refutes this row's statement in the statement's own terms — the floor is what the row claims and the floor is what was missed — and it deliberately claims no more. What it costs the device stays OPEN: the counting bound the floor secures is sufficient and not necessary, so a real balancer rate below one half spends more raw bits per whitened bit and may still carry a full bit into each, and nobody here has measured that rate. It is not a small open question, because every key, nonce, blinding factor and pinUvAuthToken drawn after boot rests on the seed either way and PLAT-TRNG-001's three continuous checks pass such a source by construction. A FAIL here is a demand for the rate measurement, not a proof the device is broken; overstating it is how the record this replaces came to name a figure nobody published. A FAIL is gate-forced to `refuted` (scripts/platform_gate.py:1337 maps `fail` to `refuted`), so it owes two things beside the number: TRNG_DEBUG_CONTROL and SAMPLE_CNT1 READ BACK after the capture and recorded, and the capture's own hash. A bypass write that did not take yields a WHITENED capture, and a whitened capture estimating low is a broken instrument rather than a weak source, which is the nearest thing to this row's failure that is reachable by accident. INCONCLUSIVE = any run that is not about the source at the spacing the statement names, and each arm is one way to get there: a verdict read off the period-0 CONTROL capture rather than the shipped-spacing one, which is a stream the device never draws and the mismatch this record was rewritten to remove; a capture shorter than the length the estimator suite is specified over; a capture taken through the shipped image, or with any bypass bit clear on read-back, which is whitened output and is the very test the statement says cannot answer this; a capture whose EHR_DATA5 read was not last on every block, since that read is what clears the result registers and a stale block repeats itself into the stream; an estimate taken on the IID track over a capture that fails SP800-90B §5's permutation tests, which is the flattering-number shape; a run at a temperature or a supply outside desk conditions, which is PLAT-TRNG-003's question and not this one's; and a result ASSEMBLED FROM MORE THAN ONE BOARD, because this row is one die: a spread across parts is PLAT-TRNG-003's claim and not this one's, so a second board is a second run of this row and never a wider one. | | `PLAT-XIP-001` | planned | Core 1 is halted for the whole of every erase and program, and resumes. PASS = no XIP fetch inside the window and a clean resume. FAIL = either a fetch during the array write — a hard fault or worse, silently wrong data — or a pause that is never released, which is a wedge. | -The candidates are DERIVED — 136 of them, from the slice bundles and design pages, the model registry, the suites no runner in this tree can pass, every `unsafe` SITE in first-party Rust, and the semantics a crate-ledger row says its model abstracts. An unclaimed candidate reddens `scripts/platform_gate.py`; so does a claim on a candidate that no longer exists. 31 of them are `unsafe` sites, keyed by their own code rather than by a position, so that a site inserted above another cannot renumber a row onto a different one — and where two sites agree, the key spends MORE of their own words rather than numbering them, because a `~2` re-points on a reorder and that is the same defect one layer down. +The candidates are DERIVED — 139 of them, from the slice bundles and design pages, the model registry, the suites no runner in this tree can pass, every `unsafe` SITE in first-party Rust, and the semantics a crate-ledger row says its model abstracts. An unclaimed candidate reddens `scripts/platform_gate.py`; so does a claim on a candidate that no longer exists. 34 of them are `unsafe` sites, keyed by their own code rather than by a position, so that a site inserted above another cannot renumber a row onto a different one — and where two sites agree, the key spends MORE of their own words rather than numbering them, because a `~2` re-points on a reorder and that is the same defect one layer down. -Which sites go under which row is not the registry's to choose alone. `docs/unsafe.md` numbers its justifications, each heading names the row it enumerates, and the numbering has to partition the runtime sites with each row's section spanning exactly the sites that row covers. Every row also has to NAME, in its own words, the file of each site it claims. Without those two, `covers` was checked only for existence in both directions: collapsing all 31 site keys onto one discharged row and emptying the eleven others was byte-identical output at exit 0. +Which sites go under which row is not the registry's to choose alone. `docs/unsafe.md` numbers its justifications, each heading names the row it enumerates, and the numbering has to partition the runtime sites with each row's section spanning exactly the sites that row covers. Every row also has to NAME, in its own words, the file of each site it claims. Without those two, `covers` was checked only for existence in both directions: collapsing all 34 site keys onto one discharged row and emptying the eleven others was byte-identical output at exit 0. ## The registry @@ -46,8 +46,8 @@ Which sites go under which row is not the registry's to choose alone. `docs/unsa | `PLAT-AUVGATE-002` | `model-abstraction` | The model's consequent for this clause is the alwaysUv FLAG and the Rust α's is the `EF_ALWAYS_UV` RECORD's presence, and the two coincide only while the compiled default is off — so every Rust-tier artifact for `SEC-FIDO-006B` is evidence about a record, not about a gate. | **pending** | Either a `cfg(feature = "always-uv")` arm in crates/rsk-fido/src/reset_assurance.rs that makes the α's consequent the EFFECTIVE state, or a stated narrowing that the clause is about the record. Neither exists today: `git grep 'feature = "always-uv"' crates/rsk-fido/src` returns crates/rsk-fido/src/config.rs:315 and crates/rsk-fido/src/conformance/config.rs:77 and nothing in the reset family. The cost is measured — the six `reset_assurance` tests report `6 passed; 0 failed` on BOTH feature arms, against two different test binaries. crates/rsk-fido/src/config.rs:351-359 is the site that makes the coincidence hold on the default build and break on the other. | contributor | `SEC-FIDO-006B` | | `PLAT-BUILD-001` | `build-configuration` | The DEFAULT image is built without `--features always-uv`, so `gate.alwaysUv` is a free state variable a platform toggles rather than the compiled default every reset restores. `firmware-always-uv` is a published package and is the other arm, not a counterexample. | **discharged** | The build itself: firmware/Cargo.toml's default feature set does not name it, and docs/assurance-matrix.md carries the firmware-always-uv column separately. Both arms run — every configuration the shipped image is about pins the constant FALSE, and AlwaysUv.cfg runs the whole invariant set with it TRUE. | contributor | `SEC-FIDO-001`, `SEC-FIDO-004`, `SEC-FIDO-006`, `SEC-FIDO-006B` | | `PLAT-BUILD-002` | `build-configuration` | `ea-conformance-rpid`'s enterprise-attestation allowlist is a conformance fixture and not an authorization gate, so the authorization properties are unchanged in that column. | **pending** | The settling question docs/assurance-matrix.md already carries for that column, answered against the two production sites that read the allowlist. | contributor | `SEC-FIDO-001` | -| `PLAT-BUILD-003` | `build-configuration` | The `display` build implements the one-hold-one-ceremony latch SOMEWHERE ELSE: it compiles `ButtonWait` out entirely and the panel's own release debounce takes over, so the model keeps a defence that build has in a different place rather than one it does not have. | **pending** | The `firmware-display` and `waveshare-touch-lcd` matrix cells, which are `gap` for this property today. `firmware/src/presence.rs:99-101` gates the button backend on `not(feature = "display")` and `:163-166` swaps `Presence` to `crate::display::TouchPresence`; `crates/rsk-display/src/presence.rs:45-48` clears the stale cancel in the panel's OWN loop, not in `ButtonWait::wait`, and `crates/rsk-display/src/lib.rs:131-134` gives the trait a no-op `set_up_pending`. `formal/RSKeySecurityState.tla:473-477` states the direction: NARROWER than the display build. No configuration and no harness in the tree runs the display build's version of this rule. | contributor | `SEC-FIDO-002` | -| `PLAT-BUILD-004` | `build-configuration` | On the four `no-touch` images no presence DECISION is produced at all — the arbiter is still compiled in, but nothing can reach the wait that would consume one — which is why this property's cells there are `out-of-scope` and not `gap`. | **pending** | Reading the cfg arms, and it is decided rather than open. `firmware/src/presence.rs:202-258`: `request` and `request_ceremony` return `Presence::Confirmed` unconditionally under `feature = "no-touch"`, `poll_pressed` returns `false`, and `ButtonPresence::wait` is `cfg(not(feature = "no-touch"))` so `ButtonWait::wait` — the sole caller of `set_up_pending(true)` on a non-display build — is never called. `Arbiter` itself, and the three `Refines`-tagged functions on it, ARE in those images; what is absent is the decision, not the arbitration code. `firmware/Cargo.toml` says "Never ship a no-touch build" and `release-build.yml` refuses to release one. | contributor | `SEC-FIDO-002` | +| `PLAT-BUILD-003` | `build-configuration` | The `display` build implements the one-hold-one-ceremony latch SOMEWHERE ELSE: it compiles `ButtonWait` out entirely and the panel's own release debounce takes over, so the model keeps a defence that build has in a different place rather than one it does not have. | **pending** | The `firmware-display` and `waveshare-touch-lcd` matrix cells, which are `gap` for this property today. `firmware/src/presence.rs:103-105` gates the button backend on `not(feature = "display")` and `:167-170` swaps `Presence` to `crate::display::TouchPresence`; `crates/rsk-display/src/presence.rs:45-48` clears the stale cancel in the panel's OWN loop, not in `ButtonWait::wait`, and `crates/rsk-display/src/lib.rs:131-134` gives the trait a no-op `set_up_pending`. `formal/RSKeySecurityState.tla:473-477` states the direction: NARROWER than the display build. No configuration and no harness in the tree runs the display build's version of this rule. | contributor | `SEC-FIDO-002` | +| `PLAT-BUILD-004` | `build-configuration` | On the four `no-touch` images no presence DECISION is produced at all — the arbiter is still compiled in, but nothing can reach the wait that would consume one — which is why this property's cells there are `out-of-scope` and not `gap`. | **pending** | Reading the cfg arms, and it is decided rather than open. `firmware/src/presence.rs:209-265`: `request` and `request_ceremony` return `Presence::Confirmed` unconditionally under `feature = "no-touch"`, `poll_pressed` returns `false`, and `ButtonPresence::wait` is `cfg(not(feature = "no-touch"))` so `ButtonWait::wait` — the sole caller of `set_up_pending(true)` on a non-display build — is never called. `Arbiter` itself, and the three `Refines`-tagged functions on it, ARE in those images; what is absent is the decision, not the arbitration code. `firmware/Cargo.toml` says "Never ship a no-touch build" and `release-build.yml` refuses to release one. | contributor | `SEC-FIDO-002` | | `PLAT-BUILD-005` | `build-configuration` | Every shipped image is built with `CONSTANT_MEMORY_ACCESS_PATTERN 0`, and that is a DEFERRAL with a price rather than a default nobody read. The vendored header defaults it to 0 and recommends that value for Cortex-M (`crates/rsk-rsa/csrc/bignum_config.h`), and `crates/rsk-rsa/build.rs` passes no `-D` for it, so there is no build knob: turning it on is a source edit. Read out of the source, the other arm does two things. It replaces `table_entry = temp + four_bits * modulus_length_bytes` with a call to `bignum_table_select`, which scans ALL sixteen entries branchlessly on `usub8`/`sel` and copies the chosen one into scratch; and it prepends an in-place cycle-following transpose that re-lays the table into 8-word blocks for that scan. Neither has ever been built here, and not merely never exercised: `bignum_table_select` sits inside `#if CONSTANT_MEMORY_ACCESS_PATTERN` in `crates/rsk-rsa/csrc/bignum_asm.S`, so at 0 it is not assembled at all, and the transpose is under the same guard in `crates/rsk-rsa/csrc/bignum_high_level.c`. | **accepted-risk** | A build and a board, in that order, and neither has been taken — every number below is COUNTED FROM THE SOURCE and none of it is a measurement. (1) Build `firmware` with the flag defined and confirm `bignum_table_select` is in the image. That is minutes, and it settles the size cost, which is not only flash: both halves are RAM-resident by section — the C carries `BIGNUM_RAMFUNC`, which is `section(".data.bignum_hl")`, and the asm's whole translation unit is `.section .data.bignum_asm` — so the flag spends SRAM on a part whose stack ceiling is already the binding one. (2) A board, because nothing else can run it: `crates/rsk-rsa/build.rs` compiles the C and the asm only for `target_os = "none"`, so no host test and no `tools/emu` run reaches this arm at all, and the transpose's correctness is exercised by an on-card RSA sign at each of the three widths and by nothing else. The Bellcore check in `crt::private_op` turns a wrong transpose into a refused signature rather than a bad one, so a passing on-card sign IS the proof — and a failing one is a key that stops working, which is why this may not be turned on blind. (3) Time one PIV GENERAL AUTHENTICATE or one OpenPGP PSO:CDS on each arm for the real cost. Counted from the instruction mix, `bignum_table_select` reads 16 x `half` bytes per window where the shipped arm reads `half`, against a window that already spends four squarings and one multiply, so the overhead is of the order of a tenth of the private operation and is LARGER at RSA-2048 than at RSA-4096 — the multiply cost grows quadratically in the width and the table scan only linearly. WHAT TAKES THE DEFERRAL OFF, since an `accepted-risk` row may carry no `revalidated_by`: `PLAT-CRYPTO-002` being re-opened by any of its triggers, or a board becoming available for the three widths — whichever comes first. The flag is the fix; this row exists so that not taking it is a decision with a price rather than a header default. | contributor | — | | `PLAT-CRED-001` | `model-abstraction` | `r \in store.rpent` is read as `the EF_RP record for r is reachable by the management surface`, and in the shipped tree it means only that the record exists: both browse surfaces additionally require the read to succeed, the count byte to be positive and the rpId to unseal. | **pending** | Either a second model variable separating `the record exists` from `the record lists`, or a test at each surface that drives a refused `EF_RP` read and a record whose domain will not unseal and asserts what the owner is shown. The six arms are `crates/rsk-fido/src/credmgmt.rs:364, :368, :402` and `crates/rsk-fido/src/passkeys.rs:113, :117, :122`, and the two surfaces do not agree on the third: `enumerate_rps` answers `CtapError::Other` for the whole walk, so one unsealable record hides every other relying party from the host, while `for_each_rp` skips that one record and lists the rest. | contributor | `SEC-FIDO-005` | | `PLAT-CRED-002` | `model-abstraction` | No action of `RSKeySecurityState` can fail a flash write: the model's only tears are `PowerCut` and `ResetAborts` between two writes that succeeded, so an `Fs::put` returning `Err` — and a best-effort rollback that itself fails — is outside every configuration the FIDO properties are checked on. | **pending** | Either a failing-write action in `formal/RSKeySecurityState.tla` (which needs the rollback modelled with it, and the rollbacks are best-effort BY CONSTRUCTION — each is itself a flash write), or a bounded proof over `rsk-fs`'s `Fs` with a symbolic write/delete outcome per step. CHANGELOG.md [0.4.11] `Still open, same class, and named here` names five open sites of exactly this class; all five strand an EF_RP entry over a missing credential, which is the direction `store.cred \subseteq store.rpent` does not constrain, so the cost falls on the opposite direction — asserted by `SEC-STORE-006 NoSilentOrphan`, on a different model. | contributor | `SEC-FIDO-005` | @@ -82,7 +82,7 @@ Which sites go under which row is not the registry's to choose alone. `docs/unsa | `PLAT-PINGATE-001` | `tool-fidelity` | `SEC-FIDO-006A`'s only bounded evidence carries a non-vacuity guard that does not guard the conjunct it omits: `reset_keeps_the_pin_gate`'s `kani::cover!` names three of the clause's four antecedent conjuncts and puts the clause's CONSEQUENT where `snapshot.credential` belongs, so a witness it accepts may be a state at which the clause holds vacuously. | **pending** | One conjunct in `crates/rsk-fido/src/reset_refinement_kani.rs:133-138`, and it was priced by measurement rather than argued. Driven on a scratch copy of the workspace with cargo-kani 0.67.0: the shipped condition conjoined with `!reset.snapshot.credential` is SATISFIED (1.08 s), which is the defect; conjoined with `reset.snapshot.credential` instead it is SATISFIED at 0.98 s against a 0.99 s baseline, which is the repair at no cost. The guard is not inert — under a real vacuity mutation, both credential draws forced false, the shipped condition goes UNSATISFIABLE (0.94 s) — so what it misses is the route through the snapshot draw alone. `crates/rsk-fido/src/reset_assurance.rs:144-151` is why the two routes differ: `begin` is the only writer of `snapshot.credential` and takes it from `persistent.credential`. The sibling harnesses at :142 and :162 have the SAME shape and were not driven. | contributor | `SEC-FIDO-006A` | | `PLAT-PINGATE-002` | `model-abstraction` | `SEC-FIDO-006A`'s isolated RED is a localisation and not a demonstration of unique reach: its falsifying state also falsifies `NoAccessibleSecretWithoutGate`, and the two properties share one model switch and one production function. | **pending** | A configuration or a witness exhibiting a state where this clause falls and `NoAccessibleSecretWithoutGate` holds, and the recorded tier contains none. Measured: `SoloClause_ResetKeepsThePinGate.cfg`'s State 16 has store.cred = {r1}, SeedReachable, pin.everSet TRUE and pin.set FALSE, so `formal/RSKeySecurityState.tla:1796` is false there too; `assurance_gate.co_refuted` credits `BugResetGatesFirst` to both names; and the two `Refines` tags sit at `crates/rsk-fido/src/reset.rs:227` and `:245`, on `is_fido_gate_fid` and the `is_fido_gate_record` it wraps. The converse IS witnessed — `Solo_NoAccessibleSecretWithoutGate.cfg` falls on the stranded-grant conjunct at `formal/RSKeySecurityState.tla:1807` where snap.surv is empty and this clause is vacuous — so the pair is asymmetric rather than redundant. What would settle it is a switch that breaks the snapshot comparison without touching the steady-state ghost `everSet`, and the model has none. | contributor | `SEC-FIDO-006A`, `SEC-FIDO-004` | | `PLAT-PRES-001` | `model-abstraction` | `Owners` — the four `SCOPE_*` bytes plus `Panel`, the model-only split of `SCOPE_NONE` — is the whole domain a presence decision can be produced for or applied to, so a wait owner outside those five does not exist. | **pending** | Reading the byte and its writers. `crates/rsk-device/src/presence.rs:26-32` defines exactly four `SCOPE_*` constants and `firmware/src/worker.rs:434, :528, :663, :665` are every `set_wait_scope` call site in the tree; `formal/RSKeySecurityState.tla:127-144` maps them and splits `SCOPE_NONE` into `NoOwner` and `Panel` because `request_cancel` refuses in both while only one can be ENDED by a touch. What would falsify it is a fifth interface that opens a wait: the byte is a `u8`, nothing bounds it, and a new transport arrives as an owner no configuration and no harness checks. | contributor | `SEC-FIDO-002` | -| `PLAT-PRES-002` | `model-abstraction` | No API in the tree can originate a cancel from `SCOPE_CCID` or from an on-panel flow, so a CCID or panel ceremony is uncancellable by ABSENCE of a producer rather than by a guard anything checks. | **pending** | A whole-tree measurement, and it is a grep rather than a proof: `git grep -n 'cancel_requested.store(true'` is exactly two sites, `crates/rsk-device/src/presence.rs:120` (`request_cancel`, gated on `SCOPE_FIDO`) and `:131` (`cancel_otp_wait`, gated on `SCOPE_OTP`), and the only other writer is `set_cancel_requested`, whose six production call sites (`crates/rsk-device/src/presence.rs:196, :233` and `crates/rsk-display/src/pin.rs:317, :441` + `crates/rsk-display/src/presence.rs:47, :58`) all pass a literal `false`, the two `cfg(feature = "display")` forwarders at `firmware/src/presence.rs:269` and `firmware/src/display.rs:198` reaching only those; the one site that passes `true` is `crates/rsk-device/src/presence_kani.rs:158`, a harness. The Kani cancel harness rests on this: `board.attempted[SCOPE_CCID]` stays false because the harness raises only the two cancels that exist, so 'a CCID wait cannot be cancelled at all' is the harness's construction and not its conclusion. A third writer added anywhere in the tree falsifies it with every harness still green. | contributor | `SEC-FIDO-002` | +| `PLAT-PRES-002` | `model-abstraction` | No API in the tree can originate a cancel from `SCOPE_CCID` or from an on-panel flow, so a CCID or panel ceremony is uncancellable by ABSENCE of a producer rather than by a guard anything checks. | **pending** | A whole-tree measurement, and it is a grep rather than a proof: `git grep -n 'cancel_requested.store(true'` is exactly two sites, `crates/rsk-device/src/presence.rs:120` (`request_cancel`, gated on `SCOPE_FIDO`) and `:131` (`cancel_otp_wait`, gated on `SCOPE_OTP`), and the only other writer is `set_cancel_requested`, whose six production call sites (`crates/rsk-device/src/presence.rs:196, :233` and `crates/rsk-display/src/pin.rs:317, :441` + `crates/rsk-display/src/presence.rs:47, :58`) all pass a literal `false`, the two `cfg(feature = "display")` forwarders at `firmware/src/presence.rs:276` and `firmware/src/display.rs:198` reaching only those; the one site that passes `true` is `crates/rsk-device/src/presence_kani.rs:158`, a harness. The Kani cancel harness rests on this: `board.attempted[SCOPE_CCID]` stays false because the harness raises only the two cancels that exist, so 'a CCID wait cannot be cancelled at all' is the harness's construction and not its conclusion. A third writer added anywhere in the tree falsifies it with every harness still green. | contributor | `SEC-FIDO-002` | | `PLAT-PRES-003` | `model-abstraction` | `NoCrossTransportTouchConsumption`'s structural conjunct — `(pres.granted = "cancel") => (pres.cancelBy = pres.scope)` — is INERT on the current module: no reachable state falsifies it that `TouchCancel`'s ghost write has not already marked in the same step. | **pending** | A configuration that checks the conjunct alone, which the tree already has a family for (`formal/SoloClause_*.cfg`, three of them today) — or the module changing. ARGUED FROM THE MODULE TEXT AND NOT RUN: `TouchCancel` (`formal/RSKeySecurityState.tla:444-455`) sets `granted = "cancel"` and writes `viol` in the same step; every other writer of `pres` either requires `WaitOpen` (`granted = "none"`), or is `ClosedWait`/`OpenWaitFor`/`VolatileCleared`, all of which set `granted` back to `"none"`; and `viol` is only ever grown. So the conjunct's arm can be reached by nothing the ghost has not already caught. It is a backstop for a producer that does not exist yet, which is the same shape as an assumption constant nothing branches on. | contributor | `SEC-FIDO-002` | | `PLAT-RESET-001` | `reset` | A real RP2350 power-on reset clears `WATCHDOG.scratch2`, so the PIN soft-lock the previous cycle recorded does not ride through it. | **pending** | A board measurement: write a known value to WATCHDOG.scratch2, cut power at the supply — not a warm reset, because firmware/src/pin_lock.rs writes the word on every CBOR dispatch, so the read must come from the first boot after — and read it back. tests/54_sram_residue.py is the shape; the register is not SRAM, so it needs its own probe. | maintainer | `SEC-BOOT-002` | | `PLAT-ROM-001` | `boot-rom` | M7-Q2: the boot ROM's BOOTSEL return path leaves `WATCHDOG.scratch2` as the firmware left it, so a drop to the bootloader is not a way to shed the soft lock. | **pending** | A board measurement on the same rig as PLAT-RESET-001, with the vendor applet's reboot-to-BOOTSEL in place of the supply cut, and the read taken on the boot after the bootloader is left. No datasheet clause states it either way; a vendor erratum would settle it as well. | maintainer | `SEC-BOOT-002` | @@ -96,13 +96,13 @@ Which sites go under which row is not the registry's to choose alone. `docs/unsa | `PLAT-SOURCE-002` | `model-abstraction` | Three of the citations that carry this property's model-to-code bridge name code the surrounding prose is not about, and `scripts/citation_gate.py` is GREEN over all three: `formal/RSKeySecurityState.tla:65` cites `state.rs:584-599` for `stopUsingPinUvAuthToken` and that span is `retire_sequences_except`; `formal/README.md:99` cites `seed.rs:323-324` for `clear_ppuat` and that span is the last two lines of the function ABOVE it; and the bare `:300-304`, written three times at `formal/RSKeySecurityState.tla:89`, `formal/RSKeySecurityState.tla:829` and `formal/RSKeySecurityState.tla:1772`, is four lines short of the `clear_ppuat` call it is cited for AND is matched by no rule of the gate, because the module writes it without the backticks `CITE`'s continuation alternative requires — measured by running `citation_gate.citations` over the module, which returns none of the three. | **pending** | Re-point the three and re-lock. The measurements behind them, so the fix is checkable: `state.rs:584-599` WAS exactly `stop_using_token` at commit `250be31` and the function is at `:547-562` today; `:300-304` WAS the §6.5.5.6-step-15 comment through `clear_ppuat(ctx.fs)` at commit `c9bdc5b` and that block is at `:305-309` today, which is the span `formal/README.md` already uses for the same claim; and `seed.rs:323-324` was `r.map(\|()\| tok)` and `}` at `5e3abef`, the commit that INTRODUCED it, so it has never named `clear_ppuat`. | contributor | `SEC-FIDO-003` | | `PLAT-STORE-001` | `model-abstraction` | The tear guarantee PLAT-FLASH-001 makes about the silicon holds one layer up too, in the KV library: a torn append or remove leaves the old record or a detectably bad one, never a plausible wrong one. This is a property of the VENDORED FORK, not of `sequential-storage` 8.0.0. | **pending** | Two things, and neither is in the tree. First a recorded verdict for fuzz/fuzz_targets/kv_durability.rs, the target that FOUND the upstream defect: third_party/sequential-storage.patch item 3 records that upstream's `remove_item_inner` erased pages from `find_first_page(PartialOpen).unwrap_or_default()`, which falls back to page 0 in the normal steady state of a closed frontier page plus an open buffer page. That inverted the oldest-first erase order, so a power cut mid-remove erased the NEWEST copy first and left an OLDER copy live, which `fetch_item` then returned — exactly the plausible wrong one PLAT-FLASH-001 rules out, false in the library until this fork fixed it, and no board run of tests/29_reset_power_cut.py could have discharged it because the defect sits ABOVE the medium. Second a rule that the fork is what actually builds: [patch.crates-io] is wired in three manifests (Cargo.toml:177, fuzz/Cargo.toml:443, tools/emu/Cargo.toml:84) and nothing holds the three together, so a workspace that missed one would link upstream 8.0.0 with the defect back and every host test still green. | contributor | `SEC-STORE-001`, `SEC-STORE-002`, `SEC-STORE-003`, `SEC-STORE-004`, `SEC-STORE-005`, `SEC-STORE-006` | | `PLAT-STORE-002` | `model-abstraction` | The backend's enumeration-completeness flag is honest: `for_each_key` answering *complete* means every committed key was yielded, which is what lets `Fs::scan` decide the whole FID space and read a cold absence O(1) instead of probing for it. | **pending** | A test driving a SELECTIVE, single-page read fault at the `Storage` seam and asserting `for_each_key` answers false. The UNCONDITIONAL case is already covered and an earlier draft of this row said it was not: crates/rsk-store/src/tests.rs:395-404 `a_faulted_walk_reports_itself_incomplete` mounts a store, writes a live key, sets `fail_reads` and asserts the walk answers false; it predates this row and scripts/check.sh:514 runs it on every gate. What that test cannot reach is the shape the defect had. The mock says so itself at crates/rsk-store/src/tests.rs:59-62: `fail_reads` fails every read `unconditionally and for every address — which no torn write produces`, so it stands for a chip or bus fault, and a walk whose FIRST read fails is refused before it can report a truncated set as complete. Item 2 of third_party/sequential-storage.patch is the other shape: upstream's page-advance loop swallowed a page-state Err via `_ => continue`, so ONE unreadable page mid-ring was skipped and the walk still reached the None terminator and reported complete. Only a fault that answers for one address and not the rest distinguishes the fork from upstream, and no test in this tree can pose one. The claim itself is stated only in two COMMENTS, crates/rsk-fs/src/fs.rs:100-106 and crates/rsk-fs/src/fs.rs:295-302, each arguing it from the backend's internals rather than from anything that runs. The fork tells Corrupted (a fully-migrated source, safe to skip) from a genuine read fault and aborts on the second — so the guarantee is the FORK's, which is what PLAT-STORE-001 carries. crates/rsk-store/src/lib.rs:179-181 ANDs the two partitions' walks, so the flag is only ever as honest as the weaker ring. | contributor | `SEC-STORE-002` | -| `PLAT-STORE-003` | `model-abstraction` | GIVEN a medium that tears the way PLAT-FLASH-001 asserts, a reboot recovers the committed store: after any number of power cuts every key whose write or remove returned cleanly reads back exactly, and only the operation the cut interrupted is ambiguous — its old value or its new one, never a third older one. The `GIVEN` is the whole narrowing and it is what makes this row dischargeable by a mock; the medium half is PLAT-FLASH-001's and is not restated here. firmware/src/main.rs:555 runs `Fs::scan` once at boot, and every later absence answer is that one walk's. | **pending** | A run record naming a verdict, which is the half that does not exist. The CONTRACT is asserted already: fuzz/fuzz_targets/kv_durability.rs states it in as many words in its header and drives one `MapStorage` partition directly, and fuzz/fuzz_targets/power_cut.rs torments the whole rsk-fs stack over the same power-cuttable mock. Both are on the weekly roster — scripts/fuzz-all.sh:41 takes it from `cargo fuzz list` rather than a hand-written table, so neither can silently fall off it. What nothing does is READ those verdicts: `kv_durability` appears in no workflow, no docs page, no script and no assurance file, so its result is a green shard nothing cites. Why this row is `model-abstraction` and not `flash`, decided by measurement rather than by taxonomy: the class is the whole difference between a discharge that must name silicon and one that need not. Marked `discharged` with `evidence = ["fuzz/fuzz_targets/kv_durability.rs"]` this row is exit 0 under `model-abstraction` and exit 1 under `flash` — `PLAT-STORE-003: a flash discharge records no 'board_revision'`. As the statement stood before the `GIVEN`, `flash` was the honest class and the row was mis-classed. Narrowing the statement is the better repair: classing it `flash` would oblige a stepping for a defect that sits ABOVE the medium, which is exactly the mistake PLAT-STORE-001 exists to undo — a software question parked behind hardware that cannot answer it — and this row's `discharge_owner` is `contributor`, who under AGENTS.md may not produce a board revision at all. What the narrowing does NOT buy, in the reading this row carried until the hole was closed: `model-abstraction` accepted any path that existed, so `evidence = ["README.md"]` on this row was also exit 0, measured. That half is now `check_evidence`'s `PROSE_PAGE` rule and it belongs to every row of the class rather than to this one: a `.md` that no generator both CLAIMS and MARKS is refused as evidence anywhere in the registry, whatever its case, and so is a directory and any spelling git's listing does not have — `docs`, `README.MD`, and a generator-claimed page with its own header line deleted, were each exit 0 after the rule's first version, measured. So is a page rendered FROM this registry, which that version admitted through its own carve-out: `evidence = ["docs/platform-assumptions.md"]` on this row was exit 0 while that page prints this row's `discharge` verbatim. What the rule still does not buy is relevance: an artifact of legal shape and no bearing on the claim, `deny.toml`, is exit 0 on this row today, measured, and only a reader stops that. Nor does it stop a `*_gate.py` DECLARING a page it does not write, because nothing here runs a generator — that move now costs a second edit, since the page must carry the marker too. | contributor | `SEC-STORE-001`, `SEC-STORE-002` | +| `PLAT-STORE-003` | `model-abstraction` | GIVEN a medium that tears the way PLAT-FLASH-001 asserts, a reboot recovers the committed store: after any number of power cuts every key whose write or remove returned cleanly reads back exactly, and only the operation the cut interrupted is ambiguous — its old value or its new one, never a third older one. The `GIVEN` is the whole narrowing and it is what makes this row dischargeable by a mock; the medium half is PLAT-FLASH-001's and is not restated here. firmware/src/main.rs:604 runs `Fs::scan` once at boot, and every later absence answer is that one walk's. | **pending** | A run record naming a verdict, which is the half that does not exist. The CONTRACT is asserted already: fuzz/fuzz_targets/kv_durability.rs states it in as many words in its header and drives one `MapStorage` partition directly, and fuzz/fuzz_targets/power_cut.rs torments the whole rsk-fs stack over the same power-cuttable mock. Both are on the weekly roster — scripts/fuzz-all.sh:41 takes it from `cargo fuzz list` rather than a hand-written table, so neither can silently fall off it. What nothing does is READ those verdicts: `kv_durability` appears in no workflow, no docs page, no script and no assurance file, so its result is a green shard nothing cites. Why this row is `model-abstraction` and not `flash`, decided by measurement rather than by taxonomy: the class is the whole difference between a discharge that must name silicon and one that need not. Marked `discharged` with `evidence = ["fuzz/fuzz_targets/kv_durability.rs"]` this row is exit 0 under `model-abstraction` and exit 1 under `flash` — `PLAT-STORE-003: a flash discharge records no 'board_revision'`. As the statement stood before the `GIVEN`, `flash` was the honest class and the row was mis-classed. Narrowing the statement is the better repair: classing it `flash` would oblige a stepping for a defect that sits ABOVE the medium, which is exactly the mistake PLAT-STORE-001 exists to undo — a software question parked behind hardware that cannot answer it — and this row's `discharge_owner` is `contributor`, who under AGENTS.md may not produce a board revision at all. What the narrowing does NOT buy, in the reading this row carried until the hole was closed: `model-abstraction` accepted any path that existed, so `evidence = ["README.md"]` on this row was also exit 0, measured. That half is now `check_evidence`'s `PROSE_PAGE` rule and it belongs to every row of the class rather than to this one: a `.md` that no generator both CLAIMS and MARKS is refused as evidence anywhere in the registry, whatever its case, and so is a directory and any spelling git's listing does not have — `docs`, `README.MD`, and a generator-claimed page with its own header line deleted, were each exit 0 after the rule's first version, measured. So is a page rendered FROM this registry, which that version admitted through its own carve-out: `evidence = ["docs/platform-assumptions.md"]` on this row was exit 0 while that page prints this row's `discharge` verbatim. What the rule still does not buy is relevance: an artifact of legal shape and no bearing on the claim, `deny.toml`, is exit 0 on this row today, measured, and only a reader stops that. Nor does it stop a `*_gate.py` DECLARING a page it does not write, because nothing here runs a generator — that move now costs a second edit, since the page must carry the marker too. | contributor | `SEC-STORE-001`, `SEC-STORE-002` | | `PLAT-STORE-004` | `model-abstraction` | `is_counter_fid` is a correct routing table over every FID the firmware persists: a record is read back from the partition it was written to, and the counter/main split never leaves a live FID on the other side. | **pending** | A derivation reading the FID constants out of the applet crates and holding them against the table, which scripts/partition_routing_gate.py is: it takes the constant NAMES out of the doc comment over `is_counter_fid`, resolves each to a `const … : u16` in a crate that is not rsk-store, and holds all four copies to the values that come back. What it does NOT settle is this row's statement, and that is why the status has not moved: the ROSTER is that doc comment, so a hot record declared at its home and named nowhere over the table derives nothing and no copy is measured against it — `over every FID the firmware persists` stays a judgement `rsk-store` makes and a reader checks. Before that row existed: crates/rsk-store/src/lib.rs:88 is a hand-written `matches!` over four bare literals (0xC000, 0xC001, 0x0093, 0xCC01) whose named homes are in other crates: `EF_COUNTER` at crates/rsk-fido/src/consts.rs:333 and `EF_CRED_CTR` at crates/rsk-fido/src/consts.rs:340. Table and constants therefore drift without a compile error, and the set HAS moved once — `EF_CRED_CTR` joined it at 0x0821 after 0x081D had been writing that FID to main. crates/rsk-store/src/lib.rs:145-152 records what the resulting state costs: a copy in the wrong partition is invisible to `read` yet yielded by every `for_each_key`, so authenticatorReset's sweep loops on it until its progress bound gives up. FOUR hand-written copies of the set, not two, and an earlier draft of this row missed the one that matters. `git grep -l is_counter_fid` reaches nine files — this registry and its generated page, assurance/crates.toml, CHANGELOG.md, docs/assurance-vector.md, formal/README.md, the two rsk-store sources, and fuzz/fuzz_targets/power_cut.rs. That last one is the fourth copy: fuzz/fuzz_targets/power_cut.rs:76-78 spells the same four literals into a `FIDS` array, and its own comment at :67-75 records that the set has ALREADY drifted there twice — the mirror it replaced listed three of the four and missed `EF_CRED_CTR` (0xC001, rewritten on every getAssertion), and the selector was `& 7` over nine entries, so `0xCC01` could never be written by any input while the sweep asserted it absent on every one. A whole partition routing was in the roster and out of the fuzzer's reach. So this row's subject is not hypothetical drift; it is drift that has happened twice in the copy the row was written without. What holds the table today is crates/rsk-store/src/tests.rs:244-264, which lists the same four numbers a third time and would move WITH a wrong edit — and which names the `power_cut` miss at :250-251, so the two copies that drifted are the two nothing derives. | contributor | `SEC-STORE-002` | | `PLAT-THREAT-001` | `threat-model` | A CTAPHID channel id is a routing label the sender writes, so channel ownership is a scoping rule and never an authentication one. | **pending** | A clause in docs/threat-model.md, which assurance/threat_clauses.toml would then derive. crates/rsk-fido/src/state.rs states it in prose beside the walk owner today, which is the wrong place for a threat-model premise. | contributor | `SEC-FIDO-001`, `SEC-FIDO-002`, `SEC-TRANS-001` | | `PLAT-THREAT-002` | `threat-model` | A PIN-derived record stays rooted in the public chip serial until its own reference is PRESENTED after the burn. The boot passes move every record sealed under the key base alone; re-keying a PIN-derived one needs the secret, so it happens in `check_pin`'s fallback (`crates/rsk-openpgp/src/pin.rs:277`). PW1 is presented by ordinary use; PW3 gates only the admin surface and may not be presented again after provisioning, and an OpenPGP resetting code may never be. Until then a flash dump plus the public serial opens the DEK copy behind that reference — every OpenPGP private key and the AES key with them — and a PW3 still on its published default needs no search at all. PIV's PUK is the same shape one crate over (`crates/rsk-piv/src/lib.rs:1328`), without a DEK behind it. | **pending** | Three routes, and the smallest one that CLOSES it is the third. (1) The operator: present PW3 once and set the resetting code again after the burn, which is what docs/production.md now says — a cure, not a guarantee. (2) An arm byte in the verifier record, which is cheaper than it sounds: `PIN_FORMAT_V1` is written at `crates/rsk-openpgp/src/pin.rs:775` and read by nothing, so the arm can ride that byte with no length change, and an older build ignores it — but a FOURTH byte would fail `check_pin`'s length test while `verifier_unusable` still called the record usable, which refuses TERMINATE DF's escape hatch. It tells the card what it cannot otherwise know: a verifier is an opaque hash, so a code set before the burn and one set after are indistinguishable. The burn BOOT is not the unknown — `crates/rsk-fido/src/seed.rs:535` already computes it — what stops a deletion there is that it destroys a recovery credential the owner configured. (3) Seal the DEK copies under an outer device-rooted layer that a boot pass can move, as `crates/rsk-oath/src/seal.rs:42` and `crates/rsk-otp/src/seal.rs:55` already do for their key-base-rooted blobs. The verifier stays brute-forceable and becomes worth nothing without the card; a downgrade fails CLOSED, because every reader gates on the format byte (`crates/rsk-openpgp/src/pin.rs:340`). That is a persistent-format change and the maintainer's call. | contributor | `SEC-BOOT-001` | -| `PLAT-TIMER-001` | `build-configuration` | The timebase every timeout is measured against is a 1 MHz 64-bit tick count that no run can wrap: `embassy_time::Instant` is a `u64` of ticks, `embassy-rp`'s driver reads RP2350 `TIMER0`'s NATIVE 64-bit counter rather than extending a 32-bit one in software, and the image pins the rate at 1 MHz — so the roll-over the timeout arithmetic would have to survive is 2^64 microseconds, about 584,542 years. That the counter also ADVANCES is a DIFFERENT claim and PLAT-TIMER-003's, not this row's. | **discharged** | READ on 2026-09-05, and it is the first half of the route this row used to carry; the second half is PLAT-TIMER-002 because no run walks the one written here. FOUR READINGS AND ONE DIVISION. (1) `embassy_time::Instant` is `ticks: u64` and nothing narrows it. (2) The driver reads the silicon's own 64-bit value: `now()` takes `timerawh`, then `timerawl`, then `timerawh` again and retries while the two high reads disagree, concatenating `(hi as u64) shifted 32 \| lo`. That is a TORN-READ guard over a hardware 64-bit counter, not a software roll-over extension — the halves are joined, never accumulated, and the SVD the PAC is generated from marks TIMERAWH and TIMERAWL read-only. The revision is pinned in Cargo.lock: embassy-rp 0.10.0, embassy-time 0.5.1 and embassy-time-driver 0.2.2 all at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (3) The rate is 1 MHz twice over. firmware/Cargo.toml:13 enables embassy-rp's `time-driver`, whose feature body is `embassy-time-driver?/tick-hz-1_000_000`; no manifest in this tree names a `tick-hz-*` feature of its own — `git grep tick-hz -- '*Cargo.toml'` exits 1 — so the driver crate's own no-feature fallback, `TICK_HZ = 1_000_000`, would give the same answer. firmware/src/presence.rs:137 asserts it at COMPILE time. TWO THINGS ABOUT THAT READING ARE NOT WHAT THEY LOOK LIKE, and both are here because nothing else in the tree says them. The grep is SCOPED because the unscoped one falsifies itself: this sentence used to say that `git grep tick-hz` over the tree answers nothing else, which was true of the tree it was written against and false of the tree it now sits in — three hits, every one of them this row's own prose and the page generated from it. A claim about what the tree contains cannot be spelled so that writing it changes the answer. And the citation to the manifest is INVISIBLE to the citation gate, so that gate's green says nothing about it: scripts/citation_gate.py:556 sets `EXTS` to `rs\|sh\|txt\|py`, and no `.toml` line reference anywhere in this tree is therefore pinned or re-checked. A pre-existing class with other members — assurance/properties.toml:101, assurance/threat_clauses.toml:216-218 and formal/runs.toml:66 among them — which this row names rather than closes; read that line by CONTENT. (4) The boot configuration is the default one: firmware/src/main.rs:524 takes `embassy_rp::config::Config::default()` and edits only the XOSC startup delay, so the clocks are `ClockConfig::crystal(12_000_000)` with `ref_clk` from XOSC at div 1, hence clk_ref 12 MHz, and embassy's RP2350 arm writes `TICKS.timer0_cycles = clk_ref / 1_000_000`, which is 12. THE DIVISION: 2^64 ticks at one per microsecond is 18446744073709.55 s, 213503982.33 days, 584,542.05 years of 365.25 days. The route this row used to demand is refused by arithmetic and not by cost. THREE THINGS THIS DOES NOT SETTLE, and one of them is owned by nothing. The compile-time pin is not on every arm: firmware/src/presence.rs:137 sits under `cfg(all(not(feature = "no-touch"), not(feature = "display")))`, so the display build and the no-touch builds rest on the feature alone. That the counter ADVANCES is unread here and is PLAT-TIMER-003's, and this sentence read `unregistered anywhere` until that row was opened on 2026-09-05: it was true, and it sat beside a `supports` over three liveness properties that need progress and not rate, which is the defect the third row exists to close. Nothing in this tree's CODE would still notice a stopped tick — no assert, test or proof covers it — and the honest instrument remains a recording of an alarm-driven cadence, since a counter that stopped would freeze that render permanently; what changed is that PLAT-TIMER-003 asserts on it, over the recording PLAT-TIMER-002's run already produces. And the counter is NOT the read-only part an earlier reading of this row took it for: RP2350 TIMER0 at 0x400b0000 exposes TIMEHW and TIMELW as WRITE-ONLY halves of the same 64-bit value (the SVD's own words are `to change time write to timelw before timehw`), beside PAUSE, LOCKED and SOURCE. So the 64-bit wrap is unreachable by a RUN and reachable by a WRITE; what this tree has is no user of any of them, in the firmware or in embassy-rp. | contributor | — | +| `PLAT-TIMER-001` | `build-configuration` | The timebase every timeout is measured against is a 1 MHz 64-bit tick count that no run can wrap: `embassy_time::Instant` is a `u64` of ticks, `embassy-rp`'s driver reads RP2350 `TIMER0`'s NATIVE 64-bit counter rather than extending a 32-bit one in software, and the image pins the rate at 1 MHz — so the roll-over the timeout arithmetic would have to survive is 2^64 microseconds, about 584,542 years. That the counter also ADVANCES is a DIFFERENT claim and PLAT-TIMER-003's, not this row's. | **discharged** | READ on 2026-09-05, and it is the first half of the route this row used to carry; the second half is PLAT-TIMER-002 because no run walks the one written here. FOUR READINGS AND ONE DIVISION. (1) `embassy_time::Instant` is `ticks: u64` and nothing narrows it. (2) The driver reads the silicon's own 64-bit value: `now()` takes `timerawh`, then `timerawl`, then `timerawh` again and retries while the two high reads disagree, concatenating `(hi as u64) shifted 32 \| lo`. That is a TORN-READ guard over a hardware 64-bit counter, not a software roll-over extension — the halves are joined, never accumulated, and the SVD the PAC is generated from marks TIMERAWH and TIMERAWL read-only. The revision is pinned in Cargo.lock: embassy-rp 0.10.0, embassy-time 0.5.1 and embassy-time-driver 0.2.2 all at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (3) The rate is 1 MHz twice over. firmware/Cargo.toml:13 enables embassy-rp's `time-driver`, whose feature body is `embassy-time-driver?/tick-hz-1_000_000`; no manifest in this tree names a `tick-hz-*` feature of its own — `git grep tick-hz -- '*Cargo.toml'` exits 1 — so the driver crate's own no-feature fallback, `TICK_HZ = 1_000_000`, would give the same answer. firmware/src/presence.rs:141 asserts it at COMPILE time. TWO THINGS ABOUT THAT READING ARE NOT WHAT THEY LOOK LIKE, and both are here because nothing else in the tree says them. The grep is SCOPED because the unscoped one falsifies itself: this sentence used to say that `git grep tick-hz` over the tree answers nothing else, which was true of the tree it was written against and false of the tree it now sits in — three hits, every one of them this row's own prose and the page generated from it. A claim about what the tree contains cannot be spelled so that writing it changes the answer. And the citation to the manifest is INVISIBLE to the citation gate, so that gate's green says nothing about it: scripts/citation_gate.py:556 sets `EXTS` to `rs\|sh\|txt\|py`, and no `.toml` line reference anywhere in this tree is therefore pinned or re-checked. A pre-existing class with other members — assurance/properties.toml:101, assurance/threat_clauses.toml:216-218 and formal/runs.toml:66 among them — which this row names rather than closes; read that line by CONTENT. (4) The boot configuration is the default one: firmware/src/main.rs:573 takes `embassy_rp::config::Config::default()` and edits only the XOSC startup delay, so the clocks are `ClockConfig::crystal(12_000_000)` with `ref_clk` from XOSC at div 1, hence clk_ref 12 MHz, and embassy's RP2350 arm writes `TICKS.timer0_cycles = clk_ref / 1_000_000`, which is 12. THE DIVISION: 2^64 ticks at one per microsecond is 18446744073709.55 s, 213503982.33 days, 584,542.05 years of 365.25 days. The route this row used to demand is refused by arithmetic and not by cost. THREE THINGS THIS DOES NOT SETTLE, and one of them is owned by nothing. The compile-time pin is not on every arm: firmware/src/presence.rs:141 sits under `cfg(all(not(feature = "no-touch"), not(feature = "display")))`, so the display build and the no-touch builds rest on the feature alone. That the counter ADVANCES is unread here and is PLAT-TIMER-003's, and this sentence read `unregistered anywhere` until that row was opened on 2026-09-05: it was true, and it sat beside a `supports` over three liveness properties that need progress and not rate, which is the defect the third row exists to close. Nothing in this tree's CODE would still notice a stopped tick — no assert, test or proof covers it — and the honest instrument remains a recording of an alarm-driven cadence, since a counter that stopped would freeze that render permanently; what changed is that PLAT-TIMER-003 asserts on it, over the recording PLAT-TIMER-002's run already produces. And the counter is NOT the read-only part an earlier reading of this row took it for: RP2350 TIMER0 at 0x400b0000 exposes TIMEHW and TIMELW as WRITE-ONLY halves of the same 64-bit value (the SVD's own words are `to change time write to timelw before timehw`), beside PAUSE, LOCKED and SOURCE. So the 64-bit wrap is unreachable by a RUN and reachable by a WRITE; what this tree has is no user of any of them, in the firmware or in embassy-rp. | contributor | — | | `PLAT-TIMER-002` | `timers` | The 32-bit alarm comparator survives its own wrap. `embassy-rp` arms `TIMER0.ALARM0` with the LOW 32 bits of a deadline — `TIMER.alarm(n).write_value(timestamp as u32)` — so once per 2^32 microseconds, 71 min 35 s, the value armed is numerically BELOW the counter's running low half and the match can only arrive after that half has wrapped. What makes the truncation safe is that it is paired with a full 64-bit re-read: `check_alarm` compares the stored `u64` deadline against `now()` before it wakes anything. This firmware arms a short alarm continuously — 2 ms or 5 ms in the LED render, 16 ms in the worker — so exactly one such truncated arm straddles every crossing, and a bounded run reaches it without arranging anything. | **pending** | A board run long enough to cross the COMPARATOR's wrap, which is the only wrap a bounded run can cross — the 64-bit counter's is 584,542 years out, and that is PLAT-TIMER-001. THE SPECIFICATION IS THE DRIVER'S OWN COMMENT, at the `TIMER.alarm(n).write_value(timestamp as u32)` in `set_alarm`: `Note that we're not checking the high bits at all. This means the irq may fire early if the alarm is more than 72 minutes (2^32 us) in the future. This is OK, since on irq fire it is checked if the alarm time has passed.` — and `check_alarm`'s else arm: `Not elapsed, arm it again. This can happen if it was set more than 2^32 us in the future.` `72 minutes` is that comment's rounding; the register is 2^32 microseconds, 4294.967296 s, 71 min 34.97 s, and the run has to be timed against the exact figure and not the rounded one. THE RUN, in assurance/board/PLAT-TIMER-002.toml: a DEFAULT-image board powered and LEFT ALONE for two full periods, with its LED recorded. Two things about it are the opposite of what the first version of this record asked for, and both were measured rather than argued. Its observable is BOARD-SCOPED: firmware/src/led.rs:618 and firmware/src/led.rs:692 re-arm a 5 ms and a 2 ms alarm for as long as the board has power, needing no host and no finger, and one of them — or firmware/src/worker.rs:307's 16 ms button poll — is the earliest deadline in the queue at every instant, so the arm that straddles each crossing is one of them. And the run is QUIET: a CTAPHID or CCID request arms a fresh short alarm as it lands (crates/rsk-usb/src/ctaphid.rs:775, crates/rsk-usb/src/ccid.rs:408), which re-arms the comparator and repairs a missed match in microseconds, so wire traffic MASKS this defect instead of producing it — and the keepalives the first version wanted flowing are inside those per-request loops, so an idle board makes none of them anyway. ONE PERIOD IS WHAT GUARANTEES A CROSSING, not two: a run of length L from phase p crosses iff L > 2^32 - p, so the worst phase costs one period. The second period is there because the first crossing lands at most one period in, so two leave a full period AFTER it — and the failure below is a stall of up to a whole period, which a run ending at the crossing could not tell from a run that stopped. WHAT THE RUN DOES NOT REACH, said here because a PASS read as covering it would be read wrong: `check_alarm`'s else arm — `Not elapsed, arm it again` — runs only when the IRQ fires while the stored deadline is still in the future, which needs a deadline at least 2^32 microseconds out. The longest alarm anything in this tree schedules is 600 ms, and the only deadline that far out is the `u64::MAX` the queue's `next_expiration` returns when nothing is pending — which this firmware never produces, because the LED render and the worker keep the queue populated. So a PASS here is compatible with that arm being wholly broken; its route is a driver-level test or a firmware that arms a long alarm, and not a board run. WHAT IT DOES NOT SERVE, said here because the row it was split out of claimed it and could not: NOT SEC-FIDO-L01, L02 or L03, and this row carries no `supports` for that reason. They are PLAT-TIMER-003's, which reads THIS row's recording for a different question — whether the counter advances at all — so the recording is shared and the claims are not. A stall of the order of one period that ENDS is this row's FAIL and that row's PASS, since only a running counter brings the armed value round again; a series that stops and never resumes is that row's FAIL and leaves this one INCONCLUSIVE, since a comparator whose counter is dead can be observed neither to miss a match nor to make one; and only a freeze beginning less than a period before the recording ends is inconclusive for both, which is the second reason the run is two periods and not one. The presence window is a busy poll — crates/rsk-device/src/presence.rs:215 compares `board.now_us().wrapping_sub(start)` against the timeout and spaces its polls with `block_for`, which spins on `Instant::now()` and arms no alarm at all — so it is wrap-immune by construction and the comparator is not in it. The pinUvAuthToken and stateful-walk windows are `saturating_sub` over a `u64` millisecond count, checked when a command ARRIVES rather than by a timer, and the longest window the firmware ever measures is crates/rsk-fido/src/consts.rs:392's `PUAT_MAX_USAGE_PERIOD_MS` at 600000 ms — 7.16 times SHORTER than the wrap this row is about. This row tests the comparator, not `Instant`. | maintainer | — | -| `PLAT-TIMER-003` | `timers` | The timebase ADVANCES: RP2350 `TIMER0`'s counter keeps counting for as long as the board has power, so every deadline armed against it is eventually reached and every spin on `Instant::now()` eventually ends. This is the condition the three liveness properties rest on and PLAT-TIMER-001's RATE reading is not: `EveryOpQuiesces`, `EveryWaitReleases` and `EveryWalkCloses` are EVENTUALLY claims, so a tick twelve times slow still releases every wait and closes every walk twelve times later, and only a tick that STOPS falsifies them. Nothing in this tree asserts it — firmware/src/presence.rs:137 pins the RATE at compile time and on one build arm of three, and no assert, test or proof anywhere covers progress. | **pending** | PLAT-TIMER-002'S RECORDING, READ FOR A DIFFERENT QUESTION, and shared deliberately rather than thriftily: the alternative is a second unattended two-hour run producing the same series. THE INSTRUMENT, AND WHY IT WITNESSES PROGRESS — per backend, because the first version of this sentence gave one mechanism to three and only one of them has it. What all three share is the STEP: each advances one step per `Timer` await and per nothing else — firmware/src/led.rs:618 (ws2812, the default), firmware/src/led.rs:727 (pimoroni) and firmware/src/led.rs:692 (the gpio soft-PWM's 2 ms period). WHAT DIFFERS IS WHAT THE STEP PAINTS. Only ws2812's frame is a function of the loop's own counter: firmware/src/led.rs:589 declares it, firmware/src/led.rs:596 advances it once per await, firmware/src/led.rs:607 is the only thing that reads it, and firmware/src/led.rs:536's `effect_sparkle` re-rolls it every eight steps. GPIO AND PIMORONI TAKE THEIRS FROM THE CLOCK: both call `Blinker::tick` (firmware/src/led.rs:353) — at firmware/src/led.rs:695 and firmware/src/led.rs:719 — which compares `Instant::now()` against a stored phase end at firmware/src/led.rs:361 and flips the phase when it is reached. THE CONCLUSION SURVIVES THE CORRECTION, and twice over rather than by luck: a stopped counter never returns the await, so no backend takes a step at all; and a clock-driven frame that somehow did step would read a frozen `Instant::now()` and paint the same colour anyway. So on all three a frame that CHANGES is a witness that the await returned, and an await returns only when the counter reached the value `set_alarm` armed; a stopped counter holds the last frame for ever and nothing on the device repairs it. THE CHAIN, named so that a reader can see what produces the observable and what reads it: XOSC at 12 MHz drives clk_ref, which firmware/src/main.rs:524's default `ClockConfig` selects; clk_ref drives the TICKS block embassy enables with `timer0_cycles` = 12; that block increments TIMER0's counter once per microsecond; the counter reaches ALARM0; `TIMER0_IRQ_0` runs `check_alarm`; the queue wakes the render task spawned at firmware/src/main.rs:1022, firmware/src/main.rs:960 or firmware/src/main.rs:968; the task writes a frame; and a camera or a photodiode is what reads it. No host, no finger and no command appears anywhere in that chain. WHY NO READING DOES IT, and the trait contract is the part that surprises: `Driver::now`'s own documentation promises monotonic in the sense of `a greater or equal value than earlier calls`, which a stopped counter satisfies exactly — the contract does not contain this claim, so reading it cannot establish it. The hardware guarantee is a datasheet clause about the very silicon a run would be taken on. And the last in-tree reading — that no code writes TIMER0's PAUSE, SOURCE, LOCKED or the write-only TIMEHW/TIMELW halves, in this firmware or in embassy-rp — already sits inside PLAT-TIMER-001, and it is a reading of what the image does NOT do, which cannot establish what the silicon DOES. Of those four only PAUSE stops the count: SOURCE swaps the tick for clk_sys cycles, which is a RATE change and PLAT-TIMER-001's axis, and LOCKED only freezes write access. What no reading reaches at all is the clock underneath — a dead XOSC, or a TICKS block that quits, leaves every register this tree can name unwritten and the counter still. WHAT THE SHARED RECORDING SETTLES HERE AND NOT THERE, because two rows on one recording is where a reader stops telling them apart: this row reads the series for a stall that NEVER ENDS, PLAT-TIMER-002 for a stall of the order of one period that DOES end, and the verdicts do not move together. A stall that resumes is this row's PASS and that row's FAIL. A series that stops and never resumes is this row's FAIL and leaves that row INCONCLUSIVE rather than joining it. The one arm they share is a freeze beginning less than a period before the recording ends, inconclusive for both, and assurance/board/PLAT-TIMER-003.toml makes it a condition on the reading rather than only on the length. | maintainer | `SEC-FIDO-L01`, `SEC-FIDO-L02`, `SEC-FIDO-L03` | +| `PLAT-TIMER-003` | `timers` | The timebase ADVANCES: RP2350 `TIMER0`'s counter keeps counting for as long as the board has power, so every deadline armed against it is eventually reached and every spin on `Instant::now()` eventually ends. This is the condition the three liveness properties rest on and PLAT-TIMER-001's RATE reading is not: `EveryOpQuiesces`, `EveryWaitReleases` and `EveryWalkCloses` are EVENTUALLY claims, so a tick twelve times slow still releases every wait and closes every walk twelve times later, and only a tick that STOPS falsifies them. Nothing in this tree asserts it — firmware/src/presence.rs:141 pins the RATE at compile time and on one build arm of three, and no assert, test or proof anywhere covers progress. | **pending** | PLAT-TIMER-002'S RECORDING, READ FOR A DIFFERENT QUESTION, and shared deliberately rather than thriftily: the alternative is a second unattended two-hour run producing the same series. THE INSTRUMENT, AND WHY IT WITNESSES PROGRESS — per backend, because the first version of this sentence gave one mechanism to three and only one of them has it. What all three share is the STEP: each advances one step per `Timer` await and per nothing else — firmware/src/led.rs:618 (ws2812, the default), firmware/src/led.rs:727 (pimoroni) and firmware/src/led.rs:692 (the gpio soft-PWM's 2 ms period). WHAT DIFFERS IS WHAT THE STEP PAINTS. Only ws2812's frame is a function of the loop's own counter: firmware/src/led.rs:589 declares it, firmware/src/led.rs:596 advances it once per await, firmware/src/led.rs:607 is the only thing that reads it, and firmware/src/led.rs:536's `effect_sparkle` re-rolls it every eight steps. GPIO AND PIMORONI TAKE THEIRS FROM THE CLOCK: both call `Blinker::tick` (firmware/src/led.rs:353) — at firmware/src/led.rs:695 and firmware/src/led.rs:719 — which compares `Instant::now()` against a stored phase end at firmware/src/led.rs:361 and flips the phase when it is reached. THE CONCLUSION SURVIVES THE CORRECTION, and twice over rather than by luck: a stopped counter never returns the await, so no backend takes a step at all; and a clock-driven frame that somehow did step would read a frozen `Instant::now()` and paint the same colour anyway. So on all three a frame that CHANGES is a witness that the await returned, and an await returns only when the counter reached the value `set_alarm` armed; a stopped counter holds the last frame for ever and nothing on the device repairs it. THE CHAIN, named so that a reader can see what produces the observable and what reads it: XOSC at 12 MHz drives clk_ref, which firmware/src/main.rs:573's default `ClockConfig` selects; clk_ref drives the TICKS block embassy enables with `timer0_cycles` = 12; that block increments TIMER0's counter once per microsecond; the counter reaches ALARM0; `TIMER0_IRQ_0` runs `check_alarm`; the queue wakes the render task spawned at firmware/src/main.rs:1071, firmware/src/main.rs:1009 or firmware/src/main.rs:1017; the task writes a frame; and a camera or a photodiode is what reads it. No host, no finger and no command appears anywhere in that chain. WHY NO READING DOES IT, and the trait contract is the part that surprises: `Driver::now`'s own documentation promises monotonic in the sense of `a greater or equal value than earlier calls`, which a stopped counter satisfies exactly — the contract does not contain this claim, so reading it cannot establish it. The hardware guarantee is a datasheet clause about the very silicon a run would be taken on. And the last in-tree reading — that no code writes TIMER0's PAUSE, SOURCE, LOCKED or the write-only TIMEHW/TIMELW halves, in this firmware or in embassy-rp — already sits inside PLAT-TIMER-001, and it is a reading of what the image does NOT do, which cannot establish what the silicon DOES. Of those four only PAUSE stops the count: SOURCE swaps the tick for clk_sys cycles, which is a RATE change and PLAT-TIMER-001's axis, and LOCKED only freezes write access. What no reading reaches at all is the clock underneath — a dead XOSC, or a TICKS block that quits, leaves every register this tree can name unwritten and the counter still. WHAT THE SHARED RECORDING SETTLES HERE AND NOT THERE, because two rows on one recording is where a reader stops telling them apart: this row reads the series for a stall that NEVER ENDS, PLAT-TIMER-002 for a stall of the order of one period that DOES end, and the verdicts do not move together. A stall that resumes is this row's PASS and that row's FAIL. A series that stops and never resumes is this row's FAIL and leaves that row INCONCLUSIVE rather than joining it. The one arm they share is a freeze beginning less than a period before the recording ends, inconclusive for both, and assurance/board/PLAT-TIMER-003.toml makes it a condition on the reading rather than only on the length. | maintainer | `SEC-FIDO-L01`, `SEC-FIDO-L02`, `SEC-FIDO-L03` | | `PLAT-TOKEN-001` | `model-abstraction` | `RSKeySecurityState`'s `tok.live` is `FidoState::paut.in_use` and its `pin.set` is `Fs::has_data(EF_PIN)`; no second live token and no second PIN record exists that the model would have to carry a field for. | **pending** | A recorded session that asserts the implication, which the corpus can already carry and no configuration checks. Both halves are projected today: crates/rsk-device/src/ctap.rs:221 and :235 record `token_in_use_raw` and `pin_set`, formal/TraceSecurity.tla:44 and :32 map them into B, :60 and :52 map the model side, and R4aRawRefinesB equates all four at every boundary. Measured over formal/traces/security-phase4.jsonl: `token_in_use_raw` is TRUE in 24 of 40 pre and 24 of 40 post snapshots, `pin_record_len == 35` in 28 of each, and the boundaries where the antecedent holds and the consequent does not number ZERO. So unlike PLAT-MODEL-003, whose field is carried and never exercised, this projection runs its TRUE arm 48 times and the only thing missing is a line in an INVARIANTS block. | contributor | `SEC-FIDO-008` | | `PLAT-TOKEN-002` | `model-abstraction` | One `pin.set` boolean stands for one record. `EF_DEVICE_PIN`, the trusted-display build's second PIN verifier, is not it, so a state where the device PIN stands and `EF_PIN` is gone is one this model cannot distinguish from a factory-fresh device. | **pending** | A source audit of the two verifiers, re-run when a third issuance door is added: crates/rsk-display/src/gates.rs:11 states that the backing record and the floor differ between them, crates/rsk-display/src/pin.rs:752 that the FIDO PIN goes to EF_PIN as the same verifier the host uses, and :455 that a host authenticatorReset clears both. What makes the single boolean sound TODAY is that built-in UV verifies EF_PIN and not the device PIN (crates/rsk-fido/src/clientpin.rs:574 and :612), so both doors to a live token are gated on the record the model has. A pad-only issuance path reading EF_DEVICE_PIN would end this row. | contributor | `SEC-FIDO-008` | | `PLAT-TOKEN-003` | `model-abstraction` | `clientpin::issue_token` is the only production maker of a live pinUvAuthToken, so the one `Refines … — SEC-FIDO-008` tag in the tree is the whole antecedent side of the invariant. | **pending** | An enumeration a derivation already in the tree can re-run, and it was re-run rather than read: `git grep -n begin_using_token` gives 28 hits, all but two in files `assurance_gate.production_rust` drops by name, and of the two remaining `crates/rsk-fido/src/conformance/mod.rs:165` is reported by `assurance_gate.cfg_excluded` as `lib.rs declares mod conformance under cfg(test)`. That leaves crates/rsk-fido/src/clientpin.rs:425, inside the tagged function. A second in-image caller ends this row, and the derivation that would find it is the one just named. | contributor | `SEC-FIDO-008` | @@ -118,13 +118,13 @@ Which sites go under which row is not the registry's to choose alone. `docs/unsa | `PLAT-TOOLCHAIN-002` | `toolchain` | docs/unsafe.md is the enumeration of the first-party `unsafe` sites: every site the tree carries has a justification there, and every justification is about a site the tree still has. What that enumeration does NOT establish is that any one justification is CORRECT — that is a reading per site, and the twelve PLAT-UNSAFE rows below are where each one is written down and can be refuted. | **pending** | Two halves, and only the first is mechanical. MEMBERSHIP is derived: scripts/platform_gate.py produces one candidate per `unsafe` site and refuses both an unclaimed site and a claim on a site that is gone, and it compares that page's own `Runtime sites` count and its file list against the tree. The READING half is the twelve rows that refine this one; this entry is settled when they are. It deliberately covers no candidate of its own, so that it cannot stand in for a site nobody read. | contributor | — | | `PLAT-TRACE-001` | `model-abstraction` | A trace-linked claim rests on the fields the recorded session VARIES; 31 of the committed trace's 81 fields are constant across all 40 events, and a conjunct whose antecedent is one of them was never exercised by the replay. | **pending** | Sessions that vary them, and there is no substitute: a conjunct is checked by the replay only where its antecedent is true somewhere in the recording. Measured on formal/traces/security-phase4.jsonl — `soft_lock_raw`, `warm_boot_raw`, `token_user_present_raw`, `backup_sealed_record`, `seed_encrypted_record` and the four `cm_*_raw` counters are false or fixed in every event (`persistent_grant_record` left the list when provisioning started minting the record — the recording opens with one and setPIN deletes it — though the ISSUED grant `gate.ppuat` is FALSE in every replay state, so no conjunct that reads it is exercised), and `token_user_present_raw` together with `soft_lock_raw` are the antecedents of BOTH conjuncts of NoAuthorizationBypass. Two members of this class already had rows of their own — PLAT-MODEL-003 for `keydev_ram_raw` and formal/README.md's paragraph for `builtin_uv` — and the class is what those two are instances of. | contributor | `SEC-TRACE-001`, `SEC-TRACE-002`, `SEC-TRACE-004`, `SEC-REF-004` | | `PLAT-TRACE-002` | `tool-fidelity` | No recorded session reaches this property. `grep -lw NoTokenAfterInvalidation formal/*.cfg` returns every configuration that names the invariant and not one of them is a `TraceSecurity*` row, so the `trace` axis reads `0 of 0` — the honest value, and not a gap waiting to be filled by inheritance from a refinement property. | **pending** | A `TraceSecurity*` configuration that checks this invariant, over a recording that performs the operations it is about: a changePIN, a stopUsingPinUvAuthToken and a panel PIN entry. `formal/traces/security-phase4.jsonl` is the existing recording and none of its 40 pre/post snapshot pairs contains one. | contributor | `SEC-FIDO-003` | -| `PLAT-TRNG-001` | `build-configuration` | The RP2350 TRNG's three health checks are CONTINUOUS, this image leaves all three armed, and they fail CLOSED: `embassy_rp::trng::Config::default()` clears every bypass bit and `Trng::new` writes them into TRNG_DEBUG_CONTROL, firmware/src/main.rs:609 edits `sample_count` and nothing else, and on a failed check the part presents no result and every EHR_DATA register reads zero. So a source that has DIED cannot be drawn from: at boot that stalls before the USB pull-up goes up, and at the 64 KiB DRBG reseed (firmware/src/handler.rs:78) it wedges or halts the operation in flight. That the surviving source has ENTROPY is a DIFFERENT claim and PLAT-TRNG-002's, not this row's: three continuous checks are a dead-source detector, never an entropy estimator. | **discharged** | READ on 2026-09-05, and it is the reading half of a route that used to carry three claims of very different reachability under one id. FOUR READINGS. (1) ALL THREE CHECKS ARE ARMED, and the image WRITES that rather than inheriting a reset value: `embassy_rp::trng::Config::default()` sets `disable_autocorrelation_test`, `disable_crngt_test` and `disable_von_neumann_balancer` all `false`, and `Trng::new`'s `initialize_rng` writes those three bits into TRNG_DEBUG_CONTROL (0x138) on every construction. firmware/src/main.rs:601 takes that default, firmware/src/main.rs:609 edits `sample_count` alone, and firmware/src/main.rs:610 is the tree's ONLY `Trng::new`, and no first-party Rust assigns any of the three bypass fields: `git grep -E 'disable_autocorrelation_test\|disable_crngt_test\|disable_von_neumann_balancer' -- 'firmware/*.rs' 'crates/*.rs' 'tools/*.rs'` exits 1. THE THREE NAMES AND THE PATHSPEC ARE BOTH THE POINT, not tidiness. Unscoped, that grep answers this row's own prose and the page generated from it, so a claim about what the tree contains would be falsified by writing it down — PLAT-TIMER-001 records that trap one row over. And `disable_` alone over the same pathspec is NOT empty, which is why the command is the three field names and not the prefix — and the count is worth getting right, because the first version of this sentence got it wrong: FOUR hits in TWO files, two test names in crates/rsk-fido/src/vendor_tests.rs and two `disable_raw_mode` calls in tools/tui/src/main.rs, because git's pathspec matching lets `tools/*.rs` cross a `/`. The revision is pinned in Cargo.lock: embassy-rp 0.10.0 at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (2) THEY ARE CONTINUOUS, AND THERE IS NO START-UP TEST. RNG_ISR (0x104, RP2350 datasheet of 29 July 2025, p.1217) carries VN_ERR — 32 identical consecutive bits — CRNGT_ERR — two equal consecutive 16-bit blocks — and AUTOCORR_ERR, and each is evaluated as a block is produced rather than once at reset — AUTOCORR_ERR latching after four failures in a row rather than on the first, which the driver's own comment quotes from the register description. That datasheet describes no start-up test and no Adaptive Proportion Test, which is worth writing down because SP800-90B's health-test vocabulary makes a reader expect both: what this part gives is a per-block continuous test and nothing else. (3) THEY FAIL CLOSED AND LOUDLY, which is the half the row this was split from had backwards. §12.12.3 p.1213: on failure no results are presented and the EHR_DATA registers all read as 0, so TRNG_VALID.ehr_valid stays low and no byte reaches the caller; AUTOCORR_ERR is LATCHED — in the datasheet's own words the RNG then ceases functioning until the next reset — and the interrupt-clear register does not lift it: what the driver does instead is assert TRNG_SW_RESET and re-initialise, which is the loop below and not a recovery. This firmware's boot draw is the BLOCKING one: firmware/src/handler.rs:63 takes the `Trng` and firmware/src/handler.rs:65 fills 48 bytes through `blocking_fill_bytes`, whose not-valid branch soft-resets and re-samples on AUTOCORR_ERR and panics on anything else. Either way the boot does not finish — it is the 30-105 s stall firmware/src/main.rs:602-608 records, taken before the bus pull-up (firmware/src/usb_attach.rs:8) — and a device that never enumerates is not a device handing out predictable keys. AND THE BOOT IS NOT THE ONLY DRAW, which the first version of this reading missed: firmware/src/handler.rs:78 calls the same `blocking_fill_bytes` again on every `RESEED_INTERVAL` (firmware/src/handler.rs:57, 64 KiB of DRBG output), so a source that dies AFTER boot meets the same loop on an ENUMERATED device, inside whatever operation asked for the bytes. Loud in a second way — a wedged command or a `panic-halt` freeze rather than a device that never appears — and still not a quiet weak key, which is the claim. (4) WHAT `sample_count = 1000` DOES, AND WHAT IT DOES NOT. SAMPLE_CNT1 (0x130, p.1218) is the number of rng_clk cycles BETWEEN two consecutive ROSC samples, so 25 -> 1000 buys DECORRELATION and costs generation time; it does not change the 192 bits an EHR block carries and it adds no entropy to any of them. Why it was raised is on record and was measured on hardware at `2cdcc282e9605d601631b4b06727b2f746325727`: at 25 the autocorrelation check failed on nearly every block on one Waveshare unit and the driver soft-reset in a loop, an inverter-chain change did NOT help, and 1000 gives a seed in about a second and a half. THREE THINGS THIS DOES NOT SETTLE. That the surviving source has ENTROPY — the checks catch a source that has DIED and a merely DEGRADED one passes all three, which is the single failure this row's statement deliberately leaves open and PLAT-TRNG-002's whole subject. WHICH loud arm a CRNGT or VN failure takes: `blocking_wait_for_successful_generation` branches on `autocorr_err` alone and panics otherwise, and whether the part drops TRNG_BUSY with `ehr_valid` low on those two is a clause this reading does not have — a stall and a `panic-halt` freeze are both loud and neither is a quiet weak key, so the claim survives either answer while the answer stays unread. And NOTHING IN THIS TREE BACKS THE FAIL-CLOSED CLAUSE: the driver's `read_ehr_registers_into_array` copies EHR_DATA0 through EHR_DATA5 with no zero check and firmware/src/handler.rs:63 does not inspect the seed it gets, so `no byte reaches the caller` rests on the datasheet's promise that `ehr_valid` stays low and on no code here — a part that ever raised that flag over zeroed result registers would seed the DRBG with 48 zero bytes and nothing would say so. And the CORNERS: everything above is about what this image configures at desk conditions, and temperature, supply and process are PLAT-TRNG-003's. TWO THINGS THE READING FOUND AND THIS CHANGE REPAIRED, because they were firmware comments rather than registry entries and they were two copies of ONE claim: firmware/src/handler.rs:44 told the reader the block was drawn only through a working ROSC config with the chain at zero, and firmware/src/handler.rs:61 repeated it as a draw through the ROSC config the caller set on the `Trng`. No such configuration ever existed on this branch: `2cdcc282e9605d601631b4b06727b2f746325727` replaced the `inverter_chain_length = InverterChainLength::None` assignment with the `sample_count = 1000` one and dropped the `InverterChainLength` import, and left both sentences standing — so from 2026-06-18 the shipped image ran the driver's DEFAULT chain length while its own doc comment called that setting catastrophic, and `git grep inverter_chain_length` answered with no Rust at all. Both lines now say what the code does — the draw finishes because of `sample_count`, and the chain length is the driver's — which is what that commit's own message already recorded when it wrote that an inverter-chain change did NOT help. THE BEHAVIOUR WAS NEVER WRONG AND THE EXPLANATION OF IT WAS, for eleven weeks, which is why this belongs in a reading's discharge rather than in a fix: nothing about the image changed when the sentences did. | contributor | — | -| `PLAT-TRNG-002` | `trng` | The RAW ring-oscillator source clears the floor the shipped whitening needs — at least half a bit of min-entropy per raw bit, measured AT THE SAMPLE SPACING THE PRODUCT USES, on the die this project owns and at desk conditions. HALF A BIT IS SUFFICIENT AND NOT NECESSARY, and which way round is the point rather than a hedge: a von Neumann debiaser emits at most one bit per two raw bits, so at 0.5 bits per raw bit every whitened bit is backed by a full bit of input min-entropy AT EVERY RATE THAT BALANCER CAN ACHIEVE, and at a slower real rate by more. Below the floor the BOUND stops holding, not the device — von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it and what actually threatens it is CORRELATION between samples, which is what `sample_count` buys (PLAT-TRNG-001) and what the non-IID estimators are chosen to see. Neither of the two things this tree already has can answer it: the three continuous checks PLAT-TRNG-001 reads detect a source that has DIED and pass a degraded one, and a statistical test of the WHITENED output passes over a stuck source, which is what whitening is for. What rests on the floor is the 48-byte DRBG seed firmware/src/handler.rs:64 declares and firmware/src/handler.rs:65 fills at every boot, and every key the device mints afterwards. | **pending** | A BENCH IMAGE AND SP800-90B, and the route is REACHABLE — the first thing this split corrects, because the row it came from said the raw source needs an image `this tree does not ship` and left a reader picturing an exotic rig. The raw pre-whitening samples are readable in software on a stock part: TRNG_DEBUG_CONTROL (0x138, RP2350 datasheet of 29 July 2025, p.1219) exposes AUTO_CORRELATE_BYPASS, TRNG_CRNGT_BYPASS and VNC_BYPASS, and that datasheet's own bootrom listing at §12.12.4.1 p.1214 sets `sample_cnt1 = 0` and `trng_debug_control = -1u` and reads EHR_DATA to stream raw TRNG ROSC samples — shipping pico-sdk does the same in `src/rp2_common/pico_rand/rand.c`. THE RUN, in assurance/board/PLAT-TRNG-002.toml: that bypass configuration on one board, at least 1,000,000 consecutive raw samples captured and hashed into the record, then the SP800-90B §6.3 non-IID estimator suite over them. AT THE SHIPPED `SAMPLE_CNT1`, AND THAT IS A CORRECTION rather than a detail: the bootrom listing sets the sample period to 0 because it wants throughput, and 0 is the MOST correlated setting this part offers, while the product spaces its samples 1000 cycles apart. A verdict taken at 0 would be about a stream the device never draws, and this row's own FAIL is gate-forced to `refuted` — so the criterion is read off the 1000 capture and the 0 capture is kept beside it as the correlated control, which is also what makes the pair say whether the spacing bought anything. AND A SECOND HALF THE SAME SESSION CAN TAKE: AUTOCORR_STATISTIC (0x134, p.1218) read with the checks re-enabled at `sample_count` 25 and at the shipped 1000 on the same unit, which is the only way this project can put a RATE behind firmware/src/main.rs:609 — what stands behind it today is one wall-clock observation of a boot on one Waveshare unit at `2cdcc282e9605d601631b4b06727b2f746325727`, and a boot time is not a health-check failure rate. WHY THE CRITERION NAMES NO DATASHEET FIGURE, said here because the row this was split from named one and there is none. §12.12.1 p.1212 makes a bare compliance assertion and gives `approximately 7.5 kb/s of entropy`, which is a RATE and not min-entropy per bit; §12.12.2 p.1213 records that `software does not configure the ROSC length and sample count settings provided by the Arm characterisation procedure`, so the vendor declined the procedure that would have produced one; and the part holds no NIST ESV or ENT listing. `PASS = min-entropy at or above the datasheet figure` was therefore a clause no passing world could walk. The record names its own threshold and justifies it there instead, which is the whole of what changed. WHAT THIS RUN DOES NOT REACH, said because a PASS read as covering it would be read wrong: one die, one desk, one temperature and one supply — the corners are PLAT-TRNG-003's — and the whitening itself, since the capture bypasses it, so this measures the SOURCE and says nothing about how many raw bits the shipped path spends per EHR block. | maintainer | `SEC-FIDO-003`, `SEC-FIDO-008` | +| `PLAT-TRNG-001` | `build-configuration` | The RP2350 TRNG's three health checks are CONTINUOUS, this image leaves all three armed, and they fail CLOSED: `embassy_rp::trng::Config::default()` clears every bypass bit and `Trng::new` writes them into TRNG_DEBUG_CONTROL, firmware/src/main.rs:658 edits `sample_count` and nothing else, and on a failed check the part presents no result and every EHR_DATA register reads zero. So a source that has DIED cannot be drawn from: at boot that stalls before the USB pull-up goes up, and at the 64 KiB DRBG reseed (firmware/src/handler.rs:78) it wedges or halts the operation in flight. That the surviving source has ENTROPY is a DIFFERENT claim and PLAT-TRNG-002's, not this row's: three continuous checks are a dead-source detector, never an entropy estimator. | **discharged** | READ on 2026-09-05, and it is the reading half of a route that used to carry three claims of very different reachability under one id. FOUR READINGS. (1) ALL THREE CHECKS ARE ARMED, and the image WRITES that rather than inheriting a reset value: `embassy_rp::trng::Config::default()` sets `disable_autocorrelation_test`, `disable_crngt_test` and `disable_von_neumann_balancer` all `false`, and `Trng::new`'s `initialize_rng` writes those three bits into TRNG_DEBUG_CONTROL (0x138) on every construction. firmware/src/main.rs:650 takes that default, firmware/src/main.rs:658 edits `sample_count` alone, and firmware/src/main.rs:659 is the tree's ONLY `Trng::new`, and no first-party Rust assigns any of the three bypass fields: `git grep -E 'disable_autocorrelation_test\|disable_crngt_test\|disable_von_neumann_balancer' -- 'firmware/*.rs' 'crates/*.rs' 'tools/*.rs'` exits 1. THE THREE NAMES AND THE PATHSPEC ARE BOTH THE POINT, not tidiness. Unscoped, that grep answers this row's own prose and the page generated from it, so a claim about what the tree contains would be falsified by writing it down — PLAT-TIMER-001 records that trap one row over. And `disable_` alone over the same pathspec is NOT empty, which is why the command is the three field names and not the prefix — and the count is worth getting right, because the first version of this sentence got it wrong: FOUR hits in TWO files, two test names in crates/rsk-fido/src/vendor_tests.rs and two `disable_raw_mode` calls in tools/tui/src/main.rs, because git's pathspec matching lets `tools/*.rs` cross a `/`. The revision is pinned in Cargo.lock: embassy-rp 0.10.0 at git rev 6c28443489ad5940ab8c1824c090b1f8a7233bf6. (2) THEY ARE CONTINUOUS, AND THERE IS NO START-UP TEST. RNG_ISR (0x104, RP2350 datasheet of 29 July 2025, p.1217) carries VN_ERR — 32 identical consecutive bits — CRNGT_ERR — two equal consecutive 16-bit blocks — and AUTOCORR_ERR, and each is evaluated as a block is produced rather than once at reset — AUTOCORR_ERR latching after four failures in a row rather than on the first, which the driver's own comment quotes from the register description. That datasheet describes no start-up test and no Adaptive Proportion Test, which is worth writing down because SP800-90B's health-test vocabulary makes a reader expect both: what this part gives is a per-block continuous test and nothing else. (3) THEY FAIL CLOSED AND LOUDLY, which is the half the row this was split from had backwards. §12.12.3 p.1213: on failure no results are presented and the EHR_DATA registers all read as 0, so TRNG_VALID.ehr_valid stays low and no byte reaches the caller; AUTOCORR_ERR is LATCHED — in the datasheet's own words the RNG then ceases functioning until the next reset — and the interrupt-clear register does not lift it: what the driver does instead is assert TRNG_SW_RESET and re-initialise, which is the loop below and not a recovery. This firmware's boot draw is the BLOCKING one: firmware/src/handler.rs:63 takes the `Trng` and firmware/src/handler.rs:65 fills 48 bytes through `blocking_fill_bytes`, whose not-valid branch soft-resets and re-samples on AUTOCORR_ERR and panics on anything else. Either way the boot does not finish — it is the 30-105 s stall firmware/src/main.rs:651-657 records, taken before the bus pull-up (firmware/src/usb_attach.rs:8) — and a device that never enumerates is not a device handing out predictable keys. AND THE BOOT IS NOT THE ONLY DRAW, which the first version of this reading missed: firmware/src/handler.rs:78 calls the same `blocking_fill_bytes` again on every `RESEED_INTERVAL` (firmware/src/handler.rs:57, 64 KiB of DRBG output), so a source that dies AFTER boot meets the same loop on an ENUMERATED device, inside whatever operation asked for the bytes. Loud in a second way — a wedged command or a `panic-halt` freeze rather than a device that never appears — and still not a quiet weak key, which is the claim. (4) WHAT `sample_count = 1000` DOES, AND WHAT IT DOES NOT. SAMPLE_CNT1 (0x130, p.1218) is the number of rng_clk cycles BETWEEN two consecutive ROSC samples, so 25 -> 1000 buys DECORRELATION and costs generation time; it does not change the 192 bits an EHR block carries and it adds no entropy to any of them. Why it was raised is on record and was measured on hardware at `2cdcc282e9605d601631b4b06727b2f746325727`: at 25 the autocorrelation check failed on nearly every block on one Waveshare unit and the driver soft-reset in a loop, an inverter-chain change did NOT help, and 1000 gives a seed in about a second and a half. THREE THINGS THIS DOES NOT SETTLE. That the surviving source has ENTROPY — the checks catch a source that has DIED and a merely DEGRADED one passes all three, which is the single failure this row's statement deliberately leaves open and PLAT-TRNG-002's whole subject. WHICH loud arm a CRNGT or VN failure takes: `blocking_wait_for_successful_generation` branches on `autocorr_err` alone and panics otherwise, and whether the part drops TRNG_BUSY with `ehr_valid` low on those two is a clause this reading does not have — a stall and a `panic-halt` freeze are both loud and neither is a quiet weak key, so the claim survives either answer while the answer stays unread. And NOTHING IN THIS TREE BACKS THE FAIL-CLOSED CLAUSE: the driver's `read_ehr_registers_into_array` copies EHR_DATA0 through EHR_DATA5 with no zero check and firmware/src/handler.rs:63 does not inspect the seed it gets, so `no byte reaches the caller` rests on the datasheet's promise that `ehr_valid` stays low and on no code here — a part that ever raised that flag over zeroed result registers would seed the DRBG with 48 zero bytes and nothing would say so. And the CORNERS: everything above is about what this image configures at desk conditions, and temperature, supply and process are PLAT-TRNG-003's. TWO THINGS THE READING FOUND AND THIS CHANGE REPAIRED, because they were firmware comments rather than registry entries and they were two copies of ONE claim: firmware/src/handler.rs:44 told the reader the block was drawn only through a working ROSC config with the chain at zero, and firmware/src/handler.rs:61 repeated it as a draw through the ROSC config the caller set on the `Trng`. No such configuration ever existed on this branch: `2cdcc282e9605d601631b4b06727b2f746325727` replaced the `inverter_chain_length = InverterChainLength::None` assignment with the `sample_count = 1000` one and dropped the `InverterChainLength` import, and left both sentences standing — so from 2026-06-18 the shipped image ran the driver's DEFAULT chain length while its own doc comment called that setting catastrophic, and `git grep inverter_chain_length` answered with no Rust at all. Both lines now say what the code does — the draw finishes because of `sample_count`, and the chain length is the driver's — which is what that commit's own message already recorded when it wrote that an inverter-chain change did NOT help. THE BEHAVIOUR WAS NEVER WRONG AND THE EXPLANATION OF IT WAS, for eleven weeks, which is why this belongs in a reading's discharge rather than in a fix: nothing about the image changed when the sentences did. | contributor | — | +| `PLAT-TRNG-002` | `trng` | The RAW ring-oscillator source clears the floor the shipped whitening needs — at least half a bit of min-entropy per raw bit, measured AT THE SAMPLE SPACING THE PRODUCT USES, on the die this project owns and at desk conditions. HALF A BIT IS SUFFICIENT AND NOT NECESSARY, and which way round is the point rather than a hedge: a von Neumann debiaser emits at most one bit per two raw bits, so at 0.5 bits per raw bit every whitened bit is backed by a full bit of input min-entropy AT EVERY RATE THAT BALANCER CAN ACHIEVE, and at a slower real rate by more. Below the floor the BOUND stops holding, not the device — von Neumann's output is exactly uniform for any fixed bias, so bias alone never starves it and what actually threatens it is CORRELATION between samples, which is what `sample_count` buys (PLAT-TRNG-001) and what the non-IID estimators are chosen to see. Neither of the two things this tree already has can answer it: the three continuous checks PLAT-TRNG-001 reads detect a source that has DIED and pass a degraded one, and a statistical test of the WHITENED output passes over a stuck source, which is what whitening is for. What rests on the floor is the 48-byte DRBG seed firmware/src/handler.rs:64 declares and firmware/src/handler.rs:65 fills at every boot, and every key the device mints afterwards. | **pending** | A BENCH IMAGE AND SP800-90B, and the route is REACHABLE — the first thing this split corrects, because the row it came from said the raw source needs an image `this tree does not ship` and left a reader picturing an exotic rig. The raw pre-whitening samples are readable in software on a stock part: TRNG_DEBUG_CONTROL (0x138, RP2350 datasheet of 29 July 2025, p.1219) exposes AUTO_CORRELATE_BYPASS, TRNG_CRNGT_BYPASS and VNC_BYPASS, and that datasheet's own bootrom listing at §12.12.4.1 p.1214 sets `sample_cnt1 = 0` and `trng_debug_control = -1u` and reads EHR_DATA to stream raw TRNG ROSC samples — shipping pico-sdk does the same in `src/rp2_common/pico_rand/rand.c`. THE RUN, in assurance/board/PLAT-TRNG-002.toml: that bypass configuration on one board, at least 1,000,000 consecutive raw samples captured and hashed into the record, then the SP800-90B §6.3 non-IID estimator suite over them. AT THE SHIPPED `SAMPLE_CNT1`, AND THAT IS A CORRECTION rather than a detail: the bootrom listing sets the sample period to 0 because it wants throughput, and 0 is the MOST correlated setting this part offers, while the product spaces its samples 1000 cycles apart. A verdict taken at 0 would be about a stream the device never draws, and this row's own FAIL is gate-forced to `refuted` — so the criterion is read off the 1000 capture and the 0 capture is kept beside it as the correlated control, which is also what makes the pair say whether the spacing bought anything. AND A SECOND HALF THE SAME SESSION CAN TAKE: AUTOCORR_STATISTIC (0x134, p.1218) read with the checks re-enabled at `sample_count` 25 and at the shipped 1000 on the same unit, which is the only way this project can put a RATE behind firmware/src/main.rs:658 — what stands behind it today is one wall-clock observation of a boot on one Waveshare unit at `2cdcc282e9605d601631b4b06727b2f746325727`, and a boot time is not a health-check failure rate. WHY THE CRITERION NAMES NO DATASHEET FIGURE, said here because the row this was split from named one and there is none. §12.12.1 p.1212 makes a bare compliance assertion and gives `approximately 7.5 kb/s of entropy`, which is a RATE and not min-entropy per bit; §12.12.2 p.1213 records that `software does not configure the ROSC length and sample count settings provided by the Arm characterisation procedure`, so the vendor declined the procedure that would have produced one; and the part holds no NIST ESV or ENT listing. `PASS = min-entropy at or above the datasheet figure` was therefore a clause no passing world could walk. The record names its own threshold and justifies it there instead, which is the whole of what changed. WHAT THIS RUN DOES NOT REACH, said because a PASS read as covering it would be read wrong: one die, one desk, one temperature and one supply — the corners are PLAT-TRNG-003's — and the whitening itself, since the capture bypasses it, so this measures the SOURCE and says nothing about how many raw bits the shipped path spends per EHR block. | maintainer | `SEC-FIDO-003`, `SEC-FIDO-008` | | `PLAT-TRNG-003` | `trng` | Nothing in this project qualifies the raw source at the TEMPERATURE, SUPPLY and PROCESS corners. PLAT-TRNG-002 is one die on one desk, and a ring oscillator's entropy is a function of the jitter its corner leaves it, so a source that measures well at room temperature on a bench supply is not thereby a source that measures well in a warm pocket, on a hub that sags, or on the next wafer. The gap is ACCEPTED here rather than scheduled, and its severity is UNKNOWN rather than low. | **accepted-risk** | ACCEPTED, and the two routes are dead for DIFFERENT reasons — which is what makes this something other than an ordinary expensive row. THE EQUIPMENT: a thermal chamber, a programmable supply and enough parts for a process spread, none of which this project has, and hardware is the maintainer's alone under AGENTS.md. That much is only cost. THE REFERENCE IS THE HALF THAT DOES NOT COME BACK: there is no vendor number a corner result could be held against. §12.12.2 p.1213 of the RP2350 datasheet of 29 July 2025 records that `software does not configure the ROSC length and sample count settings provided by the Arm characterisation procedure` — the vendor declined the procedure — §12.12.1 p.1212 publishes a generation rate and no min-entropy per bit, and the part holds no NIST ESV or ENT listing. So even a fully instrumented corner run would produce a number with nothing to compare it to except a threshold this project chose for itself, which is a self-consistency check and not a qualification. WHY THIS IS NOT PLAT-CRYPTO-002'S SHAPE, and the difference is the reason this is a row instead of a sentence inside that one: that row accepts a residual it argues LOW, and it argues it FROM EVIDENCE IN THIS TREE — four command surfaces read for whether any hands a host a time-resolved observation, and arithmetic over a window table whose stride is in the source. THIS ROW CAN MAKE NO SUCH ARGUMENT AND MUST NOT LOOK AS THOUGH IT DOES. It has no in-scope half to read, no vendor figure to lean on and no measurement in either direction, so sitting under the same status word is the only thing the two have in common. WHAT WOULD TAKE THE ACCEPTANCE OFF, since an `accepted-risk` row may carry no `revalidated_by`: Raspberry Pi publishing a characterisation or a min-entropy figure for this ROSC; an ESV or ENT listing appearing for the part; an independent corner qualification of an RP2350 TRNG published by anyone; or this project acquiring the chamber and the supply, at which point the row becomes a maintainer-owned board obligation with a record of its own — and not before, because a `planned` record for a run no equipment can take is the dead route this split exists to remove. | vendor | — | | `PLAT-UNSAFE-001` | `toolchain` | `InterruptExecutor::on_interrupt` is called only from the interrupt the executor was started on: `EXECUTOR_HIGH.start(interrupt::SWI_IRQ_1)` is the only starter in the tree, and the `#[interrupt]` handler `SWI_IRQ_1` is its only caller. | **discharged** | Read both sides in firmware/src/main.rs and count them: one `EXECUTOR_HIGH.start`, which names `interrupt::SWI_IRQ_1`, and one `on_interrupt` call, inside the handler of that same name. Done, and the counts are what make it refutable — a second starter, a second caller, or a start naming a different interrupt refutes it, and each is one grep. | contributor | — | | `PLAT-UNSAFE-002` | `toolchain` | `SendUsb` is sound: `embassy_usb::UsbDevice` is `!Send` only for the `&mut dyn Handler` control-request handlers it holds, and every stateful handler this firmware registers keeps its state in a critical-section static, while the wrapper itself is constructed once and moved into exactly one task on one executor. | **pending** | Two readings, and only one is done. The OWNERSHIP half is read and holds: firmware/src/main.rs constructs `SendUsb` at one place and hands it to `usb_task` on the high-priority executor, and nothing else names the type. The HANDLER half is what is owed — a walk of every `Handler` the USB builder is given, asking of each whether its state is a zero-sized type or a `Sync` static. A handler with plain interior mutability refutes the row. | contributor | — | | `PLAT-UNSAFE-003` | `toolchain` | `HEAP.init` runs exactly once, over a static buffer nothing else touches, and before the first allocation the image can make. | **pending** | Read firmware/src/main.rs for the first two — one `HEAP.init`, one `HEAP_MEM`, both inside one block — which hold. The third is what is owed and is not visible from the call site: no allocating call on the path from the reset vector to that line, including inside `embassy_rp::init` and any `StaticCell` or dependency initialiser that runs before it. An allocation before the allocator is initialised refutes it. | contributor | — | -| `PLAT-UNSAFE-004` | `build-configuration` | Each of the eight build-selected GPIOs handed to `AnyPin::steal` has exactly one owner at runtime: no two of them name the same pad, none of them is a pad another driver claims, and the LED data pin a host can re-aim cannot be pointed at any of them. | **pending** | The compile-time asserts in firmware/build.rs and the boot-time filter in firmware/src/main.rs, read TOGETHER with the presence button's own steal in firmware/src/presence.rs and against the five board files under firmware/boards. The asserts reject a build that collides LED_POWER_PIN, USR_LED_PIN, WAKE_PIN or the four panel control lines with each other, with the LED data pin, or with the hard-wired SPI1 and I2C1 lines; the filter drops a host-written data pin naming one of them back to the build default. What is owed is the PAIRWISE table: that the assert set covers every pair of the eight rather than the pairs somebody thought of. One uncovered pair refutes it. | contributor | — | +| `PLAT-UNSAFE-004` | `build-configuration` | Each of the eleven build-selected GPIOs handed to `AnyPin::steal` has exactly one owner at runtime: no two of them name the same pad, none of them is a pad another driver claims, and the LED data pin a host can re-aim cannot be pointed at any of them. | **pending** | The compile-time asserts in firmware/build.rs and the boot-time filter in firmware/src/main.rs, read TOGETHER with the presence button's own steal in firmware/src/presence.rs and against the five board files under firmware/boards. The asserts reject a build that collides LED_POWER_PIN, USR_LED_PIN, WAKE_PIN, the display's four panel control lines or the status panel's CS/DC/backlight pins with each other, with the LED data pin, or with the hard-wired SPI1 and I2C1 lines; the filter drops a host-written data pin naming one of them back to the build default. What is owed is the PAIRWISE table: that the assert set covers every pair of the eleven rather than the pairs somebody thought of. One uncovered pair refutes it. | contributor | — | | `PLAT-UNSAFE-005` | `toolchain` | The two `static mut` prime sieves are single-core-exclusive: `CORE0_SIEVE` is taken by reference only on core 0 and `CORE1_SIEVE` only on core 1, and the end-of-search scrub is issued by the core that owns the sieve it scrubs, so the two exclusive references never alias. | **pending** | A source audit of the three access sites in firmware/src/core1.rs, each read for WHICH core reaches it: two on core 1 (the search, and the scrub on its STOP edge) and one on core 0, in `run_rsa_search`. The partition is structural rather than locked, so it is refuted by a fourth access, by a scrub issued from the reboot path on core 0, or by `run_rsa_search` becoming reachable from core 1. | contributor | — | | `PLAT-UNSAFE-006` | `toolchain` | Each core programs MSPLIM once, on the core that owns that stack, before any frame has been pushed below the value it writes: core 0 with the linker's `_stack_end`, core 1 with its own stack base. | **pending** | Read the two write sites against the entry points that reach them — `arm_stack_limit` in firmware/src/main.rs from the first statement of `main`, and the firmware/src/core1.rs write from the top of the core-1 entry — and against the linker script that defines `_stack_end`. Whether the register TRAPS is a fact about ARMv8-M silicon and is deliberately NOT this row: no board result in this tree records it, and PLAT-UNSAFE rows are readings, not measurements. | contributor | — | | `PLAT-UNSAFE-007` | `toolchain` | The three FFI calls into the vendored ARM assembly pass fully owned, length-checked buffers on both sides, so no routine can write past what the caller sized. | **pending** | A call-by-call reading of the three calls in crates/rsk-rsa/src/lib.rs against crates/rsk-rsa/csrc/bignum_high_level.h: for each pointer, the buffer it names, the length the caller passes, and the length the C side writes. The containment already in place does not settle it and is worth being plain about — the power-on KAT gates key generation and every signature is Bellcore-checked, so a WRONG answer cannot leave the card, but neither check can see an out-of-bounds write. | contributor | — | diff --git a/docs/unsafe.md b/docs/unsafe.md index 822a68b10..84418b3c5 100644 --- a/docs/unsafe.md +++ b/docs/unsafe.md @@ -7,13 +7,13 @@ contained. Adding a new `unsafe` requires updating this page. (Safe Rust rules out memory-corruption bugs in this code. It is not a security audit; see the [threat model](threat-model.md).) -**Runtime sites: 22.** Twelve in the firmware proper (`main.rs` + `presence.rs`): -the interrupt-handler pair (2), the `Send` impl, the heap init, and the eight +**Runtime sites: 25.** Fifteen in the firmware proper (`main.rs` + `presence.rs`): +the interrupt-handler pair (2), the `Send` impl, the heap init, and the eleven GPIO-pin `steal`s (the presence button, the LED power-enable rail, the nuisance -USR LED, the display build's wake button, and — display builds only — the panel's -CS/DC/RST/TP_RST control lines). Three for the per-core prime sieves and one -stack limit per core, three in the RSA assembly FFI, two in the standalone -flash-wipe tool. +USR LED, the display build's wake button and its CS/DC/RST/TP_RST control lines, +and — status-panel builds only — that block's CS/DC/backlight pins). Three for +the per-core prime sieves and one stack limit per core, three in the RSA assembly +FFI, two in the standalone flash-wipe tool. ```mermaid flowchart TB @@ -21,7 +21,7 @@ flowchart TB a["interrupt executor (×2)"] b["Send for SendUsb"] c["heap init"] - d["GPIO pin steal ×8 (presence, LED power, USR LED, display wake + CS/DC/RST/TP_RST)"] + d["GPIO pin steal ×11 (presence, LED power, USR LED, display wake + CS/DC/RST/TP_RST, panel CS/DC/BL)"] d2["core0 stack limit (MSPLIM)"] end subgraph kg["firmware/src/core1.rs"] @@ -85,13 +85,13 @@ static buffer used by nothing else. *Safe alternative:* none; every embedded allocator initializes this way. *Containment:* one call, before any allocation can happen. -### 5–12. GPIO pin type-erasure (presence button, LED power rail, USR-LED-off, display wake + control pins, ×8) — `PLAT-UNSAFE-004` +### 5–15. GPIO pin type-erasure (presence button, LED power rail, USR-LED-off, display wake + control pins, panel control pins, ×11) — `PLAT-UNSAFE-004` ```rust let any = unsafe { AnyPin::steal(pin) }; ``` -Eight build-configurable GPIOs are chosen by *number* at build time rather than as +Eleven build-configurable GPIOs are chosen by *number* at build time rather than as a concrete `PIN_n` type, so each must be converted to embassy's type-erased `AnyPin`: the optional `PRESENCE_PIN=` presence button (`ButtonPresence::new_gpio`, `presence.rs`), the optional `LED_POWER_PIN` @@ -99,8 +99,9 @@ enable pin driven high to power a gated LED rail (the LED block in `main.rs`), the optional `USR_LED_PIN` driven to a nuisance onboard LED's OFF level and held (the boot block in `main.rs`), and — display builds only — the optional `WAKE_PIN` button that wakes the panel from display sleep **plus the panel's -CS/DC/RST/TP_RST control lines**, all by board-config number, in the panel block -of `main.rs`. `AnyPin::steal` is `unsafe` because the caller must +CS/DC/RST/TP_RST control lines**, and — status-panel (`panel`) builds only — the +status screen's **CS/DC/backlight** lines, all by board-config number, in the +panel block of `main.rs`. `AnyPin::steal` is `unsafe` because the caller must guarantee unique ownership of that hardware pin — a `match` over `p.PIN_0..=PIN_29` (as the LED *data* pin uses) is impossible here, since it would double-move the peripheral set the LED block already claims. @@ -108,23 +109,25 @@ peripheral set the LED block already claims. constructors require a statically known pin type. *Containment:* each is gated by pin-range validation and the single-owner invariant from `main` — none of the presence pin, the LED-power pin, the -USR-LED pin, the wake pin, nor the four panel control pins is ever handed to -another driver. Two checks hold that jointly: the LED *data* pin is resolved at +USR-LED pin, the wake pin, the display's four control pins, nor the status +panel's CS/DC/backlight pins is ever handed to another driver. Two checks hold +that jointly: the LED *data* pin is resolved at runtime from the host-writable phy record, and the filter chain in `main` drops a value that names the presence pin, `LED_POWER_PIN` or `USR_LED_PIN` back to the build default — so a host cannot aim the data pin at a pad another driver owns. On top of that, compile-time `assert!`s reject a build that collides `LED_POWER_PIN` or `USR_LED_PIN` with the LED data pin or a GPIO -`PRESENCE_PIN` (and refuse `USR_LED_PIN` outright on a display build, whose panel +`PRESENCE_PIN` (and refuse `USR_LED_PIN` outright on a screen build, whose panel owns those pads), rejects a `WAKE_PIN` in the LCD/touch range (`10..=18`), -**and rejects any of CS/DC/RST/TP_RST/BL colliding with each other, with the -hard-wired PIO serial output (PIN_10/11) or I2C1 (PIN_6/7) lines, an enabled `WAKE_PIN`, -or `LED_PIN`/`LED_POWER_PIN` when their LED driver is built** — a collision +**and rejects any of CS/DC/RST/TP_RST/BL, or the status panel's CS/DC/BL, +colliding with each other, with the hard-wired PIO serial output (PIN_10/11) or +I2C1 (PIN_6/7) lines, or with an enabled `WAKE_PIN`, +`LED_PIN`/`LED_POWER_PIN` when their LED driver is built** — a collision silently drives one pad from two owners at runtime, so it is checked at build time. ## Firmware dual-core keygen (`firmware/src/core1.rs`) -### 13–15. The per-core prime sieves — `PLAT-UNSAFE-005` +### 16–18. The per-core prime sieves — `PLAT-UNSAFE-005` ```rust static mut CORE0_SIEVE: IncrementalSieve = IncrementalSieve::new(); @@ -156,7 +159,7 @@ on core0; the partition (which core touches which sieve) is structural, and the a candidate, scrubbed at the top of every keygen). A wrong residue can only let a composite through to the strong-MR/Lucas test, which still rejects it. -### 16–17. The per-core stack limits (`main.rs`, `core1.rs`) — `PLAT-UNSAFE-006` +### 19–20. The per-core stack limits (`main.rs`, `core1.rs`) — `PLAT-UNSAFE-006` ```rust unsafe { cortex_m::register::msplim::write(&raw const _stack_end as u32) }; // core0, entering `main` @@ -191,7 +194,7 @@ issued by the routine that is at that moment generating and storing a key. ## RSA assembly FFI (`crates/rsk-rsa/src/lib.rs`) -### 18–20. The modexp / CRT-sign calls — `PLAT-UNSAFE-007` +### 21–23. The modexp / CRT-sign calls — `PLAT-UNSAFE-007` On-card RSA key generation needs hundreds of modular exponentiations over 1024–2048-bit candidates. The pure-Rust path was ~7× too slow on the @@ -210,7 +213,7 @@ all host tests exercise the same API safely. ## Flash wiper (`rsk-wipe/src/main.rs`) -### 21–22. Raw flash erase/program in a critical section — `PLAT-UNSAFE-008` +### 24–25. Raw flash erase/program in a critical section — `PLAT-UNSAFE-008` The wiper's entire job is to erase the flash the firmware lives on, from a RAM-resident image. It calls the ROM flash-erase/program routines inside diff --git a/docs/verified-compilation.md b/docs/verified-compilation.md index f8a6e7fe2..196889dce 100644 --- a/docs/verified-compilation.md +++ b/docs/verified-compilation.md @@ -154,7 +154,7 @@ checksums, provenance, signing, notes, publication — are language-agnostic. ## What it costs the gate -`scripts/check.sh` runs 124 rows: 55 invoke `cargo`, 48 are Python, 21 are +`scripts/check.sh` runs 126 rows: 57 invoke `cargo`, 48 are Python, 21 are neither. A kernel outside Rust is invisible to a measured nine of them, and would need a twin for many more. @@ -165,7 +165,7 @@ would need a twin for many more. workspace manifests, `scripts/kani_gate.py` from files with an `.rs` suffix — so a C or extracted-C kernel is not a thing they can fail about. -**Would need a twin:** 19 clippy rows, 8 rustdoc rows, 4 fmt rows and 14 +**Would need a twin:** 20 clippy rows, 8 rustdoc rows, 4 fmt rows and 14 `cargo test` rows. A second language does not inherit `-D warnings`, a formatter the gate can run, a doc build, or a test harness whose empty selection is already caught. @@ -195,7 +195,7 @@ A decision with no falsifier is a preference. Any one of these overturns it: generation, verifying that C would cover the whole kernel instead of a quarter of it. - **The gate stops being Rust-shaped.** If the tree acquires a second language - for an unrelated reason and pays the 19-clippy-row-shaped cost anyway, the + for an unrelated reason and pays the 20-clippy-row-shaped cost anyway, the marginal cost of this decision falls to the toolchain delta alone. None of the five is close. The first two are outside this project's control. diff --git a/firmware/Cargo.toml b/firmware/Cargo.toml index 4a5da39fb..4dbd33e82 100644 --- a/firmware/Cargo.toml +++ b/firmware/Cargo.toml @@ -173,6 +173,13 @@ strict-config = ["rsk-mgmt/strict-config", "rsk-fido/strict-config", "rsk-otp/st # firmware --features display` (the panel is the status indicator, so there is no # addressable LED, which also frees GPIO16 for the backlight). display = ["rsk-device/display", "dep:rsk-display", "dep:rsk-ui", "dep:rsk-bip39", "dep:rsk-slip39", "dep:mipidsi", "dep:embedded-graphics"] +# Status-only panel build for a board whose screen is a plain SPI module with no +# touch (ALIENTEK RP2350A small-system board: 1.14" ST7789V2, 240x135, on SPI1). +# It paints the device's own status and adds NO presence or PIN surface — user +# presence stays on the BOOTSEL button — so it is NOT the trusted display. OFF by +# default and `dep:`-gated, so a standard key without a screen compiles none of it. +# Mutually exclusive with `display`; see the guard in `main.rs`. +panel = ["dep:rsk-ui", "dep:embedded-graphics"] [[bin]] name = "firmware" diff --git a/firmware/build.rs b/firmware/build.rs index ea94927b5..76d595766 100644 --- a/firmware/build.rs +++ b/firmware/build.rs @@ -77,6 +77,11 @@ struct BoardConfig { display_i2c_freq_hz: Option, display_invert_colors: Option, display_color_order: Option, + // panel (status-only screen; see firmware/src/panel.rs) + panel_spi_freq_hz: Option, + panel_cs: Option, + panel_dc: Option, + panel_bl_pin: Option, } fn read_board() -> Option { @@ -120,6 +125,10 @@ fn parse_toml(raw: &str) -> BoardConfig { display_i2c_freq_hz: None, display_invert_colors: None, display_color_order: None, + panel_spi_freq_hz: None, + panel_cs: None, + panel_dc: None, + panel_bl_pin: None, }; let mut sec = ""; fn strip_comment(s: &str) -> &str { @@ -220,6 +229,10 @@ fn parse_toml(raw: &str) -> BoardConfig { ("display", "i2c_freq_hz") => c.display_i2c_freq_hz = Some(u32(v)), ("display", "invert_colors") => c.display_invert_colors = Some(b(v)), ("display", "color_order") => c.display_color_order = Some(u(v).to_string()), + ("panel", "spi_freq_hz") => c.panel_spi_freq_hz = Some(u32(v)), + ("panel", "cs") => c.panel_cs = Some(u8(v)), + ("panel", "dc") => c.panel_dc = Some(u8(v)), + ("panel", "bl_pin") => c.panel_bl_pin = Some(u8(v)), _ => {} } } @@ -451,8 +464,8 @@ fn main() { // Bake fake OTP keys into the image instead of reading the fuses — exercises // the kbase migration + boot path without an irreversible OTP write. // TEST BUILDS ONLY; never set for a shipped image. - // Display pin defaults (board config or env var or hardcoded). - macro_rules! disp { + // Display / panel pin defaults (board config or env var or hardcoded). + macro_rules! bake { ($key:expr, $val:expr) => { if env::var($key).is_err() { println!("cargo:rustc-env={}={}", $key, $val); @@ -463,7 +476,7 @@ fn main() { // collision block in `main.rs` only tests pins against *each other* and against the // hard-wired buses, so without this an out-of-range control pin reaches // `AnyPin::steal` and drives whatever pad it aliases onto. - macro_rules! disp_pin { + macro_rules! bake_pin { ($key:expr, $val:expr) => {{ let v: u8 = $val; assert!( @@ -473,7 +486,7 @@ fn main() { MAX_GPIO, v ); - disp!($key, v); + bake!($key, v); }}; } let b = board.as_ref(); @@ -487,41 +500,41 @@ fn main() { // boards/waveshare-touch-lcd.toml; it kept the pre-PIO 62.5 MHz after that file // moved to 80, which left every display build without an explicit `BOARD=` — the // one check.sh compiles included — asserting itself dead in `PioDisplayTx::new`. - disp!( + bake!( "PK_DISPLAY_SPI_FREQ_HZ", disp_cfg .and_then(|b| b.display_spi_freq_hz) .unwrap_or(80_000_000) ); - disp_pin!( + bake_pin!( "PK_DISPLAY_CS", disp_cfg.and_then(|b| b.display_cs).unwrap_or(13) ); - disp_pin!( + bake_pin!( "PK_DISPLAY_DC", disp_cfg.and_then(|b| b.display_dc).unwrap_or(14) ); - disp_pin!( + bake_pin!( "PK_DISPLAY_RST", disp_cfg.and_then(|b| b.display_rst).unwrap_or(15) ); - disp_pin!( + bake_pin!( "PK_DISPLAY_BL_PIN", disp_cfg.and_then(|b| b.display_bl_pin).unwrap_or(16) ); - disp!( + bake!( "PK_DISPLAY_BL_PWM_SLICE", disp_cfg.and_then(|b| b.display_bl_pwm_slice).unwrap_or(0) ); - disp!( + bake!( "PK_DISPLAY_BL_PWM_CHANNEL", disp_cfg.and_then(|b| b.display_bl_pwm_channel).unwrap_or(0) ); - disp_pin!( + bake_pin!( "PK_DISPLAY_TP_RST", disp_cfg.and_then(|b| b.display_tp_rst).unwrap_or(17) ); - disp!( + bake!( "PK_DISPLAY_I2C_FREQ_HZ", disp_cfg .and_then(|b| b.display_i2c_freq_hz) @@ -570,6 +583,37 @@ fn main() { println!("cargo:rerun-if-env-changed=PK_DISPLAY_INVERT_COLORS"); println!("cargo:rerun-if-env-changed=PK_DISPLAY_COLOR_ORDER"); + // Status-only panel (`panel` feature). The defaults are ALIENTEK's RP2350A + // small-system board (1.14" ST7789V2: CS=9, DC=8, backlight=25 over SPI1, whose + // SCK/MOSI on GPIO10/11 are hard-wired in `firmware/src/panel.rs`); a board + // file's `[panel]` section overrides them. The backlight there is an active-LOW + // PNP high-side switch, so a board that differs needs its own `[panel]` section + // *and* a polarity decision in `panel.rs`. The panel is a plain 4-wire SPI + // module — no touch controller, no PWM backlight — so these four are all of it. + let panel_cfg = b.and_then(|b| b.panel_cs.map(|_| b)); + bake!( + "PK_PANEL_SPI_FREQ_HZ", + panel_cfg + .and_then(|b| b.panel_spi_freq_hz) + .unwrap_or(40_000_000) + ); + bake_pin!( + "PK_PANEL_CS", + panel_cfg.and_then(|b| b.panel_cs).unwrap_or(9) + ); + bake_pin!( + "PK_PANEL_DC", + panel_cfg.and_then(|b| b.panel_dc).unwrap_or(8) + ); + bake_pin!( + "PK_PANEL_BL_PIN", + panel_cfg.and_then(|b| b.panel_bl_pin).unwrap_or(25) + ); + println!("cargo:rerun-if-env-changed=PK_PANEL_SPI_FREQ_HZ"); + println!("cargo:rerun-if-env-changed=PK_PANEL_CS"); + println!("cargo:rerun-if-env-changed=PK_PANEL_DC"); + println!("cargo:rerun-if-env-changed=PK_PANEL_BL_PIN"); + for (env_var, baked) in [("FAKE_MKEK", "PK_FAKE_MKEK"), ("FAKE_DEVK", "PK_FAKE_DEVK")] { if let Some(hex) = resolve_fake_key(env_var) { println!("cargo:rustc-env={baked}={hex}"); diff --git a/firmware/src/main.rs b/firmware/src/main.rs index 4b4d4e5ca..9c159bcff 100644 --- a/firmware/src/main.rs +++ b/firmware/src/main.rs @@ -52,6 +52,8 @@ mod handler; mod led; mod otp_kbd; mod otp_keys; +#[cfg(feature = "panel")] +mod panel; mod pin_lock; mod presence; mod rescue_platform; @@ -70,6 +72,16 @@ compile_error!( display` — the `firmware-display` nix flavor sets this for you" ); +// `display` is the 240x320 touchscreen trusted UI (presence on the panel); `panel` +// is the 240x135 status-only screen (presence on BOOTSEL). Two different panels with +// two different geometries and presence sources — a build that asked for both is a +// mistake, so refuse it rather than let one silently win. +#[cfg(all(feature = "display", feature = "panel"))] +compile_error!( + "the `display` and `panel` features are mutually exclusive: `display` is the \ + 240x320 touchscreen trusted UI, `panel` the 240x135 status-only screen" +); + use flash_storage::FLASH_SIZE; use handler::{FidoRng, Store}; #[cfg(not(feature = "display"))] @@ -382,6 +394,39 @@ const _: () = assert!( "USR_LED_PIN is not supported on a display build (the panel replaces the onboard LED)" ); +// Status-only panel (`panel` feature). SCK/MOSI are hard-wired to SPI1's GPIO10/11 +// in `panel.rs`, matching the board's LCD wiring, so only the control pins and the +// SPI clock come from the board config — carried by the `[display]` section, which +// is the same panel-wiring vocabulary the trusted display uses. +#[cfg(feature = "panel")] +const BUILD_PANEL_SPI_FREQ_HZ: u32 = env_u32(env!("PK_PANEL_SPI_FREQ_HZ")); +#[cfg(feature = "panel")] +const BUILD_PANEL_CS: u8 = env_u16(env!("PK_PANEL_CS")) as u8; +#[cfg(feature = "panel")] +const BUILD_PANEL_DC: u8 = env_u16(env!("PK_PANEL_DC")) as u8; +#[cfg(feature = "panel")] +const BUILD_PANEL_BL_PIN: u8 = env_u16(env!("PK_PANEL_BL_PIN")) as u8; + +// The panel's control pins are claimed via `AnyPin::steal`, so reject a pad owned +// by two drivers — including SPI1's hard-wired SCK/MOSI (10/11) — at compile time. +#[cfg(feature = "panel")] +const _: () = { + const CTLS: &[u8] = &[BUILD_PANEL_CS, BUILD_PANEL_DC, BUILD_PANEL_BL_PIN]; + let mut i = 0; + while i < CTLS.len() { + assert!( + CTLS[i] != 10 && CTLS[i] != 11, + "panel control GPIO overlaps SPI1 SCK/MOSI (10/11)" + ); + let mut j = i + 1; + while j < CTLS.len() { + assert!(CTLS[i] != CTLS[j], "duplicate panel GPIO"); + j += 1; + } + i += 1; + } +}; + #[cfg(led_kind = "ws2812")] const BUILD_DRIVER: u8 = 3; #[cfg(led_kind = "gpio")] @@ -450,6 +495,10 @@ static PHY_MANUFACTURER: StaticCell<[u8; 64]> = StaticCell::new(); /// invariant as FS/RNG above — borrows never span `.await`. #[cfg(feature = "display")] static UI: StaticCell> = StaticCell::new(); +/// The status-only panel, shared with the task that repaints it. Same `RefCell` +/// invariant as `UI` — borrows never span `.await`. +#[cfg(feature = "panel")] +static PANEL: StaticCell> = StaticCell::new(); struct SendUsb(UsbDevice<'static, Drv>); unsafe impl Send for SendUsb {} @@ -714,7 +763,7 @@ async fn main(spawner: Spawner) { config.max_power = 100; config.max_packet_size_0 = 64; // bcdDevice build counter; also surfaced on the trusted-display Firmware screen. - let device_release: u16 = 0x09DA; + let device_release: u16 = 0x09DB; config.device_release = device_release; let mut builder = Builder::new( @@ -1144,6 +1193,46 @@ async fn main(spawner: Spawner) { spawner.spawn(display::status_task(ui).unwrap()); ui }; + + // Status-only panel (the `panel` feature). Built after the USB task is spawned, + // like the display block above, so the panel's blocking init runs on the thread + // executor while the interrupt executor enumerates. It adds no presence source of + // its own — `presence_ref` below stays on the BOOTSEL button and merely gets + // wrapped so the screen can show a wait. + #[cfg(feature = "panel")] + let panel_ui: &'static RefCell = { + use embassy_rp::gpio::{Level, Output}; + use embassy_rp::spi::{Config as SpiConfig, Phase, Polarity, Spi}; + + let mut spi_cfg = SpiConfig::default(); + spi_cfg.frequency = BUILD_PANEL_SPI_FREQ_HZ; + spi_cfg.polarity = Polarity::IdleLow; + spi_cfg.phase = Phase::CaptureOnFirstTransition; + let spi = Spi::new_blocking_txonly(p.SPI1, p.PIN_10, p.PIN_11, spi_cfg); + + let cs = Output::new( + unsafe { embassy_rp::gpio::AnyPin::steal(BUILD_PANEL_CS) }, + Level::High, + ); + let dc = Output::new( + unsafe { embassy_rp::gpio::AnyPin::steal(BUILD_PANEL_DC) }, + Level::Low, + ); + // The backlight is an active-low PNP high-side switch on the reference board, + // so `Level::High` holds it dark through init; `Panel::new` pulls it low once + // the first frame is painted. + let bl = Output::new( + unsafe { embassy_rp::gpio::AnyPin::steal(BUILD_PANEL_BL_PIN) }, + Level::High, + ); + let info = panel::Info { + version: device_release, + chipid: u64::from_le_bytes(serial_id), + }; + let ui = PANEL.init(RefCell::new(panel::Panel::new(spi, cs, dc, bl, info))); + spawner.spawn(panel::status_task(ui, info).unwrap()); + ui + }; core1::spawn(p.CORE1); // Standard key: BOOTSEL by default, or a dedicated `PRESENCE_PIN` GPIO button. @@ -1151,11 +1240,17 @@ async fn main(spawner: Spawner) { // rejected at compile time — see the `BUILD_PRESENCE_IS_GPIO` assert above). #[cfg(not(feature = "display"))] let presence_ref = { - let presence = if BUILD_PRESENCE_IS_GPIO { + let button = if BUILD_PRESENCE_IS_GPIO { ButtonPresence::new_gpio(BUILD_PRESENCE_PIN, BUILD_PRESENCE_ACTIVE_HIGH) } else { ButtonPresence::new_bootsel(p.BOOTSEL) }; + // The `panel` build wraps that same button so the screen can show the press + // it is waiting for; the plain key installs the button as-is. + #[cfg(feature = "panel")] + let presence = panel::PanelPresence::new(panel_ui, button); + #[cfg(not(feature = "panel"))] + let presence = button; PRESENCE.init(RefCell::new(presence)) }; #[cfg(feature = "display")] diff --git a/firmware/src/panel.rs b/firmware/src/panel.rs new file mode 100644 index 000000000..c977bb6a2 --- /dev/null +++ b/firmware/src/panel.rs @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 RS-Key contributors + +//! The status-only panel: a 240×135 ST7789 LCD driven straight off the board's +//! hardware SPI, showing the device's own status (build, chip id, USB, uptime). +//! +//! This is NOT the trusted display. It has no touch controller, so it offers no +//! Approve/Deny and no on-panel PIN entry — user presence stays on the BOOTSEL +//! button and every ceremony the host runs is unchanged. The panel is a status +//! indicator in the place an addressable LED would otherwise sit, for boards +//! whose screen is a plain 1.14" SPI module (the reference is ALIENTEK's +//! RP2350A small-system board, whose panel is an ST7789V2 wired to SPI1). +//! +//! Everything drawn here is device-owned: the build counter, the chip serial, an +//! attach flag and uptime. No host-supplied string reaches this screen, so there +//! is nothing to sanitise and no `rsk_ui::Label` in the path. + +use core::cell::RefCell; +use core::convert::Infallible; + +use embassy_rp::gpio::Output; +use embassy_rp::peripherals::SPI1; +use embassy_rp::spi::{Blocking, Spi}; +use embassy_time::{Duration, Instant, Timer, block_for}; +use embedded_graphics::{ + Pixel, + draw_target::DrawTarget, + geometry::{Dimensions, Point as EgPoint, Size}, + pixelcolor::{IntoStorage, Rgb565}, + primitives::Rectangle, +}; +use rsk_ui::font::{self, Role}; +use rsk_ui::theme; + +/// The 1.14" panel's visible area in landscape (MADCTL below swaps the axes). +const PANEL_W: u16 = 240; +/// The panel's height in landscape. +const PANEL_H: u16 = 135; +/// Column (CASET) offset: the 240-px axis is centred in the controller's 320-px +/// GRAM, 40 on each side. A vertical one-pixel bar at the far edge means this is +/// off by one on the *other* axis, not this one. +const COL_OFFSET: u16 = 40; +/// Row (RASET) offset: the 135-px axis fills 52+135+53 = 240 of the GRAM, so one +/// edge has 52 spare rows and the other 53. `53` matches the ALIENTEK demo; if +/// the image is shifted a row, this is the value to change to 52. +const ROW_OFFSET: u16 = 53; +/// MADCTL for landscape with the panel's native RGB order (`MV|MX|ML`), the +/// value ALIENTEK's own ST7789V2 initialisation uses. +const MADCTL: u8 = 0x70; +/// One full panel row of RGB565 pixels. +const ROW_BYTES: usize = PANEL_W as usize * 2; +/// Vertical centre of the subtitle / presence-banner band. +const PROMPT_Y: u16 = 50; +/// Height of that band. Kept clear of the title above (ends at y=37) and the +/// first value line below (starts at y=63), and it is the one band the presence +/// banner and the idle subtitle share, so neither ever leaves the other's pixels. +const PROMPT_BAND: u16 = 24; + +/// The device facts the screen shows. `Copy`, no secrets: the build counter comes +/// from `main` and the chip id is the same serial the USB descriptor publishes. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Info { + pub version: u16, + pub chipid: u64, +} + +/// A rendered frame's inputs, so the loop repaints only on a change. +#[derive(Clone, Copy, PartialEq, Eq)] +struct Status { + version: u16, + chipid: u64, + usb: bool, + uptime_s: u32, +} + +/// The write-only ST7789 over hardware SPI. Blocking writes are fine here: the +/// screen repaints at most once a second, and a full frame is ~13 ms at 40 MHz. +pub struct Panel { + spi: Spi<'static, SPI1, Blocking>, + cs: Output<'static>, + dc: Output<'static>, + /// The backlight, driven and held at its ON level for the device's lifetime. + /// Dropping an embassy `Output` disconnects the pad, so the field owns it. + bl: Output<'static>, +} + +impl Panel { + /// Wake, initialise and paint the first frame, then light the backlight. The + /// panel stays dark through init (the GPIO starts low), so there is no white + /// flash before the first paint. + pub fn new( + spi: Spi<'static, SPI1, Blocking>, + cs: Output<'static>, + dc: Output<'static>, + bl: Output<'static>, + info: Info, + ) -> Self { + let mut panel = Self { spi, cs, dc, bl }; + panel.init(); + panel.paint( + &Status { + version: info.version, + chipid: info.chipid, + usb: false, + uptime_s: 0, + }, + None, + ); + // The reference board switches the backlight with a PNP high-side + // transistor (S8550: emitter to 3V3, base pulled up by R8, base driven + // through R10 from this pin), so a LOW base is what lights it — `Level::High` + // holds it dark through init and this releases it once the first frame is up, + // so there is no white flash. + panel.bl.set_low(); + panel + } + + fn command(&mut self, cmd: u8, params: &[u8]) { + self.cs.set_low(); + self.dc.set_low(); + let _ = self.spi.blocking_write(&[cmd]); + if !params.is_empty() { + self.dc.set_high(); + let _ = self.spi.blocking_write(params); + } + self.cs.set_high(); + } + + /// Open a RAM-write window in panel coordinates, adding the GRAM offsets. The + /// caller streams exactly `w * h` pixels and then calls [`Self::end_window`]. + fn begin_window(&mut self, x: u16, y: u16, w: u16, h: u16) { + let x0 = x + COL_OFFSET; + let x1 = x0 + w - 1; + let y0 = y + ROW_OFFSET; + let y1 = y0 + h - 1; + self.cs.set_low(); + self.dc.set_low(); + let _ = self.spi.blocking_write(&[0x2A]); + self.dc.set_high(); + let _ = self + .spi + .blocking_write(&[(x0 >> 8) as u8, x0 as u8, (x1 >> 8) as u8, x1 as u8]); + self.dc.set_low(); + let _ = self.spi.blocking_write(&[0x2B]); + self.dc.set_high(); + let _ = self + .spi + .blocking_write(&[(y0 >> 8) as u8, y0 as u8, (y1 >> 8) as u8, y1 as u8]); + self.dc.set_low(); + let _ = self.spi.blocking_write(&[0x2C]); + self.dc.set_high(); + } + + fn end_window(&mut self) { + self.cs.set_high(); + } + + /// Write one horizontal run of RGB565 pixels as a single one-row window. + fn write_run(&mut self, start: EgPoint, used: usize, row: &[u8]) { + if used == 0 { + return; + } + self.begin_window(start.x as u16, start.y as u16, (used / 2) as u16, 1); + let _ = self.spi.blocking_write(&row[..used]); + self.end_window(); + } + + /// The ALIENTEK RP2350A panel's ST7789V2 sequence, verbatim (there is no GPIO + /// reset on that board, so a software reset stands in), with inversion on. + fn init(&mut self) { + block_for(Duration::from_millis(120)); + self.command(0x01, &[]); // software reset + block_for(Duration::from_millis(150)); + self.command(0x11, &[]); // sleep out + block_for(Duration::from_millis(120)); + self.command(0x36, &[MADCTL]); + self.command(0x3A, &[0x05]); // 16-bit RGB565 + self.command(0xB2, &[0x0C, 0x0C, 0x00, 0x33, 0x33]); // porch + self.command(0xB7, &[0x35]); // gate control + self.command(0xBB, &[0x19]); // VCOM + self.command(0xC0, &[0x2C]); // power control 1 + self.command(0xC2, &[0x01]); // power control 2 + self.command(0xC3, &[0x12]); // power control 3 + self.command(0xC4, &[0x20]); // power control 4 + self.command(0xC6, &[0x01]); // VCOM control + self.command(0xD0, &[0xA4, 0xA1]); // power control 5 + self.command( + 0xE0, + &[ + 0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23, + ], + ); // gamma + + self.command( + 0xE1, + &[ + 0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23, + ], + ); // gamma - + self.command(0x21, &[]); // inversion on + self.command(0x29, &[]); // display on + block_for(Duration::from_millis(20)); + } + + /// Clear the whole panel to `color`. + fn clear(&mut self, color: Rgb565) -> Result<(), Infallible> { + self.fill_solid( + &Rectangle::new(EgPoint::zero(), Size::new(PANEL_W.into(), PANEL_H.into())), + color, + ) + } + + /// Repaint one text line. Clearing the line's own row band first is what makes + /// a redraw idempotent: `font::left` paints only the glyphs it places, so a + /// shorter string would otherwise leave the tail of the old one behind. + fn line(&mut self, text: &str, y: u16, band: u16, role: Role, color: Rgb565) { + let top = y - band / 2; + let _ = self.fill_solid( + &Rectangle::new( + EgPoint::new(0, top as i32), + Size::new(PANEL_W.into(), band.into()), + ), + theme::BG, + ); + let _ = font::left( + self, + text, + EgPoint::new(12, y as i32), + role, + color, + theme::BG, + ); + } + + /// The subtitle / presence banner. One band, two faces: the idle caption, or — + /// while the device is waiting for a button press — a filled bar asking for it. + /// + /// The banner says only *that* a press is wanted. It never renders anything from + /// the applet's `Confirm`, so a relying party can't be painted here, and this + /// panel stays what it is: an indicator in the LED's place, not a consent surface. + fn prompt(&mut self, on: bool) { + let (fill, text, role, fg) = if on { + (theme::WARN, "PRESS THE KEY", Role::BodyStrong, theme::BG) + } else { + (theme::BG, "status panel", Role::MonoSmall, theme::MUTED) + }; + let top = PROMPT_Y - PROMPT_BAND / 2; + let _ = self.fill_solid( + &Rectangle::new( + EgPoint::new(0, top as i32), + Size::new(PANEL_W.into(), PROMPT_BAND.into()), + ), + fill, + ); + let _ = font::left( + self, + text, + EgPoint::new(12, PROMPT_Y as i32), + role, + fg, + fill, + ); + } + + /// Repaint the screen for `st`, touching only the lines whose text changed. The + /// idle key changes one field a second (the uptime), and rewriting one 240×14 + /// band is invisible where clearing the whole panel first reads as a blink. + fn paint(&mut self, st: &Status, prev: Option<&Status>) { + let full = prev.is_none_or(|p| p.version != st.version || p.chipid != st.chipid); + if full { + let _ = self.clear(theme::BG); + self.line("RS-Key", 26, 22, Role::Heading, theme::TEXT); + self.prompt(false); + let mut line = Line::new(); + self.line( + line.push_str("FW 0x") + .push_hex(u64::from(st.version), 4) + .as_str(), + 70, + 14, + Role::Mono, + theme::TEXT_2, + ); + let mut line = Line::new(); + self.line( + line.push_str("Chip ").push_hex(st.chipid, 16).as_str(), + 86, + 14, + Role::Mono, + theme::TEXT_2, + ); + } + if full || prev.is_none_or(|p| p.usb != st.usb) { + let (usb, color) = if st.usb { + ("USB connected", theme::SUCCESS) + } else { + ("USB idle", theme::MUTED) + }; + self.line(usb, 110, 16, Role::BodyStrong, color); + } + if full || prev.is_none_or(|p| p.uptime_s != st.uptime_s) { + let mut line = Line::new(); + self.line( + line.push_str("Up ") + .push_u32(st.uptime_s) + .push_str("s") + .as_str(), + 126, + 14, + Role::MonoSmall, + theme::FAINT, + ); + } + } +} + +impl Dimensions for Panel { + fn bounding_box(&self) -> Rectangle { + Rectangle::new(EgPoint::zero(), Size::new(PANEL_W.into(), PANEL_H.into())) + } +} + +impl DrawTarget for Panel { + type Color = Rgb565; + type Error = Infallible; + + /// Write pixel runs: the renderer's text path uses `fill_contiguous`, but a + /// primitive that draws pixel-by-pixel lands here, so merge horizontal runs + /// into single windows rather than opening one per pixel. + fn draw_iter>>( + &mut self, + pixels: I, + ) -> Result<(), Self::Error> { + let mut row = [0u8; ROW_BYTES]; + let mut start = EgPoint::zero(); + let mut previous = EgPoint::new(-2, -2); + let mut used = 0usize; + for Pixel(point, color) in pixels { + if point.x < 0 + || point.y < 0 + || point.x >= i32::from(PANEL_W) + || point.y >= i32::from(PANEL_H) + { + continue; + } + if used != 0 && (point.y != previous.y || point.x != previous.x + 1) { + self.write_run(start, used, &row); + used = 0; + } + if used == 0 { + start = point; + } + let bytes = color.into_storage().to_be_bytes(); + row[used] = bytes[0]; + row[used + 1] = bytes[1]; + used += 2; + previous = point; + } + self.write_run(start, used, &row); + Ok(()) + } + + /// Blit a rectangle of colours, one window per visible row. The source is + /// walked in `area` row-major order (the embedded-graphics contract) so a + /// clipped area still consumes its colours in step. + fn fill_contiguous>( + &mut self, + area: &Rectangle, + colors: I, + ) -> Result<(), Self::Error> { + let left = area.top_left.x.max(0); + let top = area.top_left.y.max(0); + let right = (area.top_left.x + area.size.width as i32).min(i32::from(PANEL_W)); + let bottom = (area.top_left.y + area.size.height as i32).min(i32::from(PANEL_H)); + if left >= right || top >= bottom { + return Ok(()); + } + let mut source = colors.into_iter(); + let mut row = [0u8; ROW_BYTES]; + for ay in 0..area.size.height as i32 { + let y = area.top_left.y + ay; + let mut used = 0usize; + for ax in 0..area.size.width as i32 { + let Some(color) = source.next() else { break }; + let x = area.top_left.x + ax; + if x < left || x >= right || y < top || y >= bottom { + continue; + } + let bytes = color.into_storage().to_be_bytes(); + row[used] = bytes[0]; + row[used + 1] = bytes[1]; + used += 2; + } + if used != 0 { + self.begin_window(left as u16, y as u16, (used / 2) as u16, 1); + let _ = self.spi.blocking_write(&row[..used]); + self.end_window(); + } + } + Ok(()) + } + + /// Fill a solid rectangle: one window, then the same row streamed per line. + fn fill_solid(&mut self, area: &Rectangle, color: Rgb565) -> Result<(), Self::Error> { + let left = area.top_left.x.max(0); + let top = area.top_left.y.max(0); + let right = (area.top_left.x + area.size.width as i32).min(i32::from(PANEL_W)); + let bottom = (area.top_left.y + area.size.height as i32).min(i32::from(PANEL_H)); + if left >= right || top >= bottom { + return Ok(()); + } + let bytes = color.into_storage().to_be_bytes(); + let mut row = [0u8; ROW_BYTES]; + for pixel in row.as_chunks_mut::<2>().0 { + pixel.copy_from_slice(&bytes); + } + let width_bytes = (right - left) as usize * 2; + self.begin_window( + left as u16, + top as u16, + (right - left) as u16, + (bottom - top) as u16, + ); + for _ in 0..(bottom - top) { + let _ = self.spi.blocking_write(&row[..width_bytes]); + } + self.end_window(); + Ok(()) + } +} + +/// The ambient status screen. Repaints on any change, and only the line(s) that +/// changed: an idle key rewrites one 240×14 band a second for the uptime, so a +/// later value never reads as a blanked-then-redrawn screen. +#[embassy_executor::task] +pub async fn status_task(panel: &'static RefCell, info: Info) { + let mut last: Option = None; + loop { + let status = Status { + version: info.version, + chipid: info.chipid, + usb: crate::usb_attach::elapsed_ms() > 0, + uptime_s: (Instant::now().as_millis() / 1000) as u32, + }; + if last.as_ref() != Some(&status) { + panel.borrow_mut().paint(&status, last.as_ref()); + last = Some(status); + } + Timer::after_millis(500).await; + } +} + +/// The panel's presence backend: the button the key already uses, with a banner on +/// the screen for as long as the wait lasts. +/// +/// The banner is drawn **from inside the wait**, not from [`status_task`], because +/// the wait blocks the thread executor: the status task cannot run while a press is +/// being asked for, so a prompt driven from the ambient loop would only ever appear +/// after the press it was asking for. Drawing here costs nothing else — the panel is +/// a status indicator, it shows no operation and no relying party, and +/// `shows_confirm` keeps its `false` default, so CTAP semantics (including the +/// `authenticatorReset` power-up window exemption) stay exactly a button key's. +pub struct PanelPresence { + panel: &'static RefCell, + inner: crate::presence::ButtonPresence, +} + +impl PanelPresence { + pub fn new(panel: &'static RefCell, inner: crate::presence::ButtonPresence) -> Self { + Self { panel, inner } + } + + /// One non-blocking sample, for the worker's typed-ticket button watcher. + pub fn poll_pressed(&mut self) -> bool { + self.inner.poll_pressed() + } +} + +impl rsk_sdk::UserPresence for PanelPresence { + fn request(&mut self, confirm: rsk_sdk::Confirm<'_>) -> rsk_sdk::Presence { + self.panel.borrow_mut().prompt(true); + let result = self.inner.request(confirm); + self.panel.borrow_mut().prompt(false); + result + } + + fn request_ceremony(&mut self, confirm: rsk_sdk::Confirm<'_>) -> rsk_sdk::Presence { + self.panel.borrow_mut().prompt(true); + let result = self.inner.request_ceremony(confirm); + self.panel.borrow_mut().prompt(false); + result + } +} + +/// A fixed-capacity text line, so the screen composes strings without a heap. +struct Line { + buf: [u8; 40], + len: usize, +} + +impl Line { + fn new() -> Self { + Self { + buf: [0; 40], + len: 0, + } + } + + fn push_str(&mut self, s: &str) -> &mut Self { + let bytes = s.as_bytes(); + let n = bytes.len().min(self.buf.len() - self.len); + self.buf[self.len..self.len + n].copy_from_slice(&bytes[..n]); + self.len += n; + self + } + + fn push_hex(&mut self, value: u64, digits: usize) -> &mut Self { + for i in (0..digits).rev() { + let nibble = ((value >> (i * 4)) & 0xF) as u8; + let ch = if nibble < 10 { + b'0' + nibble + } else { + b'A' + nibble - 10 + }; + if self.len < self.buf.len() { + self.buf[self.len] = ch; + self.len += 1; + } + } + self + } + + fn push_u32(&mut self, mut value: u32) -> &mut Self { + let mut digits = [0u8; 10]; + let mut n = 0; + loop { + digits[n] = b'0' + (value % 10) as u8; + value /= 10; + n += 1; + if value == 0 { + break; + } + } + for i in (0..n).rev() { + if self.len < self.buf.len() { + self.buf[self.len] = digits[i]; + self.len += 1; + } + } + self + } + + fn as_str(&self) -> &str { + core::str::from_utf8(&self.buf[..self.len]).unwrap_or("?") + } +} diff --git a/firmware/src/presence.rs b/firmware/src/presence.rs index b515ba4f9..dfa4b8397 100644 --- a/firmware/src/presence.rs +++ b/firmware/src/presence.rs @@ -39,6 +39,10 @@ use rsk_device::presence::{Board, Outcome}; use rsk_device::presence::Arbiter; +// Read by `main`'s phy-publishing block (compiled out on `LED_KIND=none`) and by +// the display assert below, so a `none` build without a screen — the `panel` flavor +// — uses it nowhere and would otherwise carry an unused re-export. +#[cfg(any(feature = "display", not(led_kind = "none")))] pub(crate) use rsk_device::presence::MIN_TIMEOUT_SECS; pub use rsk_device::presence::{SCOPE_CCID, SCOPE_FIDO, SCOPE_NONE, SCOPE_OTP}; @@ -156,11 +160,14 @@ impl Board for Button { /// The presence backend the [`crate::worker::Worker`] owns, selected at build /// time so the worker wiring stays backend-agnostic. The standard key confirms -/// with the BOOTSEL button (or a `PRESENCE_PIN` GPIO); the `display` build swaps -/// this alias to the `crate::display::TouchPresence` that renders on-screen -/// Approve/Deny and returns a real `Declined`. Both satisfy the one +/// with the BOOTSEL button (or a `PRESENCE_PIN` GPIO); the `panel` build wraps +/// that same button so the screen can show a press is wanted; the `display` build +/// swaps this alias to the `crate::display::TouchPresence` that renders on-screen +/// Approve/Deny and returns a real `Declined`. All satisfy the one /// `rsk_sdk::UserPresence` every applet asks through, so only this alias changes. -#[cfg(not(feature = "display"))] +#[cfg(all(not(feature = "display"), feature = "panel"))] +pub type Presence = crate::panel::PanelPresence; +#[cfg(all(not(feature = "display"), not(feature = "panel")))] pub type Presence = ButtonPresence; #[cfg(feature = "display")] pub type Presence = crate::display::TouchPresence; diff --git a/formal/README.md b/formal/README.md index 65cc080bc..3a564e12b 100644 --- a/formal/README.md +++ b/formal/README.md @@ -1382,7 +1382,7 @@ obligation that says so where a gate can read it. The display **build** is not modelled either — `presence.shows_confirm()` stays FALSE, so the reset window still applies where a display build bypasses it (`reset.rs:37`), and `ButtonWait`'s `spent` latch stays where that build -compiles it out (`firmware/src/presence.rs:99-106`) in favour of the panel's own +compiles it out (`firmware/src/presence.rs:103-110`) in favour of the panel's own release debounce. And `OpenWaitFor` now stands for **two** different stale-cancel drops — `ButtonWait::wait`'s and the display's own (`crates/rsk-display/src/presence.rs:45-48`) — so @@ -1933,11 +1933,11 @@ it off the medium. `EF_HARDENED` says the lap has run and writes the marker only after `compact()` returns Ok (`crates/rsk-fs/src/lib.rs:80-98`) — marker AFTER scrub, the same write-order family as the store's delete and the PIN flows' revoke. The boot glue keeps only -the OTP gate and the placement of the stall (`firmware/src/main.rs:631-647`). +the OTP gate and the placement of the stall (`firmware/src/main.rs:680-696`). Every *lazy* re-key **or delete** after the lap must re-arm it — a tombstone appends too: **run-35 found four of five re-key sites skipping exactly that**, and the swept sites are the module's citations. *After the lap* means after any -lap this device ever ran, the boot pass at `firmware/src/main.rs:623-630` +lap this device ever ran, the boot pass at `firmware/src/main.rs:672-679` included: the marker latches once, so a boot that skipped a record — a faulted `read_key`, a refused `put` — leaves it standing over the boot that finally migrates that record. Those six arms re-arm for that reason, not because they @@ -3008,7 +3008,7 @@ it does not promote MODELLED-ONLY to a proof or turn bounded Kani into PROVEN. behaviour than the firmware, "which is sound for safety". That was false**, and the one that broke it was holding the green run up: `PowerCut` left the seed as the cut found it, while the firmware regenerates a missing seed on **every** -boot (`firmware/src/main.rs:629`, `tools/emu/src/device.rs:507`). A cut device +boot (`firmware/src/main.rs:678`, `tools/emu/src/device.rs:507`). A cut device was permanently seedless in the model and could never hold a usable credential again — the model was *narrower* than the code, which is the one direction a safety argument cannot absorb. It is fixed (`BootEnsuresSeed`), and every @@ -3034,7 +3034,7 @@ abstractions producing traces the firmware cannot follow. - **Any boot may mint the grant record, or not.** `BootEnsuresSeed` leaves `gate.ppuatRec` either way. `ensure_seed` skips the mint on a vendor-soft-locked key (`seed.rs:640`), and mints nothing when a step before it fails or the record - cannot be read or opened — an error `firmware/src/main.rs:629` drops. The trace + cannot be read or opened — an error `firmware/src/main.rs:678` drops. The trace mapper pins the mint it predicts for the unlocked emulator, so R4a still holds the recording to one branch. - **A regenerated seed still opens the credentials made under the old one.** diff --git a/formal/RSKeySecurityState.tla b/formal/RSKeySecurityState.tla index 5916607a1..3658ddc43 100644 --- a/formal/RSKeySecurityState.tla +++ b/formal/RSKeySecurityState.tla @@ -472,7 +472,7 @@ TouchTimeout == \* \* NARROWER than a display build in one way, stated because it is the risk \* direction: that build compiles ButtonWait out entirely -\* (firmware/src/presence.rs:99-106) and the panel's own release debounce takes +\* (firmware/src/presence.rs:103-110) and the panel's own release debounce takes \* over the `spent` latch, so the model keeps a defence the display build \* implements somewhere else rather than one it does not have. \* THE FOURTH SITE OF THE SAME RULE, and it was the one still written as a bare @@ -1516,7 +1516,7 @@ VolatileCleared == /\ upSpent' = FALSE \* EVERY boot runs ensure_seed, not just the one at the end of a reset: -\* firmware/src/main.rs:629 and tools/emu/src/device.rs:507. A cut that stranded +\* firmware/src/main.rs:678 and tools/emu/src/device.rs:507. A cut that stranded \* the device mid-wipe therefore comes back WITH a seed and can hold usable \* credentials again. Leaving it out made the model less permissive than the \* firmware -- the one direction a safety argument cannot absorb. diff --git a/formal/citations.lock b/formal/citations.lock index 116e53291..c0f5afc3d 100644 --- a/formal/citations.lock +++ b/formal/citations.lock @@ -28,11 +28,11 @@ assurance/board/PLAT-TIMER-002.toml crates/rsk-usb/src/ctaphid.rs 775-775 Timer: assurance/board/PLAT-TIMER-002.toml firmware/src/led.rs 618-618 Timer::after_millis(5).await; Timer::after_millis(5).await; assurance/board/PLAT-TIMER-002.toml firmware/src/led.rs 692-692 const PERIOD_US: u64 = 2000; const PERIOD_US: u64 = 2000; assurance/board/PLAT-TIMER-002.toml firmware/src/led.rs 727-727 Timer::after_millis(5).await; Timer::after_millis(5).await; -assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 524-524 let mut config = embassy_rp::config::Config::default(); let mut config = embassy_rp::config::Config::default(); -assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 960-960 hp.spawn(led::gpio_task(led).unwrap()); hp.spawn(led::gpio_task(led).unwrap()); -assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 968-968 hp.spawn(led::pimoroni_task(rg, b).unwrap()); hp.spawn(led::pimoroni_task(rg, b).unwrap()); -assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 1022-1022 hp.spawn(led::ws2812_task(ws2812).unwrap()); hp.spawn(led::ws2812_task(ws2812).unwrap()); -assurance/board/PLAT-TIMER-002.toml firmware/src/presence.rs 237-240 #[cfg(feature = "no-touch")] } +assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 573-573 let mut config = embassy_rp::config::Config::default(); let mut config = embassy_rp::config::Config::default(); +assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 1009-1009 hp.spawn(led::gpio_task(led).unwrap()); hp.spawn(led::gpio_task(led).unwrap()); +assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 1017-1017 hp.spawn(led::pimoroni_task(rg, b).unwrap()); hp.spawn(led::pimoroni_task(rg, b).unwrap()); +assurance/board/PLAT-TIMER-002.toml firmware/src/main.rs 1071-1071 hp.spawn(led::ws2812_task(ws2812).unwrap()); hp.spawn(led::ws2812_task(ws2812).unwrap()); +assurance/board/PLAT-TIMER-002.toml firmware/src/presence.rs 244-247 #[cfg(feature = "no-touch")] } assurance/board/PLAT-TIMER-002.toml firmware/src/usb_attach.rs 34-36 /// Milliseconds since the attach — the clock the FIDO layer gets as `Ctx::now_ms`, pub fn elapsed_ms() -> u64 { assurance/board/PLAT-TIMER-002.toml firmware/src/worker.rs 307-307 const BTN_POLL_MS: u64 = 16; const BTN_POLL_MS: u64 = 16; assurance/board/PLAT-TIMER-002.toml firmware/src/worker.rs 469-469 crate::usb_attach::elapsed_ms(), crate::usb_attach::elapsed_ms(), @@ -40,14 +40,14 @@ assurance/board/PLAT-TIMER-002.toml scripts/platform_gate.py 1337-1337 OUTCOME_S assurance/board/PLAT-TIMER-002.toml tools/rsk/audit.py 170-170 print(f"{'seq':>6} {'uptime':>10} {'event':<18} aux detail") print(f"{'seq':>6} {'uptime':>10} {'event':<18} aux detail") assurance/board/PLAT-TIMER-003.toml crates/rsk-fido/src/journal.rs 215-217 if !ctx.state.audit_boot_logged { let _ = raw_append(&ctx.dev, ctx.fs, ctx.now_ms, EV_BOOT, 0, &[]); assurance/board/PLAT-TIMER-003.toml crates/rsk-fido/src/vendor.rs 481-481 journal::vendor_read(ctx, out) journal::vendor_read(ctx, out) -assurance/board/PLAT-TIMER-003.toml firmware/src/main.rs 524-524 let mut config = embassy_rp::config::Config::default(); let mut config = embassy_rp::config::Config::default(); -assurance/board/PLAT-TIMER-003.toml firmware/src/main.rs 800-800 usb_attach::mark(); usb_attach::mark(); +assurance/board/PLAT-TIMER-003.toml firmware/src/main.rs 573-573 let mut config = embassy_rp::config::Config::default(); let mut config = embassy_rp::config::Config::default(); +assurance/board/PLAT-TIMER-003.toml firmware/src/main.rs 849-849 usb_attach::mark(); usb_attach::mark(); assurance/board/PLAT-TIMER-003.toml firmware/src/pin_lock.rs 54-54 rp_pac::WATCHDOG.scratch2().write_value(encode(lock)); rp_pac::WATCHDOG.scratch2().write_value(encode(lock)); assurance/board/PLAT-TIMER-003.toml firmware/src/usb_attach.rs 34-36 /// Milliseconds since the attach — the clock the FIDO layer gets as `Ctx::now_ms`, pub fn elapsed_ms() -> u64 { assurance/board/PLAT-TIMER-003.toml scripts/platform_gate.py 1337-1337 OUTCOME_STATUS = {"pass": "discharged", "fail": "refuted"} OUTCOME_STATUS = {"pass": "discharged", "fail": "refuted"} assurance/board/PLAT-TIMER-003.toml tools/rsk/audit.py 170-170 print(f"{'seq':>6} {'uptime':>10} {'event':<18} aux detail") print(f"{'seq':>6} {'uptime':>10} {'event':<18} aux detail") -assurance/board/PLAT-TRNG-002.toml firmware/src/main.rs 609-609 trng_cfg.sample_count = 1000; trng_cfg.sample_count = 1000; -assurance/board/PLAT-TRNG-002.toml firmware/src/main.rs 610-610 let trng = Trng::new(p.TRNG, Irqs, trng_cfg); let trng = Trng::new(p.TRNG, Irqs, trng_cfg); +assurance/board/PLAT-TRNG-002.toml firmware/src/main.rs 658-658 trng_cfg.sample_count = 1000; trng_cfg.sample_count = 1000; +assurance/board/PLAT-TRNG-002.toml firmware/src/main.rs 659-659 let trng = Trng::new(p.TRNG, Irqs, trng_cfg); let trng = Trng::new(p.TRNG, Irqs, trng_cfg); assurance/board/PLAT-TRNG-002.toml scripts/platform_gate.py 1337-1337 OUTCOME_STATUS = {"pass": "discharged", "fail": "refuted"} OUTCOME_STATUS = {"pass": "discharged", "fail": "refuted"} assurance/bundle/SEC-FIDO-001.toml crates/rsk-fido/src/clientpin.rs 724-724 /// Refines `RSKeySecurityState!NoAuthorizationBypass` — SEC-FIDO-001. /// Refines `RSKeySecurityState!NoAuthorizationBypass` — SEC-FIDO-001. assurance/bundle/SEC-FIDO-001.toml crates/rsk-fido/src/state.rs 172-172 /// Refines `RSKeySecurityState!NoAuthorizationBypass` — SEC-FIDO-001. /// Refines `RSKeySecurityState!NoAuthorizationBypass` — SEC-FIDO-001. @@ -75,9 +75,9 @@ assurance/bundle/SEC-FIDO-002.toml crates/rsk-display/src/presence.rs 45-48 let assurance/bundle/SEC-FIDO-002.toml crates/rsk-usb/src/ctaphid.rs 88-92 pub fn is_cancel_frame(frame: &[u8; HID_RPT_SIZE], n: usize, cid: u32) -> bool { } assurance/bundle/SEC-FIDO-002.toml crates/rsk-usb/src/ctaphid.rs 762-762 }); }); assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 19-19 //! Kani harnesses for `NoCrossTransportTouchConsumption`. What stays here is the //! Kani harnesses for `NoCrossTransportTouchConsumption`. What stays here is the -assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 99-101 /// User presence via BOOTSEL (default) or a dedicated GPIO button. pub struct ButtonPresence { -assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 202-258 pub fn poll_pressed(&mut self) -> bool { } -assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 263-269 #[cfg(feature = "display")] ARBITER.set_cancel_requested(requested); +assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 103-105 /// User presence via BOOTSEL (default) or a dedicated GPIO button. pub struct ButtonPresence { +assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 209-265 pub fn poll_pressed(&mut self) -> bool { } +assurance/bundle/SEC-FIDO-002.toml firmware/src/presence.rs 270-276 #[cfg(feature = "display")] ARBITER.set_cancel_requested(requested); assurance/bundle/SEC-FIDO-002.toml firmware/src/worker.rs 434-434 crate::presence::set_wait_scope(match ex.kind { crate::presence::set_wait_scope(match ex.kind { assurance/bundle/SEC-FIDO-002.toml formal/gen-configs.sh 105-107 ALL_INV=(NoAuthorizationBypass NoCrossTransportTouchConsumption NoUnmanageableCredential ResetNeverWeakensSurvivingState) assurance/bundle/SEC-FIDO-002.toml formal/run-tlc.sh 16-16 # (sha256 936a2620...), so `floors.txt` still describes the TLC that measured it. # (sha256 936a2620...), so `floors.txt` still describes the TLC that measured it. @@ -519,20 +519,20 @@ assurance/platform.toml firmware/src/led.rs 692-692 const PERIOD_US: u64 = 2000; assurance/platform.toml firmware/src/led.rs 695-695 let c = blinker.tick(); let c = blinker.tick(); assurance/platform.toml firmware/src/led.rs 719-719 let c = blinker.tick(); let c = blinker.tick(); assurance/platform.toml firmware/src/led.rs 727-727 Timer::after_millis(5).await; Timer::after_millis(5).await; -assurance/platform.toml firmware/src/main.rs 524-524 let mut config = embassy_rp::config::Config::default(); let mut config = embassy_rp::config::Config::default(); -assurance/platform.toml firmware/src/main.rs 555-555 fs.scan(); // recover dynamic files (counter, resident creds) from flash fs.scan(); // recover dynamic files (counter, resident creds) from flash -assurance/platform.toml firmware/src/main.rs 601-601 let mut trng_cfg = TrngConfig::default(); let mut trng_cfg = TrngConfig::default(); -assurance/platform.toml firmware/src/main.rs 602-608 // The default sample_count (25) is too low for some RP2350 ROSC units: the // unchanged — the NIST health checks stay enabled, the source is unchanged. -assurance/platform.toml firmware/src/main.rs 609-609 trng_cfg.sample_count = 1000; trng_cfg.sample_count = 1000; -assurance/platform.toml firmware/src/main.rs 610-610 let trng = Trng::new(p.TRNG, Irqs, trng_cfg); let trng = Trng::new(p.TRNG, Irqs, trng_cfg); -assurance/platform.toml firmware/src/main.rs 960-960 hp.spawn(led::gpio_task(led).unwrap()); hp.spawn(led::gpio_task(led).unwrap()); -assurance/platform.toml firmware/src/main.rs 968-968 hp.spawn(led::pimoroni_task(rg, b).unwrap()); hp.spawn(led::pimoroni_task(rg, b).unwrap()); -assurance/platform.toml firmware/src/main.rs 1022-1022 hp.spawn(led::ws2812_task(ws2812).unwrap()); hp.spawn(led::ws2812_task(ws2812).unwrap()); -assurance/platform.toml firmware/src/presence.rs 99-101 /// User presence via BOOTSEL (default) or a dedicated GPIO button. pub struct ButtonPresence { -assurance/platform.toml firmware/src/presence.rs 137-137 const _: () = assert!(embassy_time::TICK_HZ == 1_000_000); const _: () = assert!(embassy_time::TICK_HZ == 1_000_000); -assurance/platform.toml firmware/src/presence.rs 163-166 #[cfg(not(feature = "display"))] pub type Presence = crate::display::TouchPresence; -assurance/platform.toml firmware/src/presence.rs 202-258 pub fn poll_pressed(&mut self) -> bool { } -assurance/platform.toml firmware/src/presence.rs 269-269 ARBITER.set_cancel_requested(requested); ARBITER.set_cancel_requested(requested); +assurance/platform.toml firmware/src/main.rs 573-573 let mut config = embassy_rp::config::Config::default(); let mut config = embassy_rp::config::Config::default(); +assurance/platform.toml firmware/src/main.rs 604-604 fs.scan(); // recover dynamic files (counter, resident creds) from flash fs.scan(); // recover dynamic files (counter, resident creds) from flash +assurance/platform.toml firmware/src/main.rs 650-650 let mut trng_cfg = TrngConfig::default(); let mut trng_cfg = TrngConfig::default(); +assurance/platform.toml firmware/src/main.rs 651-657 // The default sample_count (25) is too low for some RP2350 ROSC units: the // unchanged — the NIST health checks stay enabled, the source is unchanged. +assurance/platform.toml firmware/src/main.rs 658-658 trng_cfg.sample_count = 1000; trng_cfg.sample_count = 1000; +assurance/platform.toml firmware/src/main.rs 659-659 let trng = Trng::new(p.TRNG, Irqs, trng_cfg); let trng = Trng::new(p.TRNG, Irqs, trng_cfg); +assurance/platform.toml firmware/src/main.rs 1009-1009 hp.spawn(led::gpio_task(led).unwrap()); hp.spawn(led::gpio_task(led).unwrap()); +assurance/platform.toml firmware/src/main.rs 1017-1017 hp.spawn(led::pimoroni_task(rg, b).unwrap()); hp.spawn(led::pimoroni_task(rg, b).unwrap()); +assurance/platform.toml firmware/src/main.rs 1071-1071 hp.spawn(led::ws2812_task(ws2812).unwrap()); hp.spawn(led::ws2812_task(ws2812).unwrap()); +assurance/platform.toml firmware/src/presence.rs 103-105 /// User presence via BOOTSEL (default) or a dedicated GPIO button. pub struct ButtonPresence { +assurance/platform.toml firmware/src/presence.rs 141-141 const _: () = assert!(embassy_time::TICK_HZ == 1_000_000); const _: () = assert!(embassy_time::TICK_HZ == 1_000_000); +assurance/platform.toml firmware/src/presence.rs 167-170 /// `rsk_sdk::UserPresence` every applet asks through, so only this alias changes. #[cfg(all(not(feature = "display"), not(feature = "panel")))] +assurance/platform.toml firmware/src/presence.rs 209-265 pub fn poll_pressed(&mut self) -> bool { } +assurance/platform.toml firmware/src/presence.rs 276-276 ARBITER.set_cancel_requested(requested); ARBITER.set_cancel_requested(requested); assurance/platform.toml firmware/src/usb_attach.rs 8-8 //! before the bus pull-up goes up: the TRNG seed (~1.5 s), the seal migrations, the //! before the bus pull-up goes up: the TRNG seed (~1.5 s), the seal migrations, the assurance/platform.toml firmware/src/worker.rs 307-307 const BTN_POLL_MS: u64 = 16; const BTN_POLL_MS: u64 = 16; assurance/platform.toml firmware/src/worker.rs 397-397 if rsk_mgmt::take_device_reset() && self.ccid.factory_wipe() { if rsk_mgmt::take_device_reset() && self.ccid.factory_wipe() { @@ -810,11 +810,11 @@ formal/README.md crates/rsk-usb/src/ctaphid.rs 584-584 pub async fn run(&mut sel formal/README.md crates/rsk-usb/src/ctaphid.rs 875-887 async fn write_frames(sink: &mut S, cid: u32, cmd: u8, data: &[u8], mut timeout: F) } formal/README.md crates/rsk-usb/src/ctaphid_tests.rs 449-449 fn write_frames_abandons_when_host_stalls() { fn write_frames_abandons_when_host_stalls() { formal/README.md crates/rsk-usb/src/ctaphid_tests.rs 471-471 fn write_frames_writes_every_frame_when_host_drains() { fn write_frames_writes_every_frame_when_host_drains() { -formal/README.md firmware/src/main.rs 623-630 let _ = rsk_fido::seed::migrate_keydev_boot(&dev, &mut fs); let _ = rsk_openpgp::scan_files(&dev, &mut fs, &mut rng); -formal/README.md firmware/src/main.rs 629-629 let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); -formal/README.md firmware/src/main.rs 631-647 // One-shot at-rest hardening: the seal migrations above leave the superseded } +formal/README.md firmware/src/main.rs 672-679 let _ = rsk_fido::seed::migrate_keydev_boot(&dev, &mut fs); let _ = rsk_openpgp::scan_files(&dev, &mut fs, &mut rng); +formal/README.md firmware/src/main.rs 678-678 let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); +formal/README.md firmware/src/main.rs 680-696 // One-shot at-rest hardening: the seal migrations above leave the superseded } formal/README.md firmware/src/pin_lock.rs 18-21 //! a real power cycle clears it. The *whole* lock moves: carrying the engaged flag //! -formal/README.md firmware/src/presence.rs 99-106 /// User presence via BOOTSEL (default) or a dedicated GPIO button. } +formal/README.md firmware/src/presence.rs 103-110 /// User presence via BOOTSEL (default) or a dedicated GPIO button. } formal/README.md firmware/src/worker.rs 417-430 /// One transport (CTAPHID/CCID) request: run the synchronous dispatch and let mut ex = EXCHANGE.lock().await; formal/README.md firmware/src/worker.rs 434-434 crate::presence::set_wait_scope(match ex.kind { crate::presence::set_wait_scope(match ex.kind { formal/README.md firmware/src/worker.rs 521-521 ex.req[..rl].zeroize(); ex.req[..rl].zeroize(); @@ -1044,8 +1044,8 @@ formal/RSKeySecurityState.tla crates/rsk-fido/src/vendor.rs 942-949 // no MSE ha formal/RSKeySecurityState.tla crates/rsk-fido/src/vendor.rs 1018-1024 /// `EF_BACKUP_SEALED` marker so the seed can no longer be exported **or** shown as fs.put(EF_BACKUP_SEALED, &[1]).is_ok() formal/RSKeySecurityState.tla crates/rsk-fs/src/fs.rs 385-388 /// A FID can be yielded MORE THAN ONCE — the log-structured backend walks /// below does). formal/RSKeySecurityState.tla crates/rsk-usb/src/ctaphid.rs 767-772 // frames during fast/crypto processing would consume — and then if up_pending() { -formal/RSKeySecurityState.tla firmware/src/main.rs 629-629 let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); -formal/RSKeySecurityState.tla firmware/src/presence.rs 99-106 /// User presence via BOOTSEL (default) or a dedicated GPIO button. } +formal/RSKeySecurityState.tla firmware/src/main.rs 678-678 let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); let _ = rsk_fido::seed::ensure_seed(&dev, &mut fs, &mut rng); +formal/RSKeySecurityState.tla firmware/src/presence.rs 103-110 /// User presence via BOOTSEL (default) or a dedicated GPIO button. } formal/RSKeySecurityState.tla firmware/src/worker.rs 434-434 crate::presence::set_wait_scope(match ex.kind { crate::presence::set_wait_scope(match ex.kind { formal/RSKeySecurityState.tla firmware/src/worker.rs 526-528 // Nothing is in flight again: a wait started from here on is an on-panel crate::presence::set_wait_scope(crate::presence::SCOPE_NONE); formal/RSKeySecurityState.tla firmware/src/worker.rs 528-528 crate::presence::set_wait_scope(crate::presence::SCOPE_NONE); crate::presence::set_wait_scope(crate::presence::SCOPE_NONE); @@ -1179,8 +1179,8 @@ scripts/test_run_tlc.py scripts/verdict_gate.py 619-634 if row["invariant"] is n scripts/test_security_trace.py tools/emu/src/device.rs 754-755 crate::security_trace::POWER_CYCLE, 0, scripts/test_token_refinement_gate.py crates/rsk-device/src/ctap.rs 122-122 *st = rsk_fido::FidoState::new(); *st = rsk_fido::FidoState::new(); scripts/test_token_refinement_gate.py crates/rsk-fido/src/credential.rs 919-919 let fid = EF_RP + i; let fid = EF_RP + i; -scripts/test_token_refinement_gate.py firmware/src/main.rs 1167-1167 let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); +scripts/test_token_refinement_gate.py firmware/src/main.rs 1262-1262 let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); scripts/token_refinement_gate.py crates/rsk-device/src/ctap.rs 122-122 *st = rsk_fido::FidoState::new(); *st = rsk_fido::FidoState::new(); -scripts/token_refinement_gate.py firmware/src/main.rs 1167-1167 let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); +scripts/token_refinement_gate.py firmware/src/main.rs 1262-1262 let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); let fido_state_ref = FIDO_STATE.init(RefCell::new(rsk_fido::FidoState::new())); scripts/trace_map.py crates/rsk-sdk/src/applet.rs 387-387 let reselect = self.current == Some(i); let reselect = self.current == Some(i); tools/emu/src/hid.rs crates/rsk-usb/src/ctaphid.rs 621-634 async fn on_frame(&mut self, f: &[u8; HID_RPT_SIZE]) { } diff --git a/scripts/check.sh b/scripts/check.sh index 8c539594f..8f4f87aae 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -630,6 +630,13 @@ run "display code absent from default image" sh -c ' if printf "%s\n" "$out" | grep -qE "rsk-ui|rsk-bip39|rsk-slip39|mipidsi"; then echo "FAIL: display code (rsk-ui/rsk-bip39/rsk-slip39/mipidsi) leaked into the default (no-display) firmware image"; exit 1 fi' +# The status-only panel (the `panel` feature) is a second screen build, compiled by +# no other row — like the display it is `dep:`-gated, so lint and build it explicitly. +# `LED_KIND=none` matches the reference board (ALIENTEK RP2350A has no addressable +# LED); the default image's tree check above is still what proves a screenless key +# compiles none of `rsk-ui`. +run "clippy (panel firmware)" env LED_KIND=none cargo clippy -p firmware --features panel -- -D warnings +run "build firmware (panel)" env LED_KIND=none cargo build --release -p firmware --features panel # The test build: no BOOTSEL presence, so the automated suites don't hang on a touch. run "build firmware (test, --features no-touch)" cargo build --release -p firmware --features no-touch # rsk-wipe bakes its erase length AND its LED wiring in at build time, and it is diff --git a/scripts/test_bundle_gate.py b/scripts/test_bundle_gate.py index 8b9d0b469..19c7dd92d 100644 --- a/scripts/test_bundle_gate.py +++ b/scripts/test_bundle_gate.py @@ -983,7 +983,7 @@ def dispose(doc): ("gate_ledger", "walk=4", "walk=5"), ("gate_assumption", "FALSE=91", "FALSE=90"), ("gate_ghost", "24 route(s)", "25 route(s)"), - ("gate_matrix", "1240 cells", "1241 cells")], + ("gate_matrix", "1280 cells", "1281 cells")], ) def test_a_transcribed_gate_line_that_the_gate_does_not_derive(tmp_path, key, old, new): """The second half of the hole `35afe59` named and did not close: editing @@ -1092,10 +1092,10 @@ def test_the_assumption_total_read_is_the_line_s_first(): "key,old,new,says", [("gate_matrix", "(37 covered", "(106 covered", "106 covered"), ("gate_ghost", "21 action(s)", "24 action(s)", "24 action(s) record"), - ("gate_matrix", "954 gap", "106 gap", "106 gap"), + ("gate_matrix", "994 gap", "106 gap", "106 gap"), ("gate_ghost", "11 guard(s)", "21 guard(s)", "21 guard(s)"), ("gate_matrix", "0 conditional", "37 conditional", "37 conditional"), - ("gate_matrix", "40 P0-family", "31 P0-family", "31 P0-family properties")], + ("gate_matrix", "40 P0-family", "32 P0-family", "32 P0-family properties")], ) def test_a_transcribed_unit_count_is_the_one_that_noun_was_written_for( tmp_path, key, old, new, says @@ -1185,10 +1185,10 @@ def restate(doc): @pytest.mark.parametrize( "key,old,new,says", - [("gate_matrix", "31 build configurations", "37 build-configurations", + [("gate_matrix", "32 build configurations", "37 build-configurations", "37 build-configurations"), - ("gate_matrix", "954 gap)", "106 gaps)", "106 gaps"), - ("gate_matrix", "1240 cells", "106 cell", "106 cell"), + ("gate_matrix", "994 gap)", "106 gaps)", "106 gaps"), + ("gate_matrix", "1280 cells", "106 cell", "106 cell"), ("gate_ghost", "11 guard(s)", "24 guards", "24 guards"), ("gate_ghost", "21 action(s) record", "24 actions record", "24 actions")], ) diff --git a/scripts/test_platform_gate.py b/scripts/test_platform_gate.py index 92b88ac45..a59cd05ac 100644 --- a/scripts/test_platform_gate.py +++ b/scripts/test_platform_gate.py @@ -676,7 +676,7 @@ def test_the_checkout_derives_what_it_is_measured_at(): assert len(kinds["unsafe"]) >= 22, kinds["unsafe"] assert all("#" in k for k in kinds["unsafe"]), kinds["unsafe"] steals = [k for k in kinds["unsafe"] if "anypin-steal" in k] - assert len(steals) == 8, steals + assert len(steals) == 11, steals # --- rule 1: every derived candidate is claimed -------------------------------- diff --git a/scripts/test_token_refinement_gate.py b/scripts/test_token_refinement_gate.py index d2f8f8ff3..5decee114 100644 --- a/scripts/test_token_refinement_gate.py +++ b/scripts/test_token_refinement_gate.py @@ -438,7 +438,7 @@ def test_an_unowned_concrete_site_fails(tree: Tree, body: str, finding: str): "fn stray(gna: &RefCell) {\n" " let mut st = gna.borrow_mut();\n *st = Default::default();\n}\n", # CONTROL for the swap clause: BUILDING a fresh state replaces nothing. - # `firmware/src/main.rs:1167` is exactly this, and a rule reading + # `firmware/src/main.rs:1262` is exactly this, and a rule reading # `FidoState::new` anywhere would own the boot that makes the one cell. "fn stray() { let cell = RefCell::new(FidoState::new()); hand(cell); }\n", ], diff --git a/scripts/token_refinement_gate.py b/scripts/token_refinement_gate.py index cd17004a5..e5dae77e5 100644 --- a/scripts/token_refinement_gate.py +++ b/scripts/token_refinement_gate.py @@ -588,7 +588,7 @@ def discovered_volatile( typed = re.compile(WHOLE_TYPED.format(state=state)) if state else None # `mem::replace(st, FidoState::new())` moves the same bits with no `=` in # sight. Scoped to the swap primitives naming the type, NOT to the type - # anywhere: `RefCell::new(FidoState::new())` at `firmware/src/main.rs:1167` + # anywhere: `RefCell::new(FidoState::new())` at `firmware/src/main.rs:1262` # BUILDS the one session cell at boot and replaces nothing, and a rule reading # `FidoState::new` alone owns it. Measured: this alternative adds 0 sites. swapped = re.compile(SWAP.format(state=state)) if state else None