This commit is contained in:
Laan Tungir
2026-05-02 18:08:59 -04:00
commit 257a272969
9 changed files with 2190 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
fips/

View File

@@ -0,0 +1,37 @@
# Pi Zero 2W — leaf endpoint configuration
node:
identity:
persistent: true
leaf_only: true
cache:
coord_size: 1000
identity_size: 500
limits:
max_peers: 8
max_connections: 16
max_pending_inbound: 50
tun:
enabled: true
name: fips0
mtu: 1280
dns:
enabled: true
bind_addr: "127.0.0.1"
port: 5354
transports:
udp:
bind_addr: "0.0.0.0:2121"
tcp:
bind_addr: "0.0.0.0:8443"
peers: []
# Add your upstream peer:
# - npub: "npub1..."
# alias: "upstream"
# addresses:
# - transport: udp
# addr: "x.x.x.x:2121"
# connect_policy: auto_connect

134
fips_debug.md Normal file
View File

@@ -0,0 +1,134 @@
# FIPS Debug Notes: `.fips` HTTP reachability incident
## Summary
We investigated why this URL failed locally but worked from the remote host:
- `http://npub1crpldvy49ef8z34wlacwujnfudy4nd7k96aqdx5wgn6ckztz7z8q9t59ud.fips/index.html`
Outcome:
1. Initial local failure was due to a stale `fd00::/8` route pointing to an unreachable next-hop.
2. After route recovery, TCP connected but larger HTTP responses stalled.
3. Root cause for the stall was path/MTU behavior (large payload transfer blackholing/retransmission).
4. Setting local UDP transport MTU to `1280` resolved the issue; full page download succeeded.
---
## What we observed
### 1) DNS was correct
- `.fips` name resolved to the expected IPv6 address.
- Name resolution was **not** the issue.
### 2) Endpoints were alive
- On remote host (`ubuntu@laantungir.net`), the same URL returned `HTTP/1.1 200 OK` with full content.
- Nginx was listening and serving properly.
### 3) Local routing was initially broken
- Local route had `fd00::/8` via a link-local next-hop that was unreachable.
- Neighbor state for that gateway showed `FAILED`.
- This produced local `No route to host` errors.
### 4) After local route fix, only large responses failed
- Local curl connected to remote `:80` and sent GET.
- Small responses worked:
- direct IPv6 host request without vhost Host header got `410` small body,
- ranged request got `206` with a small payload.
- Full `index.html` (10KB+) timed out with `0 bytes received` locally.
- Remote socket stats indicated retransmissions and congestion-window collapse, consistent with MTU/path fragmentation loss behavior.
### 5) Mitigation validated
- Local config changed:
```yaml
transports:
udp:
bind_addr: 0.0.0.0:2121
mtu: 1280
```
- Restarted local `fips.service`.
- Retested URL: `HTTP 200`, full `10156` bytes downloaded.
---
## Lessons learned
1. **Treat `.fips` failures as a pipeline problem**
- Validate in this order: DNS → route → reachability → app response size behavior.
2. **“Connect succeeds” does not prove path health**
- A successful TCP handshake can still hide payload delivery failures.
- Always test both tiny and realistic/full payload responses.
3. **Stale `fd00::/8` routes can silently break TUN startup**
- We saw TUN init warnings when route insertion conflicted with existing state.
- Route hygiene matters before/after service restarts.
4. **MTU defaults are not always safe on every real path**
- Especially over mixed internet routes and overlays.
- `1280` is often the practical “known-good” baseline for IPv6 paths.
5. **Use control-plane telemetry early**
- `fipsctl show status|peers|sessions|routing` quickly distinguishes control-plane up/down from data-plane degradation.
6. **Host-header/vhost behavior can mislead debugging**
- Explicit `Host:` header to the same IPv6 returned 200 remotely.
- Direct IP without host header returned a different vhost response (410).
- This is expected and should be factored into tests.
---
## Recommended runbook for future incidents
1. **Reproduce quickly**
- `curl -v --max-time 10 http://<npub>.fips/index.html`
2. **Check resolution and route**
- `getent ahosts <npub>.fips`
- `ip -6 route get <resolved_ipv6>`
- `ip -6 neigh show`
3. **Check FIPS health**
- `sudo /usr/local/bin/fipsctl show status`
- `sudo /usr/local/bin/fipsctl show peers`
- `sudo /usr/local/bin/fipsctl show sessions`
4. **Differentiate small vs large payload behavior**
- Small: direct IP / tiny endpoint / range request
- Large: full page/object transfer
5. **If large payloads fail but small succeed, test MTU mitigation**
- Set `transports.udp.mtu: 1280` locally
- Restart `fips.service`
- Retest full payload
6. **Persist fix and monitor**
- Keep 1280 unless measured path supports higher values reliably.
- Track retransmission symptoms and transfer success over time.
---
## Config change made during this debug
Local `/etc/fips/fips.yaml` was updated to include:
```yaml
transports:
udp:
mtu: 1280
```
A backup of the prior local file was created before modification.
---
## Final status
The target `.fips` web page is reachable from this local node after lowering local UDP MTU to `1280` and restarting `fips.service`.

View File

@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../tab_5/Tab5NostrClient"
}
],
"settings": {}
}

View File

@@ -0,0 +1,396 @@
# FIPS Android VPN App — Architecture Plan
## Goal
Build an Android application (.apk) that runs the Rust FIPS daemon as a VPN service, enabling any app on the device — browsers, curl, etc. — to resolve `<npub>.fips` domains and route traffic through the FIPS mesh network. The user types `http://npub1abc...xyz.fips` in Chrome and it just works.
## Strategy
1. **Compile fips as a shared library** — build the Rust `fips` crate as a `cdylib` targeting `aarch64-linux-android` (and `x86_64-linux-android` for emulators), exposing a C-ABI FFI layer
2. **Android VpnService integration** — use Android's `VpnService` API to create a TUN interface that the OS routes all traffic through, replacing the Linux-specific `tun` crate usage
3. **DNS interception** — configure the VPN to use the fips DNS resolver for `.fips` domains, so the Android system resolver forwards queries to the in-process fips DNS responder
4. **Kotlin wrapper app** — minimal Android app with a single-activity UI for start/stop, status display, and configuration
## How It Works (User Perspective)
```
User opens FIPS app → taps "Connect" → VPN icon appears in status bar
User opens Chrome → types "http://npub1abc...xyz.fips" → page loads
```
## Architecture Overview
```mermaid
graph TD
subgraph "Android Device"
subgraph "FIPS App Process"
UI["Kotlin UI<br/>MainActivity"]
SVC["FipsVpnService<br/>extends VpnService"]
FFI["JNI/FFI Bridge<br/>libfips_android.so"]
subgraph "Rust Core"
NODE["Node<br/>mesh routing engine"]
DNS["DNS Responder<br/>port 5354"]
TUN_ADAPTER["VpnTunAdapter<br/>reads/writes VPN fd"]
UDP["UdpTransport"]
TCP["TcpTransport"]
TOR["TorTransport"]
end
end
BROWSER["Chrome / any app"]
OS_VPN["Android OS<br/>VPN routing table"]
end
subgraph "FIPS Mesh Network"
PEER1["Peer Node A"]
PEER2["Peer Node B"]
end
UI -->|"start/stop"| SVC
SVC -->|"passes VPN fd"| FFI
FFI -->|"JNI calls"| NODE
NODE --- DNS
NODE --- TUN_ADAPTER
NODE --- UDP
NODE --- TCP
BROWSER -->|"DNS query: npub1...fips"| OS_VPN
BROWSER -->|"HTTP traffic"| OS_VPN
OS_VPN -->|"all traffic"| TUN_ADAPTER
TUN_ADAPTER -->|"IPv6 packets"| NODE
UDP -->|"mesh traffic"| PEER1
TCP -->|"mesh traffic"| PEER2
```
## Data Flow: Browser to Mesh
```mermaid
sequenceDiagram
participant Browser
participant AndroidOS as Android OS
participant VPN as VpnService TUN fd
participant TunAdapter as VpnTunAdapter
participant DNS as FIPS DNS Responder
participant Node as FIPS Node
participant Mesh as FIPS Mesh
Browser->>AndroidOS: DNS query npub1abc.fips
AndroidOS->>VPN: Route DNS to VPN DNS server
VPN->>TunAdapter: UDP packet to 10.0.0.1:53
TunAdapter->>DNS: Forward .fips query
DNS->>DNS: Resolve npub to fd00::... IPv6
DNS-->>TunAdapter: AAAA response fd00::abc
TunAdapter-->>VPN: DNS response packet
VPN-->>AndroidOS: fd00::abc
AndroidOS-->>Browser: fd00::abc
Browser->>AndroidOS: TCP SYN to fd00::abc:80
AndroidOS->>VPN: Route through VPN
VPN->>TunAdapter: IPv6 packet
TunAdapter->>Node: Inject into mesh routing
Node->>Mesh: Encrypted mesh packet
Mesh-->>Node: Response
Node-->>TunAdapter: IPv6 response packet
TunAdapter-->>VPN: Write to TUN fd
VPN-->>AndroidOS: Route back
AndroidOS-->>Browser: HTTP response
```
## Key Components
### 1. Rust FFI Library (`libfips_android.so`)
The fips crate is compiled as a shared library with a thin C-ABI wrapper. This is the bridge between Kotlin and Rust.
**New file:** `fips/src/android/mod.rs`
```rust
// Exposed functions (C-ABI via #[no_mangle] extern "C"):
//
// fips_android_start(config_json, vpn_fd) -> handle
// fips_android_stop(handle)
// fips_android_status(handle) -> status_json
// fips_android_get_dns_addr() -> "10.0.0.1:53"
```
The critical difference from the Linux daemon: instead of creating a TUN device via the `tun` crate, the Android build receives a **file descriptor** from the `VpnService` and reads/writes raw IP packets on it directly.
**Cargo.toml changes:**
```toml
[lib]
crate-type = ["cdylib", "rlib"]
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
android_logger = "0.14"
```
### 2. VPN TUN Adapter (`fips/src/upper/tun_android.rs`)
Replaces the Linux/macOS `TunDevice` on Android. Instead of calling `tun::create()`, it wraps a raw file descriptor received from `VpnService.Builder.establish()`.
**Key differences from Linux TUN:**
| Aspect | Linux (`tun.rs`) | Android (`tun_android.rs`) |
|--------|-----------------|---------------------------|
| Device creation | `tun::create()` with CAP_NET_ADMIN | `VpnService.Builder.establish()` in Kotlin, fd passed to Rust |
| Interface config | `rtnetlink` / `ifconfig` | `VpnService.Builder` methods in Kotlin |
| Address assignment | `ip -6 addr add` via rtnetlink | `.addAddress()` in VpnService.Builder |
| Route setup | `ip -6 route add` via rtnetlink | `.addRoute()` in VpnService.Builder |
| DNS config | External resolv.conf / systemd-resolved | `.addDnsServer()` in VpnService.Builder |
| Read/write | `tun::Device` read/write | `read(fd)` / `write(fd)` via libc |
### 3. Android VPN Service (`FipsVpnService.kt`)
The Android foreground service that:
- Calls `VpnService.Builder` to configure the TUN interface
- Passes the resulting file descriptor to the Rust core via JNI
- Manages the fips daemon lifecycle (start/stop)
- Shows a persistent notification (required for foreground services)
```kotlin
class FipsVpnService : VpnService() {
// VPN configuration:
// - addAddress("fd00::1", 8) // FIPS IPv6 prefix
// - addRoute("::", 0) // Route all IPv6 through VPN
// - addDnsServer("10.0.0.1") // In-VPN DNS server
// - setMtu(1280) // Match FIPS TUN MTU
// - establish() // Returns ParcelFileDescriptor
//
// Then: NativeLib.fipsStart(config, fd.detachFd())
}
```
### 4. DNS Resolution Strategy
The FIPS daemon already includes a DNS responder (port 5354) that resolves `<npub>.fips` → IPv6. On Android, we need to make this accessible to the system resolver:
**Option A — VPN-internal DNS (recommended):**
- Bind the DNS responder to a virtual IP inside the VPN tunnel (e.g., `10.0.0.1:53`)
- Configure `VpnService.Builder.addDnsServer("10.0.0.1")`
- Android routes all DNS queries to this address
- The fips DNS responder handles `.fips` queries directly
- Non-`.fips` queries are forwarded to an upstream resolver (e.g., `1.1.1.1`)
**Option B — Split DNS:**
- Only route `.fips` queries to the internal resolver
- Requires Android 10+ per-domain DNS routing (limited API support)
Option A is simpler and more reliable. The DNS responder already exists in [`fips/src/upper/dns.rs`](fips/src/upper/dns.rs:1); it just needs a small enhancement to forward non-`.fips` queries upstream.
### 5. Android App Structure
```
fips-android/
├── app/
│ ├── src/main/
│ │ ├── java/com/fips/android/
│ │ │ ├── MainActivity.kt # UI: connect/disconnect, status
│ │ │ ├── FipsVpnService.kt # VpnService implementation
│ │ │ ├── NativeLib.kt # JNI bindings to libfips_android.so
│ │ │ └── FipsConfig.kt # Config management
│ │ ├── jniLibs/
│ │ │ ├── arm64-v8a/
│ │ │ │ └── libfips_android.so # Rust-compiled shared library
│ │ │ └── x86_64/
│ │ │ └── libfips_android.so # For emulator testing
│ │ ├── res/
│ │ │ ├── layout/activity_main.xml
│ │ │ └── values/strings.xml
│ │ └── AndroidManifest.xml
│ └── build.gradle.kts
├── build.gradle.kts
├── settings.gradle.kts
└── gradle/
```
**AndroidManifest.xml permissions:**
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<service
android:name=".FipsVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
```
## Platform Compatibility Considerations
### What Works As-Is on Android
| Component | Status | Notes |
|-----------|--------|-------|
| `Node` mesh routing | ✅ Works | Pure Rust, no platform deps |
| `Identity` / crypto | ✅ Works | secp256k1, chacha20poly1305 — all portable |
| `TreeState` / `BloomState` | ✅ Works | Pure data structures |
| `UdpTransport` | ✅ Works | tokio UDP sockets work on Android |
| `TcpTransport` | ✅ Works | tokio TCP sockets work on Android |
| `TorTransport` | ⚠️ Needs work | Requires bundling a Tor binary or using Orbot |
| `DNS Responder` | ✅ Works | UDP socket-based, portable |
| `Config` / YAML parsing | ✅ Works | serde_yaml is portable |
### What Needs Android-Specific Implementation
| Component | Current | Android Replacement |
|-----------|---------|-------------------|
| TUN device creation | `tun` crate + rtnetlink | VpnService fd from Kotlin |
| TUN interface config | rtnetlink / ifconfig | VpnService.Builder in Kotlin |
| Signal handling | SIGTERM / Ctrl+C | Service lifecycle callbacks |
| Logging | tracing-subscriber to stderr | android_logger to logcat |
| Config file location | /etc/fips/fips.yaml | App internal storage |
| `EthernetTransport` | Raw sockets (CAP_NET_RAW) | ❌ Not available on Android |
| `BLE Transport` | BlueZ/D-Bus via bluer | ❌ Needs Android BLE API (future) |
### What to Exclude from Android Build
- `fips-gateway` binary (requires nftables, Linux-only)
- `fipstop` TUI binary (terminal UI, not useful on Android)
- `fipsctl` CLI binary (replaced by in-app controls)
- `EthernetTransport` (requires raw socket access)
- `BLE Transport` (requires BlueZ, Linux-only; Android BLE would be a separate implementation)
- `rustables` / nftables dependency
- `ratatui` TUI dependency
## Build System
### Cross-Compilation Setup
```bash
# Install Android NDK targets
rustup target add aarch64-linux-android x86_64-linux-android
# Install cargo-ndk for simplified Android builds
cargo install cargo-ndk
# Build the shared library
cargo ndk -t arm64-v8a -t x86_64 -o app/src/main/jniLibs build --release
```
### Cargo Feature Flags
```toml
[features]
default = ["tui", "ble"]
android = [] # New: excludes TUI, BLE, gateway; enables android TUN adapter
tui = ["dep:ratatui"]
ble = ["dep:bluer"]
gateway = ["dep:rustables"]
```
The `android` feature flag gates:
- `tun_android.rs` instead of `tun.rs` for TUN device handling
- `android_logger` instead of `tracing-subscriber` stderr output
- Exclusion of platform-specific Linux dependencies (bluer, rtnetlink, rustables)
### Conditional Compilation in Existing Code
The existing codebase already uses `#[cfg(target_os = "linux")]` and `#[cfg(unix)]` guards extensively. Android is `target_os = "android"` but IS `unix`, so:
- `#[cfg(unix)]`**matches Android** (good for most things)
- `#[cfg(target_os = "linux")]`**does NOT match Android** (good, excludes BLE/rtnetlink)
- New guard needed: `#[cfg(target_os = "android")]` for Android-specific TUN adapter
The `tun` crate dependency (currently under `[target.'cfg(unix)'.dependencies]`) needs to be excluded on Android since we use the VpnService fd directly:
```toml
[target.'cfg(all(unix, not(target_os = "android")))'.dependencies]
tun = { version = "0.8.5", features = ["async"] }
```
## Implementation Phases
### Phase 1: Rust Library for Android
- Add `android` feature flag to `Cargo.toml`
- Create `fips/src/upper/tun_android.rs` — TUN adapter that wraps a raw fd
- Create `fips/src/android/mod.rs` — C-ABI FFI entry points
- Add `#[cfg(target_os = "android")]` guards to swap TUN implementations
- Verify cross-compilation with `cargo ndk` builds cleanly
### Phase 2: Minimal Android App Shell
- Create `fips-android/` Gradle project with Kotlin
- Implement `FipsVpnService` with VpnService.Builder configuration
- Implement `NativeLib.kt` JNI bindings
- Implement `MainActivity` with connect/disconnect button
- Wire up: button → VpnService → JNI → Rust fips start
### Phase 3: DNS Resolution
- Enhance DNS responder to forward non-`.fips` queries upstream
- Configure VPN DNS to point to in-tunnel DNS server
- Test: `nslookup npub1...fips` resolves to fd00::... address
- Test: `nslookup google.com` still resolves normally
### Phase 4: End-to-End Testing
- Start fips node on a Linux machine with TUN enabled
- Start fips Android app on device/emulator
- Configure both to peer with each other (UDP transport)
- Verify: Android browser can load `http://npub1...fips` served by Linux node
- Verify: mesh routing works through intermediate hops
### Phase 5: Polish and APK
- Add proper Android notification for foreground service
- Add configuration UI (peer addresses, identity management)
- Add connection status display (peer count, tree state)
- Build signed release APK
- Test on physical Android device
## Key Technical Decisions
### Why VpnService (not root/iptables)?
- **No root required** — VpnService is a standard Android API available to all apps
- **Works on all Android devices** — no custom ROM or rooting needed
- **OS-level integration** — Android shows VPN status in notification bar, handles reconnection
- **DNS interception built-in** — VpnService.Builder.addDnsServer() handles DNS routing
### Why shared library (not separate process)?
- **Single process** — simpler lifecycle management, no IPC needed
- **Direct fd passing** — VpnService fd can be passed directly to Rust via JNI
- **Lower latency** — no serialization overhead for packet I/O
- **Standard pattern** — this is how WireGuard, Tailscale, and other VPN apps work on Android
### Why not use the `tun` crate on Android?
- The `tun` crate calls `ioctl(TUNSETIFF)` which requires `CAP_NET_ADMIN` — not available to unprivileged Android apps
- Android's `VpnService` is the **only** way to create a TUN interface without root
- The VpnService provides a raw fd that behaves identically to a TUN fd — we just need to read/write on it
## Dependencies Summary
### New Rust Dependencies (Android only)
| Crate | Purpose |
|-------|---------|
| `jni` | JNI bindings for Kotlin ↔ Rust calls |
| `android_logger` | Route `tracing` output to Android logcat |
### Android/Gradle Dependencies
| Dependency | Purpose |
|------------|---------|
| Android SDK 34+ | Target API level |
| Android NDK r26+ | Rust cross-compilation toolchain |
| Kotlin 2.0+ | App language |
| Gradle 8.x | Build system |
| cargo-ndk | Simplified Rust → Android cross-compilation |
## Risk Assessment
| Risk | Mitigation |
|------|-----------|
| Android kills VPN service in background | Use foreground service with persistent notification; Android protects VPN services from aggressive battery optimization |
| DNS forwarding for non-.fips domains adds latency | Use fast upstream resolver (1.1.1.1); cache aggressively; only intercept when VPN is active |
| Large APK size from Rust binary | Strip symbols, use LTO, target only arm64-v8a (covers 99%+ of modern devices) |
| tokio runtime on Android | Well-tested; WireGuard-rs, Tailscale use tokio on Android successfully |
| VPN permission prompt scares users | Clear explanation in UI before requesting; this is standard for all VPN apps |

View File

@@ -0,0 +1,386 @@
# FIPS Minimal C Port — Architecture Plan
## Goal
Create a new `c_fips` repository containing a minimal FIPS mesh protocol implementation in C, built on top of `nostr_core_lib` for crypto primitives. Develop and test on Linux first, then target ESP32.
The end result: a C program that can join a FIPS mesh network, authenticate with peers, and send/receive end-to-end encrypted data — wire-compatible with the Rust FIPS implementation.
## Strategy
1. **New repo `c_fips`** — standalone project, pulls in `nostr_core_lib` as a dependency
2. **Extend `nostr_core_lib`** — add Poly1305 MAC to complete ChaCha20-Poly1305 AEAD (the one missing crypto primitive)
3. **Linux first** — develop and test everything on Linux using standard sockets, validate interop against the Rust FIPS node
4. **ESP32 second** — once Linux interop works, add ESP-IDF platform layer and build for ESP32
## What nostr_core_lib Already Provides
| Primitive | nostr_core_lib Function | FIPS Usage |
|-----------|------------------------|------------|
| secp256k1 keygen | `nostr_ec_public_key_from_private_key()` | Identity generation |
| secp256k1 ECDH | `ecdh_shared_secret()` | Noise DH operations |
| secp256k1 Schnorr sign/verify | `nostr_schnorr_sign()` | Tree declaration signing |
| SHA-256 (streaming) | `nostr_sha256_init/update/final()` | Noise handshake hash, NodeAddr derivation, Bloom hashing |
| HMAC-SHA256 | `nostr_hmac_sha256()` | Noise key derivation |
| HKDF extract+expand | `nostr_hkdf_extract()`, `nostr_hkdf_expand()` | Noise MixKey operation |
| ChaCha20 stream cipher | `chacha20_encrypt()` | Part of AEAD construction |
| Bech32 encode/decode | `nostr_key_to_bech32()`, `nostr_decode_npub/nsec()` | npub/nsec display |
| Hex encode/decode | `nostr_bytes_to_hex()`, `nostr_hex_to_bytes()` | Debug output |
| Platform CSPRNG | `nostr_platform_random()` | Ephemeral key generation, nonces |
| cJSON | `cJSON.c` | Config parsing (optional) |
## What We Need to Add to nostr_core_lib
### Poly1305 MAC + ChaCha20-Poly1305 AEAD
`nostr_core_lib` has ChaCha20 but **not** Poly1305. FIPS needs ChaCha20-Poly1305 AEAD (RFC 8439) for all Noise encryption.
**Approach:** Add a pure-C Poly1305 implementation to `nostr_core_lib/nostr_core/crypto/`, then build the AEAD construction on top. This keeps everything self-contained and portable (no external dependency needed on Linux; on ESP32, could optionally use mbedtls hardware acceleration).
New files in nostr_core_lib:
```
nostr_core/crypto/nostr_poly1305.c — Poly1305 MAC (RFC 8439 Section 2.5)
nostr_core/crypto/nostr_chacha20poly1305.c — AEAD construction (RFC 8439 Section 2.8)
```
New API:
```c
// Poly1305 one-shot MAC
int nostr_poly1305_mac(const unsigned char *key, const unsigned char *msg,
size_t msg_len, unsigned char *tag);
// ChaCha20-Poly1305 AEAD encrypt
int nostr_chacha20poly1305_encrypt(const unsigned char key[32],
const unsigned char nonce[12],
const unsigned char *aad, size_t aad_len,
const unsigned char *plaintext, size_t pt_len,
unsigned char *ciphertext,
unsigned char tag[16]);
// ChaCha20-Poly1305 AEAD decrypt
int nostr_chacha20poly1305_decrypt(const unsigned char key[32],
const unsigned char nonce[12],
const unsigned char *aad, size_t aad_len,
const unsigned char *ciphertext, size_t ct_len,
const unsigned char tag[16],
unsigned char *plaintext);
```
## Architecture
### Module Dependency Map
```mermaid
graph TD
subgraph "nostr_core_lib - existing"
SECP["secp256k1<br/>keygen, ECDH, sign/verify"]
SHA["SHA-256<br/>streaming + single-shot"]
HMAC["HMAC-SHA256"]
HKDF["HKDF extract+expand"]
CHACHA["ChaCha20 stream cipher"]
BECH["Bech32 npub/nsec"]
PLATFORM["Platform abstraction<br/>CSPRNG"]
end
subgraph "nostr_core_lib - new"
POLY["Poly1305 MAC"]
AEAD["ChaCha20-Poly1305 AEAD"]
end
subgraph "c_fips - Noise Protocol"
NOISE_IK["Noise IK Handshake<br/>link-layer auth"]
NOISE_XK["Noise XK Handshake<br/>e2e sessions"]
NOISE_SESSION["Noise Transport Session<br/>encrypt/decrypt + replay"]
REPLAY["Replay Window<br/>2048-bit sliding bitmap"]
end
subgraph "c_fips - Wire Protocol"
WIRE["FMP Wire Format<br/>4-byte prefix parser"]
LINK_MSG["Link Messages<br/>TreeAnnounce, FilterAnnounce, etc."]
SESSION_MSG["Session Messages<br/>SessionSetup, SessionAck, DataPacket"]
end
subgraph "c_fips - Routing"
IDENTITY["FIPS Identity<br/>NodeAddr from pubkey"]
TREE["Spanning Tree<br/>coordinates, parent selection"]
BLOOM["Bloom Filter<br/>1KB bit array, k=5"]
COORD["Coordinate Cache<br/>fixed-size array"]
end
subgraph "c_fips - Node"
NODE["FIPS Node<br/>main event loop"]
PEER["Peer Table<br/>fixed-capacity"]
UDP["UDP Transport<br/>POSIX sockets / lwIP"]
end
AEAD --> CHACHA
AEAD --> POLY
NOISE_IK --> SECP
NOISE_IK --> SHA
NOISE_IK --> HKDF
NOISE_IK --> AEAD
NOISE_XK --> SECP
NOISE_XK --> SHA
NOISE_XK --> HKDF
NOISE_XK --> AEAD
NOISE_SESSION --> AEAD
NOISE_SESSION --> REPLAY
IDENTITY --> SECP
IDENTITY --> SHA
IDENTITY --> BECH
BLOOM --> SHA
NODE --> PEER
NODE --> TREE
NODE --> BLOOM
NODE --> COORD
NODE --> UDP
PEER --> NOISE_SESSION
PEER --> NOISE_IK
NODE --> WIRE
NODE --> LINK_MSG
NODE --> SESSION_MSG
UDP --> PLATFORM
```
### Memory Budget (Leaf Node, 3 Peers)
| Component | RAM | Notes |
|-----------|-----|-------|
| Identity | ~96 B | Secret key + pubkey + node_addr |
| Tree State | ~512 B | Declaration + parent + coordinates |
| Per-Peer x3 (Noise + Bloom + Tree + MMP) | ~7 KB | ~2.3 KB each |
| Coordinate Cache (32 entries) | ~5 KB | Fixed array, LRU eviction |
| Packet Buffers (2x 1500B) | ~3 KB | RX + TX |
| Handshake scratch | ~512 B | Temporary during handshake |
| Stack + misc | ~4 KB | |
| **Total RAM** | **~22 KB** | Fits easily in ESP32 520 KB SRAM |
| **Code (flash)** | **~80-120 KB** | |
## Repository Structure
```
c_fips/
├── README.md
├── Makefile # Linux build
├── CMakeLists.txt # ESP-IDF component build
├── nostr_core_lib/ # Git submodule or vendored copy
│ └── ... # With Poly1305/AEAD additions
├── include/
│ └── fips/
│ ├── fips.h # Top-level public API
│ ├── fips_types.h # Common types: NodeAddr, etc.
│ ├── fips_identity.h # Identity generation/management
│ ├── fips_noise.h # Noise protocol API
│ ├── fips_wire.h # Wire format constants and parsers
│ ├── fips_protocol.h # Protocol message types
│ ├── fips_bloom.h # Bloom filter
│ ├── fips_tree.h # Spanning tree
│ ├── fips_node.h # Node state and event loop
│ └── fips_platform.h # Platform abstraction
├── src/
│ ├── identity/
│ │ └── fips_identity.c # NodeAddr derivation, key management
│ ├── noise/
│ │ ├── fips_noise_symmetric.c # SymmetricState: MixHash, MixKey
│ │ ├── fips_noise_ik.c # Noise IK handshake (link layer)
│ │ ├── fips_noise_xk.c # Noise XK handshake (session layer)
│ │ ├── fips_noise_session.c # Transport encrypt/decrypt
│ │ └── fips_noise_replay.c # 2048-bit sliding window
│ ├── wire/
│ │ └── fips_wire.c # FMP wire format parse/build
│ ├── protocol/
│ │ ├── fips_link_msg.c # Link-layer message encode/decode
│ │ ├── fips_session_msg.c # Session-layer message encode/decode
│ │ ├── fips_tree_msg.c # TreeAnnounce encode/decode
│ │ ├── fips_filter_msg.c # FilterAnnounce encode/decode
│ │ └── fips_discovery_msg.c # LookupRequest/Response
│ ├── routing/
│ │ ├── fips_bloom.c # Bloom filter operations
│ │ ├── fips_tree_coord.c # Coordinate math, distance
│ │ ├── fips_tree_state.c # Parent selection, declarations
│ │ └── fips_coord_cache.c # Fixed-size coordinate cache
│ ├── node/
│ │ ├── fips_node.c # Node state + event loop
│ │ ├── fips_peer.c # Peer table management
│ │ ├── fips_node_handshake.c # Handshake handling
│ │ ├── fips_node_tree.c # TreeAnnounce logic
│ │ ├── fips_node_bloom.c # FilterAnnounce logic
│ │ ├── fips_node_session.c # E2E session management
│ │ └── fips_node_data.c # Application data send/receive
│ ├── transport/
│ │ ├── fips_udp_linux.c # Linux: POSIX UDP sockets
│ │ └── fips_udp_esp32.c # ESP32: lwIP sockets
│ └── platform/
│ ├── fips_platform_linux.c # Linux: time, logging
│ └── fips_platform_esp32.c # ESP32: time, logging
├── test/
│ ├── test_aead.c # ChaCha20-Poly1305 test vectors (RFC 8439)
│ ├── test_noise_ik.c # Noise IK handshake unit tests
│ ├── test_noise_xk.c # Noise XK handshake unit tests
│ ├── test_wire.c # Wire format round-trip tests
│ ├── test_bloom.c # Bloom filter tests
│ ├── test_tree.c # Tree coordinate/distance tests
│ ├── test_identity.c # NodeAddr derivation tests
│ └── test_interop.c # Connect to Rust FIPS node over UDP
├── examples/
│ ├── linux_leaf_node.c # Complete Linux leaf node example
│ └── esp32_leaf_node/ # ESP-IDF project
│ ├── CMakeLists.txt
│ └── main/
│ ├── CMakeLists.txt
│ └── app_main.c
└── tools/
└── gen_test_vectors.sh # Script to generate test vectors from Rust FIPS
```
## Implementation Phases
### Phase 1: Crypto Foundation (Linux)
Add Poly1305 + AEAD to nostr_core_lib, then build the Noise protocol.
- [ ] Add `nostr_poly1305.c` to nostr_core_lib (RFC 8439 Section 2.5)
- [ ] Add `nostr_chacha20poly1305.c` to nostr_core_lib (RFC 8439 Section 2.8)
- [ ] Write AEAD test vectors from RFC 8439 Appendix A
- [ ] Implement `fips_noise_symmetric.c` — SymmetricState (MixHash, MixKey, EncryptAndHash, DecryptAndHash)
- [ ] Implement `fips_noise_ik.c` — Noise IK initiator and responder
- [ ] Implement `fips_noise_session.c` — transport encrypt/decrypt with counter
- [ ] Implement `fips_noise_replay.c` — 2048-bit sliding window
- [ ] Write Noise IK unit tests (handshake completes, keys match, encrypt/decrypt works)
- [ ] Generate test vectors from Rust FIPS node for cross-validation
### Phase 2: Wire Protocol + Identity (Linux)
Port the binary wire format and identity system.
- [ ] Implement `fips_identity.c` — NodeAddr derivation from pubkey (SHA-256 + fd prefix)
- [ ] Implement `fips_wire.c` — common prefix, established frame header, handshake msg1/msg2
- [ ] Implement `fips_tree_msg.c` — TreeAnnounce encode/decode
- [ ] Implement `fips_filter_msg.c` — FilterAnnounce encode/decode
- [ ] Implement `fips_link_msg.c` — SessionDatagram, Heartbeat, Disconnect
- [ ] Implement `fips_session_msg.c` — SessionSetup, SessionAck, DataPacket
- [ ] Implement `fips_discovery_msg.c` — LookupRequest, LookupResponse
- [ ] Write wire format round-trip tests
- [ ] Validate against captured packets from Rust FIPS node
### Phase 3: Routing Data Structures (Linux)
- [ ] Implement `fips_bloom.c` — 1KB Bloom filter with k=5 double hashing
- [ ] Implement `fips_tree_coord.c` — TreeCoordinate, distance calculation, LCA
- [ ] Implement `fips_tree_state.c` — parent selection, declaration creation/signing
- [ ] Implement `fips_coord_cache.c` — fixed-size (32 entry) LRU cache
- [ ] Write unit tests for each
### Phase 4: Node Core + Linux Interop (Linux)
Build the node, connect to a Rust FIPS node.
- [ ] Implement `fips_udp_linux.c` — POSIX UDP socket transport
- [ ] Implement `fips_platform_linux.c` — monotonic time, logging, random
- [ ] Implement `fips_peer.c` — fixed-capacity peer table (4 slots)
- [ ] Implement `fips_node.c` — node state, poll-based event loop
- [ ] Implement `fips_node_handshake.c` — Noise IK handshake handling
- [ ] Implement `fips_node_tree.c` — TreeAnnounce send/receive
- [ ] Implement `fips_node_bloom.c` — FilterAnnounce send/receive
- [ ] Implement `fips_node_session.c` — Noise XK e2e session setup
- [ ] Implement `fips_node_data.c` — application data send/receive
- [ ] **Milestone: C leaf node connects to Rust FIPS node, completes handshake**
- [ ] **Milestone: C node joins spanning tree, exchanges bloom filters**
- [ ] **Milestone: C node sends/receives encrypted data to remote Rust node**
### Phase 5: ESP32 Port
- [ ] Implement `fips_udp_esp32.c` — lwIP UDP sockets
- [ ] Implement `fips_platform_esp32.c` — ESP-IDF time, logging
- [ ] Create ESP-IDF CMakeLists.txt (component registration)
- [ ] Create `esp32_leaf_node` example project (WiFi init + FIPS node)
- [ ] Test ESP32 connecting to Rust FIPS node over WiFi
## Key Design Decisions
### No Dynamic Allocation in Protocol Core
All state is either:
- **Statically sized** (peer table = 4 slots, coord cache = 32 entries, bloom = 1024 bytes)
- **Caller-provided buffers** (all encode/decode functions take output buffer + size)
- **Stack-allocated scratch** (handshake temporary state)
This makes the code predictable for embedded and eliminates memory leak concerns.
### Poll-Based Event Loop (No Async Runtime)
Replace Tokio with a simple cooperative loop:
```c
void fips_node_run(fips_node_t *node) {
while (node->running) {
// 1. Poll UDP for incoming packets (non-blocking)
fips_node_poll_rx(node);
// 2. Process pending handshakes
fips_node_process_handshakes(node);
// 3. Periodic tick (1s): tree announce, filter update, keepalive
uint64_t now = fips_time_ms();
if (now - node->last_tick_ms >= 1000) {
fips_node_tick(node);
node->last_tick_ms = now;
}
// 4. Process application TX queue
fips_node_poll_tx(node);
// 5. Brief sleep (Linux: usleep; ESP32: vTaskDelay)
fips_platform_sleep_ms(10);
}
}
```
### Platform Abstraction
Same pattern as nostr_core_lib — a header with platform-specific `.c` files:
```c
// fips_platform.h
uint64_t fips_time_ms(void); // Monotonic milliseconds
void fips_platform_sleep_ms(uint32_t ms);
void fips_log(int level, const char *fmt, ...);
```
### Interop Testing Strategy
The Rust FIPS node is the reference. Testing approach:
1. **Generate test vectors** from Rust node (keypair → expected NodeAddr, handshake bytes, etc.)
2. **Packet capture** — run Rust nodes, capture UDP traffic, replay against C parser
3. **Live interop** — C leaf node connects to Rust node, validates full protocol flow
4. **Docker test** — Rust FIPS node in Docker, C node on host, automated test
## Crypto Primitive Mapping
| FIPS Rust Code | Rust Crate | C Function (nostr_core_lib) |
|----------------|------------|----------------------------|
| `Sha256::new()` / `update()` / `finalize()` | `sha2` | `nostr_sha256_init/update/final()` |
| `Hkdf::<Sha256>::new(salt, ikm)` / `expand()` | `hkdf` | `nostr_hkdf_extract()` + `nostr_hkdf_expand()` |
| `ChaCha20Poly1305::encrypt(nonce, payload)` | `chacha20poly1305` | `nostr_chacha20poly1305_encrypt()` **NEW** |
| `shared_secret_point(pub, sec)` | `secp256k1` | `ecdh_shared_secret()` |
| `Keypair::new(secp, &secret_key)` | `secp256k1` | `nostr_ec_public_key_from_private_key()` |
| `rand::thread_rng().gen()` | `rand` | `nostr_platform_random()` |
| `bech32::encode("npub", ...)` | `bech32` | `nostr_key_to_bech32()` |
## Reference Documentation
The FIPS design docs in `docs/design/` are strong enough to implement from:
- **Wire formats**: [`fips-wire-formats.md`](../docs/design/fips-wire-formats.md) — byte-level spec with field tables and SVG diagrams
- **Mesh layer**: [`fips-mesh-layer.md`](../docs/design/fips-mesh-layer.md) — peer auth, link encryption, forwarding
- **Session layer**: [`fips-session-layer.md`](../docs/design/fips-session-layer.md) — e2e encryption, Noise XK
- **Mesh operation**: [`fips-mesh-operation.md`](../docs/design/fips-mesh-operation.md) — routing decisions, discovery, error recovery
- **Spanning tree**: [`fips-spanning-tree.md`](../docs/design/fips-spanning-tree.md) — algorithms, parent selection
- **Bloom filters**: [`fips-bloom-filters.md`](../docs/design/fips-bloom-filters.md) — parameters, propagation rules

View File

@@ -0,0 +1,539 @@
# FIPS ESP32 Noise Bridge — Architecture Plan
## Goal
Enable ESP32 devices to be first-class addressable endpoints on the FIPS mesh
— each with its own npub and true end-to-end Noise XK encryption — without
running the full FIPS mesh protocol stack. A **bridge daemon** on a Linux host
handles all mesh protocol complexity; the ESP32 only implements the Noise XK
session cryptography.
The end result: an ESP32 light switch with npub `npub1kitchen...` is directly
reachable from anywhere on the FIPS mesh. Traffic is end-to-end encrypted
between the remote peer and the ESP32. The bridge daemon never sees plaintext.
## Problem Statement
The full `c_fips` port (spanning tree, bloom filters, wire format, routing,
Noise IK link layer, Noise XK session layer) is complex to implement on
ESP32 — roughly 20 C source files and 5,0008,000 lines of code. Meanwhile,
the ESP32 already has all the cryptographic primitives needed for Noise XK:
- secp256k1 keygen, ECDH, sign/verify
- SHA-256 (streaming)
- HMAC-SHA256 / HKDF
- ChaCha20-Poly1305 AEAD
These are the same primitives used by NIP-44. The insight: if the ESP32 can
do the session-layer crypto, a bridge daemon can handle everything else.
## Architecture
```mermaid
graph TD
subgraph "ESP32 Device"
APP["Application<br/>e.g. light controller"]
NOISE_XK["Noise XK<br/>Responder + Session<br/>encrypt/decrypt"]
TRANSPORT_E["WiFi UDP Client"]
end
subgraph "Linux Host"
TRANSPORT_B["WiFi UDP Server"]
BRIDGE["Bridge Daemon<br/>fips-bridge"]
FIPSD["fipsd<br/>Full mesh node"]
CONTROL["Control Socket"]
TUN["fips0 TUN"]
end
subgraph "FIPS Mesh"
REMOTE["Remote Peer<br/>e.g. office laptop"]
end
APP <--> NOISE_XK
NOISE_XK <--> TRANSPORT_E
TRANSPORT_E <-->|"WiFi UDP<br/>Noise handshake bytes<br/>+ encrypted data"| TRANSPORT_B
TRANSPORT_B <--> BRIDGE
BRIDGE <-->|"leaf_dependent registration<br/>+ identity cache priming"| CONTROL
BRIDGE <-->|"SessionDatagram relay<br/>via fips0 TUN"| TUN
TUN <--> FIPSD
FIPSD <--> REMOTE
```
### Component Responsibilities
**ESP32 (Noise XK endpoint):**
- Holds its own nsec/npub keypair
- Implements Noise XK responder (process msg1, generate msg2, process msg3)
- Implements Noise XK initiator (for ESP32-initiated sessions)
- Encrypts/decrypts data with session keys
- Runs application logic (GPIO, sensors, etc.)
**Bridge daemon (`fips-bridge`, new Linux binary):**
- Manages ESP32 device registry (npub → WiFi address mapping)
- Registers each ESP32 as a leaf dependent in fipsd (bloom filter advertising)
- Primes fipsd identity cache with ESP32 npub → NodeAddr → IPv6 mappings
- Intercepts mesh traffic for ESP32 addresses on fips0
- Relays Noise XK handshake messages between fipsd and ESP32
- Relays encrypted session data (opaque ciphertext) between fipsd and ESP32
- Handles all FIPS wire format encoding/decoding
- Never possesses ESP32 private keys; never sees plaintext data
**fipsd (existing, with minor modifications):**
- Advertises ESP32 node_addrs in bloom filters via leaf_dependents
- Routes mesh traffic toward ESP32 destinations
- Needs modification: treat leaf dependent destinations as local delivery
## Data Flow
### Inbound: Remote Peer → ESP32
```mermaid
sequenceDiagram
participant REMOTE as Remote Peer
participant MESH as FIPS Mesh
participant FIPSD as fipsd
participant BRIDGE as Bridge Daemon
participant ESP as ESP32
Note over REMOTE: Wants to reach npub1kitchen
rect rgb(230, 245, 255)
Note right of REMOTE: Session Handshake
REMOTE->>MESH: SessionSetup (XK msg1)<br/>dest: node_addr of npub1kitchen
MESH->>FIPSD: Bloom-guided routing to home node
FIPSD->>FIPSD: dest is leaf_dependent → local delivery
FIPSD->>BRIDGE: SessionDatagram on fips0
BRIDGE->>BRIDGE: Extract XK msg1 payload
BRIDGE->>ESP: WiFi UDP: msg1 bytes
ESP->>ESP: Process msg1 (ECDH, MixKey, etc.)
ESP->>ESP: Generate msg2
ESP->>BRIDGE: WiFi UDP: msg2 bytes
BRIDGE->>FIPSD: SessionDatagram (SessionAck) on fips0
FIPSD->>MESH: Route back to remote
MESH->>REMOTE: SessionAck (XK msg2)
REMOTE->>MESH: SessionMsg3 (XK msg3)
MESH->>FIPSD->>BRIDGE->>ESP: msg3 bytes
ESP->>ESP: Process msg3 → session established
end
rect rgb(230, 255, 230)
Note right of REMOTE: Encrypted Data
REMOTE->>MESH: Encrypted datagram (session key)
MESH->>FIPSD: Route to home
FIPSD->>BRIDGE: Ciphertext on fips0
BRIDGE->>ESP: WiFi UDP: ciphertext
ESP->>ESP: Decrypt with recv_key
ESP->>ESP: Execute command (light ON)
ESP->>ESP: Encrypt response with send_key
ESP->>BRIDGE: WiFi UDP: ciphertext
BRIDGE->>FIPSD: On fips0
FIPSD->>MESH: Route to remote
MESH->>REMOTE: Encrypted response
end
```
### Outbound: ESP32 → Remote Peer
The ESP32 can also initiate sessions (e.g., push sensor data). The bridge
daemon handles the routing; the ESP32 runs the Noise XK initiator side.
## Changes Required to fipsd
### 1. Leaf Dependent Local Delivery
Currently, [`handle_session_datagram()`](../src/node/handlers/forwarding.rs:61)
only delivers locally when `dest_addr == self.node_addr()`. Packets for leaf
dependents fall through to `find_next_hop()`, which has no route (the
destination isn't a peer, and bloom filters won't match due to split-horizon).
**Change:** Add a leaf dependent check to the local delivery path:
```rust
// Local delivery: self OR leaf dependent
if datagram.dest_addr == *self.node_addr()
|| self.bloom_state.leaf_dependents().contains(&datagram.dest_addr)
{
self.handle_session_payload(...).await;
return;
}
```
This delivers the SessionDatagram to the session layer, which passes it to
the IPv6 adapter, which writes it to fips0 — where the bridge daemon picks
it up.
### 2. Control Socket: Leaf Dependent Management
Add control socket commands for the bridge daemon to register/unregister
ESP32 devices:
```json
{"command": "add_leaf_dependent", "params": {"npub": "npub1kitchen..."}}
{"command": "remove_leaf_dependent", "params": {"npub": "npub1kitchen..."}}
{"command": "list_leaf_dependents"}
```
These commands call the existing [`add_leaf_dependent()`](../src/bloom/state.rs:85)
and [`remove_leaf_dependent()`](../src/bloom/state.rs:90) methods, plus prime
the identity cache with the npub → NodeAddr → IPv6 mapping.
### 3. Session Layer: Proxy Mode for Leaf Dependents
The session layer currently expects to complete Noise XK handshakes using the
local node's keypair. For leaf dependent destinations, the session layer needs
to **pass through** the SessionSetup/SessionAck/SessionMsg3 payloads without
processing them — the ESP32 handles the crypto.
**Approach:** When a SessionSetup arrives for a leaf dependent address, instead
of creating a local HandshakeState, fipsd delivers the raw SessionDatagram to
fips0. The bridge daemon extracts the handshake payload, relays it to the
ESP32, receives the response, and injects the reply SessionDatagram back into
fips0. fipsd forwards it into the mesh.
This requires the session layer to recognize leaf dependent addresses and skip
local handshake processing, instead treating the traffic as pass-through to
the TUN interface.
## Bridge Daemon Design
### Configuration
```yaml
# /etc/fips/bridge.yaml
bridge:
listen: "0.0.0.0:2122" # WiFi UDP listener for ESP32 devices
fipsd_control: "/tmp/fips-control.sock" # fipsd control socket path
devices:
- npub: "npub1kitchen..."
alias: "kitchen-light"
address: "192.168.1.50:2122" # ESP32 WiFi address
transport: udp
- npub: "npub1bedroom..."
alias: "bedroom-light"
address: "192.168.1.51:2122"
transport: udp
```
### Startup Sequence
1. Parse config, load device registry
2. Connect to fipsd control socket
3. For each device:
a. Compute NodeAddr from npub (SHA-256 truncation)
b. Register as leaf dependent via control socket
c. Prime identity cache via control socket
4. Open WiFi UDP listener for ESP32 connections
5. Open raw socket or TUN tap on fips0 for intercepting leaf dependent traffic
6. Enter event loop: relay packets between fips0 and ESP32 devices
### Packet Relay Logic
The bridge daemon operates as a bidirectional relay:
**fips0 → ESP32:**
1. Read IPv6 packet from fips0 destined for a leaf dependent address
2. Look up destination in device registry → find ESP32 WiFi address
3. Extract the FSP payload (strip IPv6 header, reverse the IPv6 adapter
compression)
4. Forward FSP payload to ESP32 via WiFi UDP
**ESP32 → fips0:**
1. Receive FSP payload from ESP32 via WiFi UDP
2. Look up source ESP32 in device registry → find its npub/NodeAddr
3. Wrap in IPv6 header (apply IPv6 adapter compression)
4. Write to fips0 TUN interface
The bridge handles the IPv6 ↔ FSP translation that the IPv6 adapter normally
does, but for leaf dependent addresses instead of the local node.
### Alternative: Direct SessionDatagram Relay
Instead of going through the IPv6 adapter on fips0, the bridge could
interact at the SessionDatagram level via a new fipsd control socket API:
```json
{"command": "send_session_datagram", "params": {"src": "npub1kitchen...", "dest": "npub1remote...", "payload": "<base64>"}}
```
And receive inbound datagrams via a subscription:
```json
{"command": "subscribe_leaf_datagrams"}
// fipsd streams: {"event": "session_datagram", "src": "npub1remote...", "dest": "npub1kitchen...", "payload": "<base64>"}
```
This avoids the IPv6 adapter entirely and operates at the raw FSP level.
It's cleaner but requires more fipsd changes.
## ESP32 Firmware
### What to Implement
The ESP32 needs only the Noise XK state machine and session transport:
```
esp32_fips_endpoint/
├── noise_symmetric.c — SymmetricState (MixHash, MixKey, EncryptAndHash, DecryptAndHash)
├── noise_symmetric.h
├── noise_xk.c — XK responder + initiator
├── noise_xk.h
├── noise_session.c — Transport encrypt/decrypt with counter
├── noise_session.h
├── noise_replay.c — Sliding window replay protection (2048-bit)
├── noise_replay.h
├── fips_identity.c — NodeAddr derivation from pubkey
├── fips_identity.h
├── bridge_transport.c — WiFi UDP communication with bridge
├── bridge_transport.h
├── bridge_protocol.c — Simple framing for bridge ↔ ESP32 messages
├── bridge_protocol.h
└── app_main.c — Application entry point
```
### Crypto Primitive Mapping
All primitives come from the existing `nostr_core_lib` (already on ESP32):
| Noise Operation | Primitive | nostr_core_lib Function |
|----------------|-----------|------------------------|
| DH | secp256k1 ECDH | `ecdh_shared_secret()` |
| Keygen | secp256k1 | `nostr_ec_public_key_from_private_key()` |
| Hash | SHA-256 | `nostr_sha256_init/update/final()` |
| KDF | HKDF-SHA256 | `nostr_hkdf_extract()` + `nostr_hkdf_expand()` |
| AEAD | ChaCha20-Poly1305 | `nostr_chacha20poly1305_encrypt/decrypt()` |
| Random | CSPRNG | `nostr_platform_random()` |
### Noise XK Responder Implementation
The responder processes the three-message handshake:
```
Protocol: Noise_XK_secp256k1_ChaChaPoly_SHA256
Pre-message: ← s (responder's static pubkey known to initiator)
msg1 (→ e, es):
- Receive initiator ephemeral e_init (33 bytes compressed secp256k1)
- MixHash(e_init)
- es = ECDH(s_local, e_init) ← uses responder's static nsec
- MixKey(es)
msg2 (← e, ee):
- Generate ephemeral keypair e_resp
- MixHash(e_resp.pub)
- ee = ECDH(e_resp, e_init)
- MixKey(ee)
- EncryptAndHash(epoch) ← 8-byte random startup epoch
- Output: e_resp.pub (33) + encrypted_epoch (24) = 57 bytes
msg3 (→ s, se):
- DecryptAndHash(encrypted_static) → initiator's pubkey
- se = ECDH(e_resp, s_init)
- MixKey(se)
- DecryptAndHash(encrypted_epoch) → initiator's epoch
- Split() → (send_cipher, recv_cipher)
```
**Message sizes** (from [`noise/mod.rs`](../src/noise/mod.rs:76)):
- XK msg1: 33 bytes (ephemeral only)
- XK msg2: 33 + 24 = 57 bytes (ephemeral + encrypted epoch)
- XK msg3: 49 + 24 = 73 bytes (encrypted static + encrypted epoch)
**secp256k1 parity normalization** (from
[session layer spec](../docs/design/fips-session-layer.md:259)): The
pre-message hash uses even parity (`0x02` prefix), and ECDH hashes only the
x-coordinate. This must match the Rust implementation exactly.
### Memory Budget
| Component | RAM | Notes |
|-----------|-----|-------|
| Noise XK handshake state | ~256 B | Temporary during handshake |
| Session keys (per session) | ~128 B | send_cipher + recv_cipher + counters |
| Replay window (per session) | ~256 B | 2048-bit bitmap |
| Max concurrent sessions | 2-4 | ~512 B1 KB total |
| WiFi UDP buffers | ~3 KB | RX + TX |
| Application state | ~512 B | GPIO, sensor data |
| **Total** | **~4-5 KB** | vs ~22 KB for full c_fips |
### Bridge ↔ ESP32 Protocol
Simple length-prefixed binary framing over WiFi UDP:
```
┌──────────┬──────────┬──────────┬─────────────────┐
│ msg_type │ session │ payload │ payload │
│ (1 byte) │ id (4B) │ len (2B) │ (variable) │
└──────────┴──────────┴──────────┴─────────────────┘
```
Message types:
| Type | Direction | Description |
|------|-----------|-------------|
| 0x01 | Bridge → ESP | Handshake msg1 (SessionSetup payload) |
| 0x02 | ESP → Bridge | Handshake msg2 (SessionAck payload) |
| 0x03 | Bridge → ESP | Handshake msg3 (SessionMsg3 payload) |
| 0x04 | ESP → Bridge | Handshake msg2 for ESP-initiated session |
| 0x10 | Bridge → ESP | Encrypted data (FSP ciphertext) |
| 0x11 | ESP → Bridge | Encrypted data (FSP ciphertext) |
| 0x20 | ESP → Bridge | Initiate session to remote npub |
| 0xF0 | Bridge → ESP | Device registration ack |
| 0xF1 | ESP → Bridge | Device heartbeat / keepalive |
| 0xFF | Either | Error |
The session_id field allows multiplexing multiple concurrent sessions
(e.g., two different remote peers talking to the same ESP32).
## Security Properties
| Property | Status | Mechanism |
|----------|--------|-----------|
| End-to-end encryption | ✅ | Noise XK between ESP32 and remote peer |
| Forward secrecy | ✅ | Ephemeral DH in Noise XK handshake |
| Mutual authentication | ✅ | Both parties prove nsec ownership |
| Replay protection | ✅ | Counter-based sliding window |
| Bridge sees plaintext | ❌ Never | Bridge relays opaque ciphertext |
| Bridge has ESP32 nsec | ❌ Never | ESP32 holds its own keys |
| WiFi eavesdropper | Sees ciphertext only | Noise XK encrypts from ESP32 |
### Trust Model
The bridge daemon is trusted to:
- Faithfully relay packets (availability, not confidentiality)
- Not drop or delay packets maliciously
- Correctly register leaf dependents with fipsd
The bridge daemon is NOT trusted with:
- ESP32 private keys
- Plaintext application data
- Session key material
This is the same trust model as any FIPS transit node — it can observe
metadata (timing, packet sizes, which sessions are active) but cannot read
content.
### WiFi Segment Security
Traffic between ESP32 and bridge over WiFi is already Noise XK encrypted
(the session keys are established between ESP32 and the remote peer, and
the bridge just relays ciphertext). A WiFi eavesdropper sees encrypted
blobs. WPA2/WPA3 on the WiFi network provides an additional layer but is
not required for confidentiality.
## Implementation Phases
### Phase 1: fipsd Modifications
Changes to the Rust fipsd codebase:
- [ ] Add leaf dependent check to `handle_session_datagram()` local delivery path
- [ ] Add control socket commands: `add_leaf_dependent`, `remove_leaf_dependent`, `list_leaf_dependents`
- [ ] Add control socket command to prime identity cache for an npub
- [ ] Add session layer pass-through for leaf dependent addresses (skip local Noise XK, deliver raw SessionDatagram to fips0)
- [ ] Test: verify SessionDatagram for leaf dependent arrives on fips0
### Phase 2: Bridge Daemon (Linux)
New Rust binary `fips-bridge`:
- [ ] YAML config parser (device registry)
- [ ] Control socket client (connect to fipsd, register leaf dependents)
- [ ] Identity computation (npub → NodeAddr → IPv6 address)
- [ ] fips0 TUN packet interception for leaf dependent addresses
- [ ] WiFi UDP server for ESP32 communication
- [ ] Bridge protocol framing (msg_type + session_id + payload)
- [ ] Bidirectional relay: fips0 ↔ WiFi UDP
- [ ] Session tracking (map session_id to remote NodeAddr)
- [ ] Device keepalive / liveness detection
- [ ] Test: bridge relays handshake bytes between fipsd and a test client
### Phase 3: ESP32 Noise XK Endpoint
C firmware for ESP32:
- [ ] Port Noise SymmetricState from Rust to C (MixHash, MixKey, EncryptAndHash, DecryptAndHash)
- [ ] Implement Noise XK responder (process msg1, generate msg2, process msg3)
- [ ] Implement Noise XK initiator (generate msg1, process msg2, generate msg3)
- [ ] Implement session transport encrypt/decrypt with counter
- [ ] Implement replay window (2048-bit sliding bitmap)
- [ ] Implement NodeAddr derivation (SHA-256 of pubkey, truncate to 16 bytes)
- [ ] Implement bridge protocol framing
- [ ] Implement WiFi UDP transport to bridge
- [ ] Write test vectors validated against Rust Noise implementation
- [ ] Test: ESP32 completes Noise XK handshake with Rust FIPS node via bridge
### Phase 4: Integration Testing
- [ ] End-to-end test: remote FIPS node → mesh → fipsd → bridge → ESP32
- [ ] Verify bridge never sees plaintext (instrument bridge, check logs)
- [ ] Test session rekey (if supported)
- [ ] Test ESP32 reboot recovery (epoch mismatch detection)
- [ ] Test multiple concurrent sessions to same ESP32
- [ ] Test multiple ESP32 devices behind one bridge
- [ ] Latency measurement: added overhead of bridge relay
### Phase 5: Example Application
- [ ] ESP32 light switch example (GPIO control via FIPS mesh)
- [ ] ESP32 sensor example (push temperature readings to mesh)
- [ ] Documentation: setup guide for bridge + ESP32
## Comparison With Alternatives
| Approach | ESP32 Code | Own npub | E2E Encrypted | Forward Secrecy | Mesh Addressable | Bridge Trust |
|----------|-----------|----------|---------------|-----------------|-----------------|--------------|
| LAN Gateway (existing) | None | ❌ | ❌ (gateway sees plaintext) | ❌ | ❌ (outbound only) | Full trust |
| Full c_fips port | ~8,000 LOC | ✅ | ✅ | ✅ | ✅ | N/A |
| NIP-44 bridge | ~200 LOC | ✅ (app-level) | ✅ (NIP-44) | ❌ | ❌ (via bridge npub) | Relay only |
| **Noise XK bridge** | **~1,200 LOC** | **✅** | **✅ (Noise XK)** | **✅** | **✅** | **Relay only** |
## Key Design Decisions
### Why Noise XK Instead of NIP-44
Both use the same crypto primitives (secp256k1 ECDH + ChaCha20-Poly1305).
The difference:
- **NIP-44**: Static-static ECDH. No forward secrecy. Simpler (no state
machine). But the remote peer needs a NIP-44-aware application — standard
FIPS nodes don't speak NIP-44.
- **Noise XK**: Ephemeral-static DH. Forward secrecy. The ESP32 speaks the
same session protocol as every other FIPS node — full interoperability
with unmodified remote peers.
Noise XK means the remote peer doesn't need to know or care that the
destination is an ESP32 behind a bridge. It's a standard FIPS session.
### Why Bridge Instead of Full Port
The full c_fips port requires implementing: Noise IK (link layer), Noise XK
(session layer), spanning tree, bloom filters, wire format, routing, peer
management, and an event loop. The bridge approach requires only Noise XK
on the ESP32 — everything else runs on Linux where it's already implemented
and tested.
### Why fips0 TUN Instead of Custom API
Using fips0 TUN for the bridge ↔ fipsd interface means:
- No new IPC protocol to design
- Standard IPv6 packet handling
- Works with existing fipsd IPv6 adapter
- Bridge can be developed and tested independently
The tradeoff is IPv6 header overhead on the local path, but this is
negligible for IoT command/response traffic.
## References
- [fips-session-layer.md](../docs/design/fips-session-layer.md) — Noise XK handshake, session lifecycle
- [fips-mesh-layer.md](../docs/design/fips-mesh-layer.md) — Link encryption, peer authentication
- [fips-gateway.md](../docs/design/fips-gateway.md) — Existing LAN gateway (outbound only)
- [fips-ipv6-adapter.md](../docs/design/fips-ipv6-adapter.md) — IPv6 adapter, identity cache, TUN
- [fips-bloom-filters.md](../docs/design/fips-bloom-filters.md) — Bloom filter parameters
- [fips-configuration.md](../docs/design/fips-configuration.md) — YAML config reference
- [fips-wire-formats.md](../docs/design/fips-wire-formats.md) — Wire format reference
- [Noise Protocol Framework](https://noiseprotocol.org/) — Noise XK pattern specification
- [fips-esp32-minimal-port.md](fips-esp32-minimal-port.md) — Original full c_fips port plan (superseded by this approach for constrained devices)

View File

@@ -0,0 +1,374 @@
# FIPS ESP32 Internet Tunnel Proxy — Architecture Plan (Option 2)
## Goal
Enable an ESP32 device on **any WiFi network** to reach FIPS mesh destinations by **npub** without running the full FIPS mesh stack locally.
This option introduces a public **Tunnel Proxy** service:
- ESP32 connects to proxy over an internet-safe authenticated tunnel transport.
- Proxy joins/runs on FIPS mesh as a full node.
- ESP32 keeps end-to-end session cryptography (Noise XK to remote FIPS destination).
- Proxy handles mesh routing, peer management, tree/bloom/discovery internals.
## Why This Exists
The full embedded FIPS port is powerful but heavy (link layer, mesh routing, discovery, bloom filters, tree state, wire framing, etc.).
A plain internet gateway is simpler, but it degrades the core FIPS property (routing/addressing by npub identity at the endpoint layer).
This tunnel-proxy design targets a middle path:
- Keep ESP32 lightweight enough for constrained firmware.
- Preserve destination addressing by npub.
- Keep session-layer crypto endpoint-bound (ESP32 ↔ remote destination).
## Non-Goals
- Replacing native full-node FIPS implementations.
- Guaranteeing relay-free NAT traversal between two arbitrary private endpoints.
- Eliminating all trust in proxy operators (proxy still affects availability/metadata).
## High-Level Architecture
```mermaid
graph TD
subgraph "ESP32 Device (Any WiFi)"
APP["Nostr App / Publisher"]
XK["Noise XK Session Engine\n(init/responder)"]
TP["Tunnel Transport Client\n(TLS/Noise-IK stream)"]
TPC["Tunnel Proxy Client Protocol\n(frame mux)"]
end
subgraph "Public Internet"
NET["Internet"]
end
subgraph "Proxy Host (Public)"
TPS["Tunnel Proxy Server\n(auth + session map)"]
BR["Leaf Session Broker\nXK frame relay"]
FIPSD["fipsd full node\n(tree+bloom+routing)"]
CTL["Control API to fipsd"]
TUN["fips0 TUN / session ingress-egress"]
end
subgraph "FIPS Mesh"
REMOTE["Remote FIPS Destination\n(e.g., Nostr relay node)"]
end
APP <--> XK
XK <--> TPC
TPC <--> TP
TP <--> NET
NET <--> TPS
TPS <--> BR
BR <--> CTL
BR <--> TUN
TUN <--> FIPSD
FIPSD <--> REMOTE
```
## Core Design Choice
### Split responsibilities
**ESP32 keeps:**
- Device identity key material (nsec/npub).
- Noise XK session handshake + transport encryption/decryption.
- Application framing (Nostr over WS-like stream abstraction).
**Proxy keeps:**
- FIPS mesh connectivity (peer transport, IK links, routing, tree/bloom/discovery).
- Mapping device identity to routable leaf representation.
- Reliable relay of opaque session datagrams between mesh and device tunnel.
## Trust Model
### Proxy is trusted for
- Availability (may drop, delay, or reorder packets).
- Correct registration and routing relay behavior.
- Access control enforcement for admitted devices.
### Proxy is NOT trusted for
- Device private key custody.
- Session key custody.
- Decrypting application payload that is Noise XK-protected end-to-end.
### Metadata exposure
Proxy can observe timing, volume, target identity metadata, and liveness.
## Identity and Addressing Model
Each ESP32 has its own FIPS identity (npub/node_addr). The proxy maintains a **device registry**:
- `device_npub -> node_addr -> active_tunnel_connection`
- `device_npub -> auth policy`
- optional aliases/tags for operations
The proxy registers device identities as managed dependents in the local FIPS node state so mesh traffic can be routed to those addresses.
## Tunnel Transport Options
## Option A (recommended first): TLS + mutual device authentication
- ESP32 opens outbound TCP/TLS to public proxy endpoint.
- Device authenticates with challenge signed by device nsec (or static client cert mapped to npub).
- Easy NAT/firewall compatibility.
## Option B (future): Noise IK tunnel
- Lower overhead and cryptographic consistency with FIPS stack.
- More custom implementation and operational tooling required.
### Requirement regardless of option
Tunnel transport itself must provide anti-injection and replay-resilient framing for control messages.
## Tunnel Control Protocol (TCP over TLS/Noise)
Length-prefixed binary messages, multiplexed per logical FIPS session.
Frame header proposal:
```text
+---------+-----------+------------+-------------+
| type(1) | flags(1) | stream_id | len (4 BE) |
| | | (4 bytes) | |
+---------+-----------+------------+-------------+
| payload (len bytes)
+---------------------------------------------------+
```
### Message types
- `0x01 AUTH_HELLO` (device -> proxy)
- `0x02 AUTH_CHALLENGE` (proxy -> device)
- `0x03 AUTH_PROOF` (device -> proxy)
- `0x04 AUTH_OK` / `0x05 AUTH_FAIL`
- `0x10 OPEN_SESSION` (device requests remote npub + service/port)
- `0x11 SESSION_OPENED` / `0x12 SESSION_OPEN_FAIL`
- `0x20 XK_MSG1` (initiator msg1 payload)
- `0x21 XK_MSG2`
- `0x22 XK_MSG3`
- `0x30 DATA_CIPHERTEXT`
- `0x40 CLOSE_SESSION`
- `0x50 HEARTBEAT`
- `0xFF ERROR`
`stream_id` is tunnel-local multiplexing key for concurrent destination sessions.
## Session/Data Path
### Outbound (ESP32 initiates to remote npub)
1. ESP32 authenticates tunnel.
2. ESP32 sends `OPEN_SESSION(remote_npub, service_port)`.
3. Proxy resolves/registers route context in fipsd.
4. ESP32 sends XK msg1 via `XK_MSG1(stream_id)`.
5. Proxy wraps as FIPS SessionSetup toward remote node_addr.
6. Mesh delivers to remote destination.
7. Remote returns msg2/msg3 path via mesh; proxy forwards raw handshake bytes.
8. ESP32 and remote complete XK and derive session keys.
9. ESP32 sends encrypted application ciphertext frames; proxy relays opaquely.
### Inbound (remote initiates to ESP32)
1. Remote sends SessionSetup to device node_addr.
2. fipsd local-delivers to proxy-managed dependent handler.
3. Proxy forwards raw msg1 to active ESP32 tunnel.
4. ESP32 generates msg2; proxy relays back through mesh.
5. msg3 relayed to ESP32; session established.
6. Ongoing ciphertext data relay in both directions.
## Required fipsd/Proxy Integration Surface
Two viable server-side integration styles:
## Style 1: TUN interception (lower daemon changes)
- Proxy reads/writes on `fips0` for managed dependent addresses.
- Performs IPv6 adapter translation where needed.
- Faster prototype but more packet-shim complexity.
## Style 2: SessionDatagram control API (preferred long-term)
Expose explicit APIs/events from fipsd:
- `register_managed_leaf(npub)` / `unregister_managed_leaf(npub)`
- `send_session_datagram(src,dst,payload)`
- `subscribe_managed_leaf_datagrams()`
This avoids tunnel-proxy dependence on TUN packet plumbing and keeps relay at protocol-native layer.
## Security Requirements
- Device authentication bound to npub identity.
- Per-device ACLs (allowed destinations, rate limits, max sessions).
- Replay protection for tunnel control plane (nonce/window or strict monotonic sequence per connection).
- Idle/session timeout and forced re-auth.
- Structured audit logs (auth, open/close, errors, throttles).
## Failure Handling
### Device disconnect
- Mark device offline.
- Keep managed leaf registration with short grace period (configurable).
- Reject inbound session opens until re-authenticated.
### Proxy restart
- Rebuild device registry from persisted config/state.
- Re-register managed leaves in fipsd.
- Active sessions re-established lazily.
### Mesh path failures
- Surface CoordsRequired/PathBroken/MtuExceeded upstream as control events.
- ESP32 may retry setup using same stream_id or a new stream_id.
## Performance Targets (initial)
- Tunnel auth handshake: < 1.5 s on typical WiFi.
- Session open (XK) median: < 2.5 s internet-to-mesh.
- Added proxy relay overhead: < 15 ms median per direction (excluding mesh path latency).
- Concurrent sessions per device: 2 initially (expand later).
## Implementation Phases
## Phase 0 — Protocol/spec freeze
- Finalize tunnel frame format and auth method.
- Decide Style 1 (TUN) vs Style 2 (SessionDatagram API) for first implementation.
- Define compatibility/version negotiation field in `AUTH_HELLO`.
Deliverables:
- `docs/design/fips-esp32-tunnel-proxy-spec.md` (wire-level spec)
- test vectors for auth and framing
## Phase 1 — Proxy MVP (single device, outbound only)
- Public listener with TLS auth challenge.
- Static registry for one device npub.
- `OPEN_SESSION` + outbound XK relay only.
- Relay encrypted data frames.
Validation:
- ESP32 publishes kind1 to FIPS-hosted relay by remote npub.
## Phase 2 — Inbound initiation and multi-device
- Inbound SessionSetup forwarding to correct device tunnel.
- Multi-device concurrent tunnels.
- Session map eviction and collision handling.
Validation:
- Remote node initiates to ESP32 identity successfully.
## Phase 3 — Robustness and controls
- ACL, rate-limits, heartbeat, reconnect backoff.
- Observability metrics and trace IDs.
- Integration tests under packet loss/reorder.
Validation:
- 24h soak test with intermittent WiFi loss.
## Phase 4 — Hardening and rollout
- Key rotation procedures.
- Proxy HA strategy (active/passive or sticky shard by device npub).
- Operational docs and incident runbooks.
## ESP32 Firmware Delta vs Current Projects
Relative to existing internet nostr client:
- Add tunnel client transport module.
- Add auth challenge signer using existing nsec.
- Keep/use Noise XK session module from bridge plan.
- Keep Nostr message construction/signing path unchanged.
Relative to current full leaf port effort:
- Remove need for on-device tree/bloom/discovery/link IK stack.
- Keep only session crypto + app protocol + tunnel mux.
## Suggested Repository Layout (new components)
```text
proxy/
fips-tunnel-proxy/
src/
auth.rs
frame.rs
session_broker.rs
fips_integration.rs
main.rs
esp32/
tunnel_client/
tunnel_transport.c
tunnel_transport.h
tunnel_proto.c
tunnel_proto.h
tunnel_auth.c
tunnel_auth.h
```
## Test Plan
### Unit
- Frame parser fuzz tests.
- Auth challenge/response verification tests.
- Session map state machine tests.
### Integration
- ESP32 simulator/client -> proxy -> test fipsd node -> relay service.
- Inbound and outbound XK handshake relay.
- Packet loss/reorder simulation on tunnel and mesh links.
### End-to-end
- Real ESP32 from multiple WiFi networks (home, hotspot, enterprise guest).
- Publish + readback nostr events over FIPS path by destination npub.
## Migration Strategy
1. Keep current full-leaf client as reference implementation.
2. Implement tunnel-proxy MVP and switch one test device.
3. Compare reliability/latency and code size.
4. Decide whether to deprecate full on-device mesh stack for production devices.
## Risks and Mitigations
- **Proxy centralization risk** -> support multiple proxies and device failover list.
- **Tunnel protocol complexity creep** -> strict scope; avoid embedding mesh semantics in tunnel layer.
- **Auth/key misuse** -> use explicit key separation labels and domain-separated signatures.
- **Operational single point of failure** -> health checks, watchdog restart, persistent registry snapshots.
## Success Criteria
- ESP32 can publish to FIPS-hosted nostr relay from arbitrary internet WiFi.
- Destination addressed by npub at session setup interface.
- ESP32 firmware complexity materially reduced versus full mesh port.
- Proxy never requires device private keys and cannot decrypt end-to-end session payload.
## References
- [fips-session-layer.md](../fips/docs/design/fips-session-layer.md)
- [fips-mesh-layer.md](../fips/docs/design/fips-mesh-layer.md)
- [fips-gateway.md](../fips/docs/design/fips-gateway.md)
- [fips-esp32-noise-bridge.md](fips-esp32-noise-bridge.md)
- [fips-esp32-minimal-port.md](fips-esp32-minimal-port.md)

View File

@@ -0,0 +1,312 @@
# FIPS on Raspberry Pi Zero 2W — Deployment Plan
## Goal
Deploy the Rust FIPS mesh daemon on a Raspberry Pi Zero 2W as a personal mesh endpoint. The Pi joins the FIPS network, gets its own npub identity, and lets applications on the device (or forwarded from the LAN) reach other FIPS nodes by npub address.
## Hardware Profile
| Spec | Pi Zero 2W | FIPS Requirement |
|------|-----------|-----------------|
| SoC | BCM2710A1 — quad-core Cortex-A53 @ 1 GHz | Rust binary, Noise crypto |
| Architecture | ARMv8-A (aarch64) | First-class target in FIPS |
| RAM | 512 MB | ~10-30 MB typical for FIPS |
| WiFi | 802.11 b/g/n (2.4 GHz) | UDP/TCP transports |
| Bluetooth | BT 4.2 / BLE | BLE L2CAP transport (with BlueZ) |
| Storage | microSD | ~5-10 MB for binaries + config |
| GPIO | 40-pin header | Not needed for FIPS |
| USB | 1x micro-USB OTG | USB Ethernet adapter for wired transport |
**Verdict: Fully capable.** The Pi Zero 2W has the same Cortex-A53 cores found in many OpenWrt routers already running FIPS, with 4-16x more RAM than typical router targets.
## Strategy
1. **Cross-compile** from an x86_64 dev machine targeting `aarch64-unknown-linux-gnu` (Raspberry Pi OS) or `aarch64-unknown-linux-musl` (static binary)
2. **Deploy via systemd tarball** — the simplest packaging path for Raspberry Pi OS
3. **Configure as leaf-only** — minimize resource usage by delegating routing to an upstream peer
4. **Optionally enable BLE** — the onboard Bluetooth can bridge nearby BLE mesh devices
## OS Choice
```mermaid
graph TD
A["Pi Zero 2W"] --> B{"Which OS?"}
B -->|Simplest| C["Raspberry Pi OS 64-bit<br/>Debian bookworm based"]
B -->|Router-style| D["OpenWrt<br/>bcm27xx/bcm2710 target"]
C --> E["Deploy via .deb or systemd tarball"]
D --> F["Deploy via .ipk package"]
C --> G["Best for: personal endpoint,<br/>BLE bridge, development"]
D --> H["Best for: headless relay,<br/>network appliance"]
```
**Recommended: Raspberry Pi OS 64-bit** — it's the path of least resistance. Debian-based, so the existing FIPS `.deb` packaging or systemd tarball works directly. BlueZ is available from apt for BLE support.
## Architecture
```mermaid
graph TD
subgraph "Pi Zero 2W"
subgraph "FIPS Daemon"
NODE["fips<br/>mesh node"]
TUN["fips0 TUN<br/>fd00::/8 IPv6"]
DNS["DNS responder<br/>127.0.0.1:5354"]
end
subgraph "Transports"
WIFI_UDP["UDP via WiFi<br/>0.0.0.0:2121"]
WIFI_TCP["TCP via WiFi<br/>0.0.0.0:8443"]
BLE_T["BLE L2CAP<br/>optional"]
end
subgraph "Applications"
SSH["SSH to npub.fips"]
PING["ping6 npub.fips"]
APP["Any IPv6 app"]
end
end
subgraph "FIPS Mesh"
UPSTREAM["Upstream Peer<br/>handles routing"]
OTHER["Other Mesh Nodes"]
end
APP --> TUN
SSH --> TUN
PING --> TUN
TUN --> NODE
NODE --> WIFI_UDP
NODE --> WIFI_TCP
NODE --> BLE_T
WIFI_UDP --> UPSTREAM
WIFI_TCP --> UPSTREAM
UPSTREAM --> OTHER
DNS --> NODE
```
## Build & Deploy Steps
### Phase 1: Cross-Compile
Build on an x86_64 host targeting the Pi's architecture.
**Option A — Dynamic linking (Raspberry Pi OS):**
```bash
# Install cross-compilation toolchain
sudo apt install gcc-aarch64-linux-gnu
# Add Rust target
rustup target add aarch64-unknown-linux-gnu
# Build FIPS
cd fips
cargo build --release --target aarch64-unknown-linux-gnu --features gateway
```
**Option B — Static binary via cargo-zigbuild (any Linux):**
```bash
cargo install cargo-zigbuild
cargo zigbuild --release --target aarch64-unknown-linux-musl --features gateway
```
**Option C — Use the existing tarball script:**
```bash
./packaging/systemd/build-tarball.sh --target aarch64-unknown-linux-gnu
# Output: deploy/fips-<version>-linux-aarch64.tar.gz
```
**Option D — Build a .deb package:**
```bash
cargo install cargo-deb
./packaging/debian/build-deb.sh --target aarch64-unknown-linux-gnu
# Output: deploy/fips_<version>_arm64.deb
```
### Phase 2: Pi OS Setup
On the Pi Zero 2W running Raspberry Pi OS 64-bit:
```bash
# Update system
sudo apt update && sudo apt upgrade -y
# Install TUN support (usually already present)
sudo modprobe tun
# For BLE transport (optional):
sudo apt install bluez libdbus-1-dev
# Create fips user and directories
sudo useradd -r -s /usr/sbin/nologin fips
sudo mkdir -p /etc/fips /run/fips
sudo chown fips:fips /run/fips
```
### Phase 3: Install FIPS
**From tarball:**
```bash
# Copy tarball to Pi
scp deploy/fips-*-linux-aarch64.tar.gz pi@raspberrypi:
# On the Pi:
sudo tar -xzf fips-*-linux-aarch64.tar.gz -C /
sudo systemctl daemon-reload
sudo systemctl enable fips
```
**From .deb:**
```bash
scp deploy/fips_*_arm64.deb pi@raspberrypi:
sudo dpkg -i fips_*_arm64.deb
```
### Phase 4: Configure
Create `/etc/fips/fips.yaml`:
```yaml
# Pi Zero 2W — leaf endpoint configuration
node:
identity:
persistent: true # Keep same npub across restarts
leaf_only: true # Delegate routing to upstream peer
cache:
coord_size: 1000 # Reduced from default 50,000
identity_size: 500 # Reduced from default 10,000
limits:
max_peers: 8 # Reduced from default 128
max_connections: 16 # Reduced from default 256
tun:
enabled: true
name: fips0
mtu: 1280
dns:
enabled: true
bind_addr: "127.0.0.1"
port: 5354
transports:
udp:
bind_addr: "0.0.0.0:2121"
tcp:
bind_addr: "0.0.0.0:8443"
# Uncomment for BLE mesh bridging:
# ble:
# adapter: "hci0"
# mtu: 2048
# advertise: true
# scan: true
# auto_connect: true
# accept_connections: true
peers:
- npub: "npub1..." # Your upstream peer's npub
alias: "upstream"
addresses:
- transport: udp
addr: "x.x.x.x:2121" # Upstream peer's address
connect_policy: auto_connect
```
### Phase 5: Start & Verify
```bash
# Start the daemon
sudo systemctl start fips
# Check status
sudo systemctl status fips
journalctl -u fips -f
# Verify mesh connectivity
fipsctl show_node
fipsctl show_peers
# Test connectivity to another node
ping6 npub1<other-node>.fips
```
## Resource Tuning for 512 MB RAM
The default FIPS configuration targets servers/desktops with ample RAM. For the Pi Zero 2W, these config overrides keep memory usage well under 50 MB:
| Parameter | Default | Pi Recommended | Why |
|-----------|---------|---------------|-----|
| `node.cache.coord_size` | 50,000 | 1,000 | Small mesh, few destinations |
| `node.cache.identity_size` | 10,000 | 500 | Few unique peers |
| `node.limits.max_peers` | 128 | 8 | Leaf node, 1-3 peers typical |
| `node.limits.max_connections` | 256 | 16 | Proportional to max_peers |
| `node.limits.max_pending_inbound` | 1,000 | 50 | Leaf gets few inbound |
| `node.leaf_only` | false | true | Delegate routing overhead |
## Transport Options on Pi Zero 2W
### WiFi (UDP + TCP) — Primary
The onboard 2.4 GHz WiFi connects to your home network and reaches internet-connected FIPS peers. This is the default and requires no extra hardware.
### BLE — Local Mesh Bridge
The Pi's Bluetooth 4.2 supports BLE L2CAP, which FIPS uses for short-range mesh links. Build with `--features ble` and install BlueZ. The Pi could bridge BLE-only devices (like ESP32 nodes) into the wider mesh.
```mermaid
graph LR
ESP1["ESP32<br/>BLE only"] -->|"BLE L2CAP"| PI["Pi Zero 2W<br/>BLE + WiFi"]
ESP2["ESP32<br/>BLE only"] -->|"BLE L2CAP"| PI
PI -->|"UDP over WiFi"| MESH["Internet Mesh<br/>Peers"]
```
### USB Ethernet — Wired Option
A USB Ethernet adapter on the OTG port enables the raw Ethernet transport (EtherType 0x2121) for direct L2 mesh links to nearby nodes without IP overhead.
### Tor — Anonymous Overlay
Install the `tor` package and configure the Tor transport for anonymous mesh participation. Adds latency (200ms-2s RTT) but works.
## Leaf-Only Mode Status
The `node.leaf_only` config flag exists in the codebase and is wired through:
- Config parsing: `node.leaf_only` in `fips.yaml`
- Node constructor: `Node::leaf_only()`
- Bloom filter support: upstream peer includes leaf identity via `add_leaf_dependent()`
- Status reporting: visible in `fipsctl show_node` and `fipstop` dashboard
The design docs mark it as "under development" — the config flag and data structures are in place, but the full behavioral changes (suppressing tree participation, simplified routing) may not all be active yet. Even without leaf-only mode fully enabled, a standard FIPS node with 2-3 peers runs comfortably on the Pi Zero 2W's hardware.
## Monitoring
The Pi can run `fipstop` (the TUI dashboard) over SSH for live monitoring:
```bash
ssh pi@raspberrypi
fipstop
```
Or use `fipsctl` for scriptable status checks:
```bash
fipsctl show_node # Node identity and state
fipsctl show_peers # Connected peers
fipsctl show_links # Active transport links
fipsctl show_routing # Spanning tree and bloom filter state
fipsctl show_sessions # End-to-end encrypted sessions
```
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|-----------|
| Compilation on Pi is slow | Dev friction | Cross-compile from x86_64 host |
| 512 MB RAM under heavy transit load | OOM if forwarding for many nodes | Use `leaf_only: true` + reduced cache sizes |
| WiFi-only networking (no Ethernet) | Single transport, no redundancy | Add USB Ethernet adapter for second transport |
| BLE range limited (~10m) | Short-range only | Use as bridge, not primary transport |
| `leaf_only` mode not fully implemented | May still participate in routing | Acceptable — Pi has enough resources for light routing |
| SD card wear from logging | Card failure over time | Use `journald` rate limiting, consider tmpfs for `/run/fips` |
## Future Enhancements
- **Headless auto-provisioning**: Script that generates identity on first boot and registers with a known upstream peer
- **BLE bridge mode**: Dedicated configuration for bridging BLE-only devices into the mesh
- **Watchdog integration**: Use systemd watchdog to auto-restart on hang
- **Read-only root filesystem**: Protect SD card, write only to `/etc/fips/` and `/run/fips/`