Skip to content

Commit 8beab71

Browse files
authored
Merge pull request #17 from lpiob/feat/custom-presence-pin
feat(presence): add configurable GPIO presence pin
2 parents fbff330 + fca95b9 commit 8beab71

12 files changed

Lines changed: 151 additions & 46 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Full reasoning: [docs/threat-model.md](docs/threat-model.md).
9292

9393
Any RP2350 board with USB. Developed and tested on the **Waveshare RP2350-One**
9494
(WS2812 status LED on GPIO16; boards without an LED run fine). A different flash
95-
size or LED pin is a one-line build knob. Details:
95+
size, LED pin, or presence-button GPIO is a one-line build knob. Details:
9696
[docs/hardware.md](docs/hardware.md).
9797

9898
## Quick start

docs/architecture.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ reader name.
113113

114114
## User presence
115115

116-
One BOOTSEL button, shared by all applets through a `UserPresence` trait the
117-
firmware implements once: FIDO operations, OpenPGP UIF, PIV touch policies,
118-
OATH touch accounts, and OTP slot typing (1–4 presses select the slot) all
119-
gate on it. The no-touch build (`--features no-touch`) auto-confirms — for
120-
test rigs, not for daily use.
116+
One presence button (BOOTSEL by default, or `PRESENCE_PIN`), shared by all
117+
applets through a `UserPresence` trait the firmware implements once: FIDO
118+
operations, OpenPGP UIF, PIV touch policies, OATH touch accounts, and OTP slot
119+
typing (1–4 presses select the slot) all gate on it. The no-touch build
120+
(`--features no-touch`) auto-confirms — for test rigs, not for daily use.
121121

122122
## Provenance
123123

docs/build.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ flowchart TD
2626

2727
| Feature | Default | Effect |
2828
|---|---|---|
29-
| `no-touch` | off | Auto-confirm user presence — FIDO operations (makeCredential, getAssertion, U2F, reset, selection) and OpenPGP UIF data objects no longer require a press of the BOOTSEL button. **Requiring a touch is the default and is not a feature**; `--features no-touch` is the explicit escape hatch for the automated suites (`tests/`, python-fido2, OpenPGP card tests), which cannot press a button and would hang on the (default) touch build. **Never ship a no-touch build.** |
29+
| `no-touch` | off | Auto-confirm user presence — FIDO operations (makeCredential, getAssertion, U2F, reset, selection) and OpenPGP UIF data objects no longer require a press of the presence button (BOOTSEL by default, or `PRESENCE_PIN` when set). **Requiring a touch is the default and is not a feature**; `--features no-touch` is the explicit escape hatch for the automated suites (`tests/`, python-fido2, OpenPGP card tests), which cannot press a button and would hang on the (default) touch build. **Never ship a no-touch build.** |
3030
| `advertise-pqc` | off | Prepends ML-DSA-44 (COSE −48) to the getInfo `algorithms` list. Off by default because released Firefox versions abort the *entire* getInfo parse on an unknown COSE id and report the authenticator broken. **PQC capability is on regardless of this flag** — makeCredential negotiates −48 from the request's `pubKeyCredParams`; the flag only controls advertising. |
3131
| `fips-profile` | off | Bakes a locked FIPS-style policy into the image: ES256K (secp256k1) leaves the FIDO menu, the minimum PIN rises to 6, the vendor seed *export* is refused, and PIV refuses new 3DES management keys and RSA-1024. The default build is unchanged; with secure boot the policy is sealed by your signature. A profile, **not** a FIPS validation — details and rationale: [guides/fips.md](guides/fips.md). |
3232
| `strict-up` | off | Require a touch on **every** `getAssertion`. By default RS-Key honors the platform's silent pre-flight probe (`up:false`): it returns the discovery assertion with no touch and the UP flag clear, so a WebAuthn login with an `allowCredentials` (non-resident) credential is a **single** touch — matching the CTAP2 spec and YubiKey. With `strict-up` the button is polled even for that probe, so such a login asks for **two** touches (one for the probe, one for the real assertion). A deliberately stricter "every assertion needs an explicit gesture" stance for those who want it; it is **not** spec-conformant for `up:false`. Resident-credential / passkey logins are a single touch either way. `fido-conformance` enables this implicitly (the conformance pass was validated with it). |
@@ -42,6 +42,7 @@ flowchart TD
4242
| `XOSC_DELAY_MULT` | `128` | `1..=1024` | Crystal-oscillator startup-delay multiplier ("delayed boot"). A longer settle wait is intended to harden the early-boot clock-switch window against glitch/fault injection. 128 is the embassy default. |
4343
| `FLASH_SIZE` | `4M` | bytes, `0xHEX`, or `<n>K`/`<n>M` | External QSPI flash size. build.rs regenerates `memory.x` from it — the KV store stays a fixed 1.5 MB at the top, the code region is the rest. `4M` reproduces the checked-in layout byte-for-byte. Use this for boards with a different flash chip (e.g. `8M`); must be ≥ ~2 MB and ≤ 16 MB. |
4444
| `LED_PIN` | `16` | `0..=29` | The status-LED GPIO for the `ws2812` and `gpio` backends (RP2350A). Default GPIO16 is the Waveshare RP2350-One. Point it at a free GPIO on boards that use 16 for something else; the indicator simply drives whatever pin you pick. (Unused by `pimoroni`, which has fixed PWM pins, and by `none`.) |
45+
| `PRESENCE_PIN` | `bootsel` | `bootsel` or `0..=29` | User-presence input source. Default `bootsel` keeps the BOOTSEL hardware-button path. Set a GPIO number for a dedicated button (active-low with internal pull-up). Example: `PRESENCE_PIN=0` for a button to ground on GPIO0. |
4546
| `LED_KIND` | `ws2812` | `ws2812` / `gpio` / `pimoroni` / `none` | The LED driver backend, and the **boot default** — a non-`none` build compiles all three so the driver/pin/order are runtime-switchable via `rsk hw` / PicoForge (see below). `ws2812` = a single addressable RGB on `LED_PIN` (the Waveshare default). `gpio` = a plain on/off LED on `LED_PIN` — hue/brightness collapse to lit/unlit, but the blink *pattern* still distinguishes statuses. `pimoroni` = a 3-pin PWM RGB (Pimoroni Tiny 2350: R=GPIO18, G=GPIO19, B=GPIO20, common-anode). `none` = no indicator (the status engine still runs; nothing renders it, and the phy LED fields are ignored). |
4647
| `LED_ORDER` | `rgb` | `rgb` / `grb` | WS2812 wire byte order (`ws2812` backend only). The Waveshare RP2350-One is unusually `rgb` (the default); standard WS2812B parts (e.g. the TenStar RP2350-USB) are `grb`. If a `ws2812` board comes up with red and green swapped (blue fine), flip this to `grb`. |
4748
| `MAX_LEDS` | `8` | `1``64` | PIO state-machine and frame-buffer ceiling for addressable LEDs. The **actual** connected count is set at runtime via `rsk hw --led-num` and must be ≤ `MAX_LEDS`. Default 8 covers the common 1–8 range; boards with more (up to 64) override this.|
@@ -157,7 +158,7 @@ Two caveats:
157158
{ name = "fw"; vidpid = "Nitro3"; fwVersion = "2.0.0"; }'
158159
```
159160
Knobs: `vidpid`, `usbVid`, `usbPid`, `fwVersion`, `xoscDelayMult`, `flashSize`,
160-
`ledPin`, `fakeMkek`, `fakeDevk` (mirroring the env vars above). As a convenience each also falls
161+
`ledPin`, `presencePin`, `fakeMkek`, `fakeDevk` (mirroring the env vars above). As a convenience each also falls
161162
back to the like-named env var, so `VIDPID=Pico nix build --impure .#firmware`
162163
works for a quick throwaway — but the declarative arg is the reproducible path
163164
and needs no `--impure`.

docs/hardware.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ cable are enough.
2121

2222
## Defaults and the knobs to change them
2323

24-
The default build targets a 4 MB flash chip with the LED on GPIO16 and assumes a
25-
standard 12 MHz crystal. For a different board, two compile-time knobs usually
26-
cover it:
24+
The default build targets a 4 MB flash chip with the LED on GPIO16, uses BOOTSEL
25+
for user presence, and assumes a standard 12 MHz crystal. For a different board,
26+
three compile-time knobs usually cover it:
2727

2828
| Knob | Default | When to change it |
2929
|---|---|---|
3030
| `FLASH_SIZE` | `4M` | A board with a different QSPI flash chip (e.g. `8M`). `build.rs` regenerates `memory.x` from it. Must be ≥ ~2 MB and ≤ 16 MB. |
3131
| `LED_PIN` | `16` | A board that uses GPIO16 for something else, or wires its addressable LED elsewhere (RP2350A: GPIO `0..=29`). |
32+
| `PRESENCE_PIN` | `bootsel` | A board with a dedicated user-presence button on a GPIO. Set a pin number (`0..=29`) for an active-low button with pull-up (e.g. `0` for GPIO0-to-GND). |
3233
| `LED_KIND` | `ws2812` | `ws2812` (addressable RGB, default), `gpio` (plain on/off), `pimoroni` (3-pin PWM RGB), or `none` (no indicator). See [build.md](build.md). |
3334
| `LED_ORDER` | `rgb` | A `ws2812` board whose red and green come out swapped (blue fine): set `grb` (the WS2812B standard). The Waveshare RP2350-One is `rgb`; most other parts are `grb`. |
3435
| `MAX_LEDS` | `8` | A board with **more than 8** daisy-chained addressable LEDs. The buffer ceiling; the actual connected count is set at runtime ([guides/led.md](guides/led.md)). |
@@ -39,6 +40,9 @@ env FLASH_SIZE=8M LED_KIND=gpio LED_PIN=25 cargo build --release -p firmware
3940

4041
# example: a 16 MB TenStar RP2350-USB — WS2812 on GP22, standard GRB order
4142
env FLASH_SIZE=16M LED_PIN=22 LED_ORDER=grb cargo build --release -p firmware
43+
44+
# example: WS2812 on GP22 and a dedicated touch button on GP0
45+
env LED_PIN=22 PRESENCE_PIN=0 cargo build --release -p firmware
4246
```
4347

4448
The four LED knobs (`LED_PIN` / `LED_KIND` / `LED_ORDER` / `MAX_LEDS`) set only the

docs/quickstart.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ picotool uf2 convert target/thumbv8m.main-none-eabihf/release/firmware -t elf fi
3333
```
3434

3535
This is the **touch build**: FIDO operations (registering, logging in) require
36-
a press of the BOOTSEL button — it stands in for the touch sensor on a typical
37-
hardware key. For a no-touch build (needed by the automated test suites, or if
38-
your board is hard to reach) add `--features no-touch`. All build knobs:
36+
a press of the presence button (BOOTSEL by default; set `PRESENCE_PIN=<gpio>` for
37+
a dedicated GPIO button). For a no-touch build (needed by the automated test
38+
suites, or if your board is hard to reach) add `--features no-touch`. All build knobs:
3939
[build.md](build.md).
4040

4141
## 2. Flash

docs/unsafe.md

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,33 @@ contained. Adding a new `unsafe` requires updating this page. (Safe Rust rules
77
out memory-corruption bugs in this code; it is not a security audit — see the
88
[threat model](threat-model.md).)
99

10-
**Runtime sites: 9**three concerns in the firmware proper (the interrupt
10+
**Runtime sites: 10**four concerns in the firmware proper (the interrupt
1111
handler pair counted honestly as two), two for the per-core prime sieves, one
1212
in the RSA assembly FFI, two in the standalone flash-wipe tool.
1313

1414
```mermaid
1515
flowchart TB
16-
subgraph fw["firmware/src/main.rs"]
16+
subgraph fw["firmware/src/main.rs + firmware/src/presence.rs"]
1717
a["interrupt executor (×2)"]
1818
b["Send for SendUsb"]
1919
c["heap init"]
20+
d["GPIO presence pin steal"]
2021
end
2122
subgraph kg["firmware/src/core1.rs"]
22-
d["per-core prime sieves (×2)"]
23+
e["per-core prime sieves (×2)"]
2324
end
2425
subgraph asm["rsk-rsa-asm"]
25-
e["modexp FFI"]
26+
f["modexp FFI"]
2627
end
2728
subgraph wipe["rsk-wipe"]
28-
f["raw flash erase/program (×2)"]
29+
g["raw flash erase/program (×2)"]
2930
end
3031
```
3132

3233
The `unsafe` lives only in plumbing — none of it is in a parser, applet, crypto
3334
wrapper, or the filesystem.
3435

35-
## Firmware (`firmware/src/main.rs`)
36+
## Firmware (`firmware/src/main.rs`, `firmware/src/presence.rs`)
3637

3738
### 1–2. The high-priority interrupt executor
3839

@@ -78,9 +79,25 @@ static buffer used by nothing else.
7879
*Safe alternative:* none; every embedded allocator initializes this way.
7980
*Containment:* one call, before any allocation can happen.
8081

82+
### 5. GPIO presence pin type-erasure
83+
84+
```rust
85+
let any = unsafe { AnyPin::steal(pin) };
86+
```
87+
88+
The optional `PRESENCE_PIN=<gpio>` path chooses the presence button at runtime
89+
(instead of a concrete `PIN_n` type at compile time), so the code must convert a
90+
numeric pin to embassy's type-erased `AnyPin`. `AnyPin::steal` is `unsafe`
91+
because the caller must guarantee unique ownership of that hardware pin.
92+
*Safe alternative:* none for a runtime-selected GPIO; the safe constructors
93+
require a statically known pin type.
94+
*Containment:* one call site in `BootselPresence::new_gpio`, gated by pin-range
95+
validation and the single-owner invariant from `main` (the chosen presence pin
96+
is never handed to the LED/backends; an LED/presence conflict panics at boot).
97+
8198
## Firmware dual-core keygen (`firmware/src/core1.rs`)
8299

83-
### 5–6. The per-core prime sieves
100+
### 6–7. The per-core prime sieves
84101

85102
```rust
86103
static mut CORE0_SIEVE: IncrementalSieve = IncrementalSieve::new();
@@ -109,7 +126,7 @@ a composite through to the strong-MR/Lucas test, which still rejects it.
109126

110127
## RSA assembly FFI (`crates/rsk-rsa-asm/src/lib.rs`)
111128

112-
### 7. The modexp call
129+
### 8. The modexp call
113130

114131
On-card RSA key generation needs hundreds of modular exponentiations over
115132
1024–2048-bit candidates; the pure-Rust path was ~7× too slow on the
@@ -125,7 +142,7 @@ tests exercise the same API safely.
125142

126143
## Flash wiper (`rsk-wipe/src/main.rs`)
127144

128-
### 8–9. Raw flash erase/program in a critical section
145+
### 9–10. Raw flash erase/program in a critical section
129146

130147
The wiper's entire job is to erase the flash the firmware lives on, from a
131148
RAM-resident image. It calls the ROM flash-erase/program routines inside

firmware/build.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ fn main() {
6666
println!("cargo:rustc-env=PK_LED_PIN={led_pin}");
6767
println!("cargo:rerun-if-env-changed=LED_PIN");
6868

69+
// User-presence source: default BOOTSEL, or `PRESENCE_PIN=<gpio>` for an
70+
// active-low GPIO button (internal pull-up). Encoded as two numeric envs so
71+
// `main` can parse them with the existing const decimal parser.
72+
let (presence_is_gpio, presence_pin) = resolve_presence_pin();
73+
println!(
74+
"cargo:rustc-env=PK_PRESENCE_IS_GPIO={}",
75+
if presence_is_gpio { 1 } else { 0 }
76+
);
77+
println!("cargo:rustc-env=PK_PRESENCE_PIN={presence_pin}");
78+
println!("cargo:rerun-if-env-changed=PRESENCE_PIN");
79+
6980
// LED backend (default `ws2812`, the Waveshare RP2350-One). Selected at
7081
// compile time so only the chosen driver — and its dependencies (PIO, PWM) —
7182
// is built. `gpio` = a plain on/off indicator, `pimoroni` = a 3-pin PWM RGB
@@ -190,6 +201,24 @@ fn resolve_led_pin() -> u8 {
190201
v
191202
}
192203

204+
/// Resolve `PRESENCE_PIN` to either BOOTSEL (unset / `bootsel`) or a GPIO
205+
/// number `0..=29` for an active-low button with internal pull-up.
206+
fn resolve_presence_pin() -> (bool, u8) {
207+
let raw = env::var("PRESENCE_PIN").unwrap_or_default();
208+
let v = raw.trim().to_ascii_lowercase();
209+
if v.is_empty() || v == "bootsel" {
210+
return (false, 0);
211+
}
212+
let pin = v.parse::<u8>().unwrap_or_else(|_| {
213+
panic!("PRESENCE_PIN={raw:?} must be `bootsel` or a GPIO number 0..=29")
214+
});
215+
assert!(
216+
pin <= 29,
217+
"PRESENCE_PIN={pin} out of range 0..=29 (RP2350A GPIOs)"
218+
);
219+
(true, pin)
220+
}
221+
193222
/// Resolve `LED_KIND` (the LED driver backend) to a known value; defaults to
194223
/// `ws2812`. One of: `ws2812` (addressable RGB on `LED_PIN`), `gpio` (a plain
195224
/// on/off LED on `LED_PIN`), `pimoroni` (3-pin PWM RGB, Pimoroni Tiny 2350), or

firmware/src/ccid_handler.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ pub struct CcidApplets<'a> {
4949
impl<'a> CcidApplets<'a> {
5050
/// `serial_id` is the device chip id (its first 4 bytes go into the OpenPGP
5151
/// full AID); `rng` is the hardware TRNG shared with the CTAPHID handler.
52-
/// The three `presence` params are the same BOOTSEL button behind per-applet
53-
/// traits (the caller's concrete `&RefCell` coerces to each).
52+
/// The three `presence` params are the same physical presence source (BOOTSEL
53+
/// by default, optionally a GPIO button) behind per-applet traits (the
54+
/// caller's concrete `&RefCell` coerces to each).
5455
#[allow(clippy::too_many_arguments)] // one-time wiring from the worker
5556
pub fn new(
5657
fs: &'a RefCell<Store>,
@@ -75,8 +76,8 @@ impl<'a> CcidApplets<'a> {
7576
// Touch-flagged OATH credentials gate CALCULATE on the same button.
7677
oath: OathApplet::new(serial_id, serial_hash, otp_key, rng, oath_presence),
7778
otp: OtpApplet::new(serial_id, otp_presence),
78-
// PIV reuses the OpenPGP user-presence trait, so the same BOOTSEL
79-
// `presence` drives its slot/management touch policies.
79+
// PIV reuses the OpenPGP user-presence trait, so the same presence
80+
// source drives its slot/management touch policies.
8081
piv: PivApplet::new(serial_id, serial_hash, otp_key, rng, presence),
8182
// The recovery/provisioning interface: phy config, flash stats,
8283
// secure-boot status, session RTC, device-key attestation, reboot.

firmware/src/handler.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ pub struct AppletHandler<'a> {
116116
/// Cross-message PIN/UV-auth state (PIN token, the ephemeral ECDH key …);
117117
/// lives for one power cycle.
118118
fido_state: rsk_fido::FidoState,
119-
/// Physical user presence (BOOTSEL), shared with the OpenPGP applet through a
119+
/// Physical user presence (BOOTSEL by default, optionally a GPIO button),
120+
/// shared with the OpenPGP applet through a
120121
/// `RefCell`; borrowed only for a touch wait inside one dispatch.
121122
presence: &'a RefCell<dyn rsk_fido::UserPresence>,
122123
serial_id: [u8; 8],

firmware/src/main.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ const XOSC_DELAY_MULT: u32 = env_u32(env!("PK_XOSC_DELAY_MULT"));
116116
// numbering (1=gpio, 2=pimoroni, 3=ws2812).
117117
#[cfg(not(led_kind = "none"))]
118118
const BUILD_LED_PIN: u8 = env_u16(env!("PK_LED_PIN")) as u8;
119+
const BUILD_PRESENCE_IS_GPIO: bool = env_u16(env!("PK_PRESENCE_IS_GPIO")) != 0;
120+
const BUILD_PRESENCE_PIN: u8 = env_u16(env!("PK_PRESENCE_PIN")) as u8;
119121
#[cfg(led_kind = "ws2812")]
120122
const BUILD_DRIVER: u8 = 3;
121123
#[cfg(led_kind = "gpio")]
@@ -310,7 +312,7 @@ async fn main(_spawner: Spawner) {
310312
config.serial_number = Some("rs-key-0001");
311313
config.max_power = 100;
312314
config.max_packet_size_0 = 64;
313-
config.device_release = 0x0781; // bcdDevice: our build counter
315+
config.device_release = 0x0782; // bcdDevice: our build counter
314316

315317
let mut builder = Builder::new(
316318
driver,
@@ -405,6 +407,12 @@ async fn main(_spawner: Spawner) {
405407
.and_then(|p| p.led_gpio)
406408
.filter(|&g| g <= 29)
407409
.unwrap_or(BUILD_LED_PIN);
410+
if BUILD_PRESENCE_IS_GPIO && BUILD_PRESENCE_PIN == led_gpio {
411+
panic!(
412+
"PRESENCE_PIN={} conflicts with active LED pin",
413+
BUILD_PRESENCE_PIN
414+
);
415+
}
408416
// PHY led_driver (1=gpio, 2=pimoroni, 3=ws2812) overrides the build kind;
409417
// anything else (unset, or the N/A esp32 value) keeps the build default.
410418
let led_driver = match phy.as_ref().and_then(|p| p.led_driver) {
@@ -522,7 +530,12 @@ async fn main(_spawner: Spawner) {
522530

523531
core1::spawn(p.CORE1);
524532

525-
let presence_ref = PRESENCE.init(RefCell::new(BootselPresence::new(p.BOOTSEL)));
533+
let presence = if BUILD_PRESENCE_IS_GPIO {
534+
BootselPresence::new_gpio(BUILD_PRESENCE_PIN)
535+
} else {
536+
BootselPresence::new_bootsel(p.BOOTSEL)
537+
};
538+
let presence_ref = PRESENCE.init(RefCell::new(presence));
526539
let platform_ref = RESCUE_PLATFORM.init(RefCell::new(rescue_platform::RescuePlatform));
527540
let (kvm, kvc) = (kvmain_range(), kvcnt_range());
528541
let kv_total = (kvm.end - kvm.start) + (kvc.end - kvc.start);

0 commit comments

Comments
 (0)