- Rust 97.8%
- Shell 0.7%
- Dockerfile 0.7%
- Linker Script 0.7%
- Logos 0.1%
|
stewie
ecf98633c8
Write-block-by-default with a BOOT-button write-enable and a blue alarm
The device arms in Deliver (read-only / write-blocked) by default — LED steady green. The first WRITE(10) the host attempts is refused (the card is never touched) and latches a write-block alarm: the LED then blinks blue (~6 Hz) continuously. A debounced BOOT-button press (PA8, ≥100 ms) acknowledges the alarm (back to green) and enables writing for the rest of the power cycle — a one-way latch, Deliver -> Receive, never back. Everything else keeps working; only writes are blocked until the button is pressed. All driven non-blockingly from the USB loop (button debounce + LED via hw::cycles, written only on change), so the FIFO is never stalled. - security.rs: `Event::Toggle` (Armed flips direction, Locked arms) + tests; the firmware applies it one-way (only when still write-protected). - hw.rs: PA8 input + `boot_pressed()` + `cycles()` (mcycle exposed). - usb/mod.rs: button poll/debounce, the `write_alarm` latch, the LED state machine. Also drops the per-block SHA-256 session fingerprint: it ran for the first time at USB speed once Deliver became the default and overran the read prefetch (reset storm). `src/sha256.rs` is kept and FIPS-tested for a future off-hot-path use. Verified on hardware: read-only mount, 8 MB read with 0 USB resets (~966 KB/s). Build + 23 host tests green. Interactive parts (blue alarm, BOOT enable) are manual-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .cargo | Upgrade embedded-sdmmc 0.3 -> 0.6: whole-block SPI transfer (~270 KB/s) | |
| docs | Docs: SBOM + README for the final 2-crate, in-tree-USB state | |
| scripts | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| src | Write-block-by-default with a BOOT-button write-enable and a blue alarm | |
| tests | Write-block-by-default with a BOOT-button write-enable and a blue alarm | |
| vendor | Update gd32vf103-pac 0.4.0 -> 0.5.0 (latest upstream), re-patched to stay minimal | |
| .dockerignore | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| .gitignore | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| bridge-link.x | Own reset path: drop riscv-rt (and its macro / r0 / rand tail) | |
| build.rs | Own reset path: drop riscv-rt (and its macro / r0 / rand tail) | |
| Cargo.lock | Update gd32vf103-pac 0.4.0 -> 0.5.0 (latest upstream), re-patched to stay minimal | |
| Cargo.toml | Release profile: keep symbols + DWARF for reverse-engineerability | |
| CHANGELOG.md | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| Dockerfile | Refactor: single firmware config, extract CRC to the core, drop display deps | |
| LICENSE | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| memory.x | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| README.md | Docs: SBOM + README for the final 2-crate, in-tree-USB state | |
| REQUIREMENTS.md | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
| rust-toolchain.toml | bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) | |
🌉 bridge
A pocket-sized, auditable air-gap USB mass-storage bridge — in Rust, on RISC-V.
License: AGPL-3.0 Rust Target Board MCU Build: reproducible Core: no unsafe Deps: AGPL-compatible
It plugs into an online (untrusted) machine and into an offline (protected) machine — never both at once, because it has a single USB port. To each it looks like an ordinary USB stick; files live on a microSD card, and a small, auditable Rust firmware sits between the host and that card so that the human, not the host, decides what may happen.
One USB port means it is only ever connected to one machine at a time. That is the point: it carries data across an air gap (online ⇄ offline) under physical, button-gated control, instead of giving either machine raw access to the card.
✨ Why it's different
- 🔌 Only a USB stick, never a keyboard. It enumerates only as mass storage — the entire BadUSB / keyboard-injection class is gone by construction.
- 🙈 Invisible until you arm it. Power-on presents no medium. Nothing is readable or writable until a human presses BOOT. A bridge in your pocket, or in a hostile machine that just waits, exposes nothing.
- ↔️ You pick the direction — a software data-diode.
Receive(host may write — load data on the online side) vsDeliver(host is read-only — pull data off on the offline side, which then physically cannot write back). - 🔒 Mediated & fingerprinted. Every host block read/write passes through one audited choke-point; each session is byte-counted and folded into a SHA-256 fingerprint.
- 🦀 Memory-safe trusted core. All security logic lives in
bridge_core— zero external dependencies,#![forbid(unsafe_code)], exercised by host tests against FIPS vectors. - 📦 Bit-reproducible & vendored. Pinned toolchain + pinned Alpine + the full source of every dependency in-tree; two builds produce byte-identical firmware.
📊 Status
| Area | State |
|---|---|
| Trusted core (state machine, mediation, SHA-256) | ✅ done, host-tested (22 tests) |
| Reproducible build (Docker, vendored, verified bit-identical) | ✅ done |
| Bare-metal bring-up (clocks, RGB LED, BOOT button on PA8) | ✅ done |
| SD card (own SD/SDHC-over-SPI driver, DMA) | ✅ reads/writes a real card on hardware |
| USB Mass Storage (own MSC/SCSI over the GD32 USBFS) | ✅ works end-to-end: a real FAT32 microSD mounts, and files read and write through the bridge |
| Throughput | ✅ ~977 KB/s read — multi-packet USB-IN (PKTCNT=N) + SD block DMA prefetched into a double buffer, so the SD read overlaps the USB transfer. ×ばつ the first working build; near the USB Full-Speed ceiling (~1.1 MB/s) — see docs/MULTIBLOCK.md |
| Button-confirmed write + on-device hash + progress | 🚧 next |
| Encryption at rest · minimal verify-then-jump bootloader | 🗺️ roadmap |
See REQUIREMENTS.md for the authoritative spec and milestones.
🔭 Repository layout
src/
lib.rs bridge_core — the trusted core, ZERO external dependencies
sha256.rs SHA-256 (verified against FIPS known-answer vectors)
security.rs BOOT-button state machine + access policy
bridge.rs block-level mediation (every host access goes through here)
crc.rs SD-link CRC-7 / CRC-16 (integrity of the SPI transfer)
hw.rs register-level board bring-up (clock/GPIO/SPI/delay; no HAL)
boot.rs reset path: _start + .data/.bss init (no riscv-rt)
sd.rs minimal SD/SDHC-over-SPI block driver (own, DMA-prefetched)
usb/ USB Mass Storage — the usb-device + synopsys-usb-otg + usbd-storage
stack vendored in-tree (device/ otg/ storage/) and trimmed; cs.rs
is removed-favouring critical-section. Only the GD32 register defs
and critical-section remain external (2 crates total)
main.rs bare-metal firmware wiring; a host build is an empty stub
tests/ host-run tests for the trusted core (cargo test)
vendor/ audited sources of every dependency (2 crates) — build uses only these
Dockerfile pinned, offline, reproducible build environment
docs/ SECURITY.md (threat model + audit checklist) · SBOM.md
🔁 Reproducible build
scripts/build.sh # -> ./out/{bridge.elf,bridge.bin,SHA256SUMS}
scripts/build.sh --verify # build twice and prove the hashes match
What pins the output: Alpine base by digest, rustup + Rust 1.92.0,
Cargo.lock + in-tree vendor/ (built --offline --locked), a fixed release
profile (codegen-units = 1, lto, strip) and SOURCE_DATE_EPOCH. The
firmware bytes come solely from the pinned toolchain; --verify checks it.
The firmware is a single configuration — no cargo features: it always enumerates
as USB Mass Storage backed by the real microSD card. scripts/build.sh (or
cargo build --release --bin bridge) builds it.
Local development
cargo test # trusted-core tests on the host
cargo build --release --bin bridge # build firmware (riscv)
⚡ Flash
The Longan Nano flashes over the GD32 ROM DFU bootloader — no programmer:
- Hold BOOT, tap RESET, release BOOT → it appears as
28e9:0189 GD32 DFU Bootloader. scripts/flash.shwritesout/bridge.binto0x08000000viadfu-util.
Non-root flashing: install scripts/99-gd32-dfu.rules.
🛡️ Security
Threat model, the honest hardware limits (the GD32 Bumblebee core has no PMP,
weak readout protection), and the running audit checklist are in
docs/SECURITY.md. The dependency inventory and licences are
in docs/SBOM.md — every crate is AGPL-compatible permissive.
📜 License
AGPL-3.0-or-later — see LICENSE.