SHA256
1
0
Fork
You've already forked bridge
0
No description
  • 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>
2026年06月06日 15:59:11 +02:00
.cargo Upgrade embedded-sdmmc 0.3 -> 0.6: whole-block SPI transfer (~270 KB/s) 2026年06月04日 11:02:06 +02:00
docs Docs: SBOM + README for the final 2-crate, in-tree-USB state 2026年06月06日 11:16:32 +02:00
scripts bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
src Write-block-by-default with a BOOT-button write-enable and a blue alarm 2026年06月06日 15:59:11 +02:00
tests Write-block-by-default with a BOOT-button write-enable and a blue alarm 2026年06月06日 15:59:11 +02:00
vendor Update gd32vf103-pac 0.4.0 -> 0.5.0 (latest upstream), re-patched to stay minimal 2026年06月06日 11:12:43 +02:00
.dockerignore bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
.gitignore bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
bridge-link.x Own reset path: drop riscv-rt (and its macro / r0 / rand tail) 2026年06月05日 11:31:47 +02:00
build.rs Own reset path: drop riscv-rt (and its macro / r0 / rand tail) 2026年06月05日 11:31:47 +02:00
Cargo.lock Update gd32vf103-pac 0.4.0 -> 0.5.0 (latest upstream), re-patched to stay minimal 2026年06月06日 11:12:43 +02:00
Cargo.toml Release profile: keep symbols + DWARF for reverse-engineerability 2026年06月06日 13:48:39 +02:00
CHANGELOG.md bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
Dockerfile Refactor: single firmware config, extract CRC to the core, drop display deps 2026年06月05日 10:25:19 +02:00
LICENSE bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
memory.x bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
README.md Docs: SBOM + README for the final 2-crate, in-tree-USB state 2026年06月06日 11:16:32 +02:00
REQUIREMENTS.md bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00
rust-toolchain.toml bridge: air-gap USB mass-storage bridge for the Longan Nano (GD32VF103) 2026年06月03日 16:59:37 +02:00

🌉 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) vs Deliver (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_corezero 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:

  1. Hold BOOT, tap RESET, release BOOT → it appears as 28e9:0189 GD32 DFU Bootloader.
  2. scripts/flash.sh writes out/bridge.bin to 0x08000000 via dfu-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.

Built for sneakernet that doesn't trust either end.