security: remove all infrastructure and internal process material from the repo
Demo images / Build & push demo images (push) Failing after 2m13s
Demo images / Build & push demo images (push) Failing after 2m13s
The repo is source code and guidelines only. Nothing about how Archipelago's own fleet is run, or how the team works, stays in it. Untracked (kept on disk, gitignored) — 250 files: - .planning/ (199) and loop/ — internal development process - fleet operations tooling that targets specific nodes: deploy-to-target, deploy-tailscale, deploy-config-defaults, setup-target-dev, setup-aiui-server, setup-https-dev, debug-frontend, node-profile, fleet-fips-pair/unpair, image-recipe/sync-from-live.sh - image-recipe/INTEGRATION-GUIDE.md and docs/multinode-testing-plan.md, both of which are live-server workflow and fleet node inventories - the Phase 10 on-node verification and evidence records, which cite .planning/ as their evidence base KEY-05-ENTROPY-ENFORCEMENT.md was initially moved out with the other Phase 10 docs and then put back: it is cited as normative rationale from ten places in the codebase, including core/clippy.toml, which bans rand::thread_rng and points at it for the reason. That makes it a guideline, not an internal record. Node names removed from source (48 occurrences across comments, manifests and test fixtures): archi-dev-box, archy-x250*, shorty-s, framework-pt, zaza-optiplex, archi-thinkpad. Comments keep the engineering context and the date, which is what carried the meaning; the machine name did not. Three of those were live test values rather than comments and were replaced with valid stand-ins, not prose: two mDNS hostnames and a mesh peer name. An earlier pass substituted "a test node" into a hostname assertion, producing an invalid hostname; caught and fixed as test-node.local. Wipe mechanism: .local-only/manifest.txt inventories every local-only path and .local-only/wipe.sh deletes them on one confirmation, refusing to touch anything git still tracks. Both are themselves untracked, so the public repo does not carry a map of internal filenames. Verified: cargo check -p archipelago --all-features clean; archipelago-container 75/75 tests pass; appOrigin vitest 7/7; audit-secrets 5/5; every relative link in tracked markdown resolves (0 broken). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cc00884b98
commit
6ba0599639
@@ -1,205 +0,0 @@
|
||||
# HANDOFF — deploy companion APK 0.5.1 (vc21) to nodes
|
||||
|
||||
**For: the agent on archi-dev-box.** User-reported failure this evening:
|
||||
pairing flow on Framework PT — downloaded the companion from the node's
|
||||
QR, then the pairing scan didn't work. The APK the node serves predates
|
||||
today's scanner fixes; the pipeline below gets the fixed build into the
|
||||
user's hands.
|
||||
|
||||
## What changed on main today (all merged)
|
||||
|
||||
- **Pairing-scanner fix** (`QrScannerOverlay.kt`): ZXing decode attempts are
|
||||
frame-gated (~7/s, was every frame — the CPU contention made the preview
|
||||
stutter badly enough to never decode) and PreviewView uses TextureView (no
|
||||
more black flash on open). This is the likely fix for "doesn't scan".
|
||||
- Three-finger menu gesture (was two-finger, collided with scroll) + one-time
|
||||
teaching overlay ~2 min after login.
|
||||
- Native wallet QR scanner behind `window.ArchipelagoQr` + WebView file-chooser
|
||||
support; web scan modal hands live scanning to it.
|
||||
- npub-keyed saved servers (pairing contract item 1, PR #106).
|
||||
- Served APK refreshed: `neode-ui/public/packages/archipelago-companion.apk`
|
||||
is now **0.5.3 / versionCode 23**. On top of the 0.5.1 scanner fixes it
|
||||
guarantees dual-path peering — the node's LAN endpoint (direct p2p, npub-
|
||||
keyed dial hints) AND the Archipelago public anchor (vps2, baked into the
|
||||
app so even an old node's QR can't leave the phone LAN-only) — and fixes
|
||||
the two field failures from the user's 5G test (screenshots, 21:54):
|
||||
- **Mesh VPN no longer kills the phone's internet** — the IPv6-only TUN
|
||||
never called `allowFamily(AF_INET)`, so Android blocked all IPv4 while
|
||||
the mesh was up. Now allowed (+ `allowBypass`).
|
||||
- **Off-LAN connect works** — `connect()` no longer hard-fails when the
|
||||
scanned LAN IP doesn't answer; it brings the mesh up and probes the
|
||||
node's ULA (`meshIp`) with retries before reporting failure.
|
||||
|
||||
## What to do
|
||||
|
||||
1. Redeploy the web-ui bundle from current main to the active nodes —
|
||||
web root `/opt/archipelago/web-ui/` (NOT a neode-ui/ subfolder), all
|
||||
nodes the user pairs against, at minimum the one Framework PT scans.
|
||||
2. Verify the served artifact really updated:
|
||||
`curl -sI http://<node>/packages/archipelago-companion.apk` — size should
|
||||
change (~27 MB build of 2026-07-23), or pull it and check
|
||||
`aapt dump badging` shows `versionCode='21' versionName='0.5.1'`.
|
||||
3. The demo stack gets its images from CI (run 100 pushed today with the new
|
||||
web bundle) — confirm the Portainer stack re-pulled, or trigger its
|
||||
redeploy, so the demo QR also serves vc21.
|
||||
4. **Node side is half the 5G story**: away-from-home reachability needs the
|
||||
NODE connected to the public anchor too. On Framework PT (and any test
|
||||
node): deploy current main (node-side npub-first `fips.pair-info`), then
|
||||
verify `sudo -n fipsctl show status` reports the anchor connected —
|
||||
`fips.reconnect` RPC if not. A phone can dial the anchor perfectly and
|
||||
still fail if the node never enrolled with it.
|
||||
5. Re-test the user's exact flows with **vc24** (updates any older install in
|
||||
place): (a) pair ON the LAN, then switch the phone to 5G — the UI must
|
||||
come up via the mesh ULA; (b) pair while ALREADY on 5G (never on the
|
||||
node's LAN) — scan, VPN consent, and the connect must succeed through
|
||||
the anchor.
|
||||
|
||||
## Live diagnosis update (22:30–22:50, phone on adb — Mac agent)
|
||||
|
||||
vc23 on-device testing found and fixed the phone-side blocker, and narrowed
|
||||
what remains to the node side. State as of vc24:
|
||||
|
||||
- **Fixed: TUN reader died at startup.** Android hands the VpnService fd over
|
||||
non-blocking; the fips fork's blocking reader thread treats EAGAIN as fatal
|
||||
("TUN read error … Try again (os error 11)") — so mesh sessions came up but
|
||||
NO packet ever entered the tunnel. archy-fips-core now forces the fd
|
||||
blocking before `start_with_tun_fd`. Verified on-device: reader survives,
|
||||
and the 30s anchor-link flap disappeared with it (stable 8+ min on 5G).
|
||||
- **Fixed: VPN marked not-metered** (`setMetered(false)`) — Android 10+
|
||||
defaults VPNs to metered, putting the phone into data-restricted behaviour
|
||||
while the mesh is up. Note the user's phone also has system **always-on
|
||||
VPN** enabled for the app (`always_on_vpn_app`), a Settings-side toggle.
|
||||
- **Verified good on-device**: peer store has node (LAN udp/tcp hints) + vps2
|
||||
anchor; saved server is npub-keyed with ULA; anchor session establishes
|
||||
from 5G in ~6s; VPN is bypassable, VALIDATED, only fd00::/8 routed.
|
||||
- **REMAINING BLOCKER (node side)**: from the phone (app uid), ping6 and
|
||||
HTTP to the node's ULA `fd79:1aa:b9e9:4c9f:1f80:5376:9385:1824` get no
|
||||
reply — packets enter the mesh, nothing returns. Phone↔anchor works, so
|
||||
suspect phone-fork ↔ node-daemon session/routing mismatch (FIPS wire
|
||||
format is not stable across revs; phone pins fips-native fork 46494a74).
|
||||
From Framework PT please capture:
|
||||
- `fipsctl show status` (daemon version + anchor state)
|
||||
- `fipsctl show sessions` and `show bloom` while the phone pings
|
||||
- `ping6 <phone ULA fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586>` from the
|
||||
node (tests the reverse path)
|
||||
- `ip addr show fips0` + confirm the web server listens on `[::]:80`
|
||||
Report whether the node ever sees a session attempt from
|
||||
`npub132c5whrsa6ccs0eylcpzaejq9uxul5ldvczz0axq78dh7fxkqj9st4uvzu`.
|
||||
|
||||
## Node-side diagnosis complete (23:00–23:30, archi-dev-box agent)
|
||||
|
||||
Chain of findings, each verified live:
|
||||
|
||||
1. **FIXED: nginx had no IPv6 listener anywhere** — every shipped config
|
||||
listened on 0.0.0.0 only, so `http://[<ULA>]` could NEVER connect on any
|
||||
node, ever. Live-fixed on framework-pt + .116, canonical conf + bootstrap
|
||||
self-heal shipped (`1e89362e`), heal binary deployed. ULA HTTP verified
|
||||
answering on both nodes (local + over-mesh).
|
||||
2. Firewall clean, fd00::/8 routes correct both ends, wire compat proven
|
||||
(node + vps2 anchor both run fips 0.4.1 rev 15db6471db — the latest
|
||||
upstream stable; nothing newer exists).
|
||||
3. **THE REMAINING PROBLEM IS MESH SESSION PATH QUALITY.** From the vps2
|
||||
anchor — a DIRECT connected peer — `GET /health` on the node's ULA takes
|
||||
**15–17 s per request and intermittently fails outright** (nginx logs
|
||||
show 499 client-gave-up then 200; TCP SYN-retransmit backoff signature).
|
||||
Session MMP is wildly asymmetric: node→vps2 srtt 204 ms, **vps2→node
|
||||
srtt 4270 ms** — on a direct link whose raw RTT is 4 ms. Session traffic
|
||||
is not riding the direct link; it appears to route through the ~1271-node
|
||||
public tree (node's tree root is the public 00001a8c, depth 8; the node's
|
||||
log also shows chronic "Discovery lookup timed out" for other targets).
|
||||
4. The phone's npub never appears in the node's sessions — consistent with
|
||||
discovery/handshake dying on the same degraded tree path, and the app's
|
||||
~8 s probe window being far smaller than the observed 15 s+ first-request
|
||||
latency even on the GOOD path.
|
||||
|
||||
### Recommendations
|
||||
|
||||
- **App side (Mac agent):** widen the ULA probe/connect window to ≥30 s
|
||||
with retransmit-friendly pacing, and PRE-WARM the mesh session (start
|
||||
pinging the node ULA as soon as the VPN is up, decoupled from the UI
|
||||
probe) so the WebView hits a warm session.
|
||||
- **Infra decision (user):** consider detaching the fleet from the public
|
||||
v0l mesh — private tree rooted at the vps2 anchor (drop the legacy
|
||||
185.18.221.160 seed anchor fleet-wide AND vps2's public peering). A
|
||||
2-hop private tree would make session paths ride the direct links and
|
||||
should collapse latency to ms. Trade-off: no reachability to/from the
|
||||
broader public mesh.
|
||||
- **Upstream:** report the direct-peer session-path asymmetry to
|
||||
jmcorgan/fips (0.4.1).
|
||||
|
||||
## App-side recommendations implemented (23:30–23:50, Mac agent — 0.5.5/vc25)
|
||||
|
||||
- Connect probe: mesh ULA now probed inside a **60s budget** with 15s
|
||||
per-phase timeouts (rides out TCP retransmit backoff), replacing the old
|
||||
~8s window.
|
||||
- **Session pre-warm**: the VPN service starts probing every saved node ULA
|
||||
the moment the tunnel is up (5s cadence for the first minute, then a 60s
|
||||
keep-warm tick) — discovery/handshake cost is paid in the background, and
|
||||
the session never idles out while the mesh is connected.
|
||||
- (A phone-side ping test in this window still showed zero replies — that
|
||||
measurement predated the vps2 daemon restart below and is superseded.)
|
||||
|
||||
## RESOLVED — root cause was vps2's degraded daemon, NOT the public tree (23:45)
|
||||
|
||||
The privatize-the-mesh recommendation above is WITHDRAWN. Final diagnosis:
|
||||
vps2's fips daemon (3 days uptime, 0.2% CPU, idle box) had internally
|
||||
degraded — EVERY link it carried showed ~4.5 s RTT (even to peers 30 ms
|
||||
away), and since the anchor sits on the phone↔node path, everything through
|
||||
it inherited that. `systemctl restart fips` on vps2 restored link RTTs to
|
||||
40–340 ms, and anchor→node mesh HTTP went from 14–17 s (intermittent hard
|
||||
fails) to a steady **165–275 ms**. Node↔node direct sessions were always
|
||||
fine (.116→framework-pt ULA HTTP: 894 ms cold, sub-second warm) — the
|
||||
user's read was correct.
|
||||
|
||||
Actions taken: dead legacy anchor (185.18.221.160) removed from
|
||||
framework-pt + .116 seed files (fleet keeps vps2 + public-mesh membership
|
||||
via vps2 — we stay in the open mesh); fresh daemons on both nodes;
|
||||
**vps2 fips now has RuntimeMaxSec=1d + Restart=always** so a wedged anchor
|
||||
daemon can never rot for days again. Report the slow-degradation behaviour
|
||||
upstream (jmcorgan/fips, 0.4.1): long-running daemon in a ~1400-node mesh
|
||||
accumulates multi-second link latency at idle CPU, cleared by restart.
|
||||
|
||||
Phone side: vc25's 60 s probe + pre-warm now has a millisecond-latency mesh
|
||||
to work with. Ready for the user's 5G test.
|
||||
|
||||
## NEXT (00:05, Mac agent → dev-box agent): app direct ports are IPv4-only over the mesh
|
||||
|
||||
The kiosk loads over the ULA now — but opening any APP dies with
|
||||
`ERR_CONNECTION_REFUSED` at `http://[<ULA>]:<port>/`. User-hit first on
|
||||
**Bitcoin Knots (:8334)**, and it will be every catalog app: the web UI
|
||||
builds app URLs from the current host + the app's DIRECT port (Direct Port
|
||||
Rule), and container-published ports only bind 0.0.0.0. Verified:
|
||||
`192.168.63.249:8334` → HTTP 200 (nginx), ULA:8334 → refused. Same disease
|
||||
as your :80 nginx fix, one layer down.
|
||||
|
||||
Fix must cover EVERY catalog app port and survive app install/remove. Two
|
||||
shapes; pick what fits the container layer best:
|
||||
|
||||
1. **IPv6 publish at the container layer** — publish on `[::]` too
|
||||
(pasta/rootless podman support address-specific `-p`), wired into the
|
||||
container manager so new apps inherit it; or
|
||||
2. **Host-side v6→v4 forwarders** — generated nginx `stream {}` (or
|
||||
systemd-socket) units: `listen [::]:<port>` → `127.0.0.1:<port>`, one per
|
||||
catalog app port, regenerated on app install/remove, boot-time
|
||||
self-healed like the :80 fix. Keeps the Direct Port Rule URL contract
|
||||
without touching containers.
|
||||
|
||||
Either way: extend the bootstrap self-heal, and verify from the MESH side
|
||||
(curl the ULA on 2–3 app ports incl. :8334 from vps2 or .116) — not just
|
||||
from the LAN.
|
||||
|
||||
## DONE (00:30, dev-box agent): app direct ports live over the mesh
|
||||
|
||||
Shape 2-variant implemented INSIDE the backend (`mesh_ports.rs`, `2ad57c63`):
|
||||
a reconcile loop mirrors every public IPv4 listener (>=1024, bound 0.0.0.0,
|
||||
no existing IPv6 any-listener) as a v6-ONLY `[::]:<port>` forwarder to
|
||||
`127.0.0.1:<port>`, following `/proc/net/tcp*` every 15s — so app
|
||||
install/remove and hardcoded companion ports (bitcoin-ui :8334) are covered
|
||||
with zero container changes and no generated units; self-healing because it
|
||||
lives in the binary. Strictly ADDITIVE: IPv4/LAN/Tor paths untouched, v6only
|
||||
cannot intercept v4, foreign IPv6 listeners win.
|
||||
|
||||
Verified FROM THE MESH (vps2 → node ULA): :8334 HTTP 200 (466ms),
|
||||
:18083 200 (306ms), :50002 200 (239ms); LAN :8334 still 200. Deployed to
|
||||
framework-pt + .116 (binary sha 52ac0d8a…). Direct-port apps should now
|
||||
open in the companion over 5G.
|
||||
+8
-14
@@ -54,7 +54,6 @@ archy/
|
||||
│ ├── vite.config.ts
|
||||
│ └── package.json
|
||||
├── scripts/ # Deployment and utility scripts
|
||||
│ ├── deploy-to-target.sh # Main deploy script
|
||||
│ ├── first-boot-containers.sh # ISO first-boot setup
|
||||
│ └── run-tests.sh # CI test runner
|
||||
├── image-recipe/ # ISO build configuration
|
||||
@@ -91,19 +90,16 @@ The dev server at `http://localhost:8100` uses a mock backend.
|
||||
|
||||
### Deploying Changes
|
||||
|
||||
Release and host-integration builds should run on Linux. The deploy script rsyncs
|
||||
source to a configured Linux target and builds there.
|
||||
Release and host-integration builds should run on Linux. Build the backend and
|
||||
frontend on the target, or cross-build and copy the artifacts across:
|
||||
|
||||
```bash
|
||||
# Deploy to the configured primary target (builds backend + frontend, restarts services)
|
||||
./scripts/deploy-to-target.sh --live
|
||||
|
||||
# Deploy to both configured targets
|
||||
./scripts/deploy-to-target.sh --both
|
||||
cd core && cargo build --release
|
||||
cd neode-ui && npm ci && npm run build
|
||||
```
|
||||
|
||||
The deploy script:
|
||||
1. Rsyncs source to the server
|
||||
A deploy then:
|
||||
1. Copies the build output to the node
|
||||
2. Builds Rust backend on the server (`cargo build --release`)
|
||||
3. Builds Vue frontend (`npm run build`)
|
||||
4. Copies artifacts to production paths
|
||||
@@ -203,7 +199,6 @@ async myAction(params: { name: string }): Promise<{ ok: boolean; result: string
|
||||
### 5. Deploy and Test
|
||||
|
||||
```bash
|
||||
./scripts/deploy-to-target.sh --live
|
||||
curl -X POST http://<node-host>/rpc/v1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-b "archipelago_session=YOUR_SESSION" \
|
||||
@@ -312,6 +307,5 @@ mod tests {
|
||||
1. Create a feature branch: `git checkout -b feature/my-feature`
|
||||
2. Make changes following the standards above
|
||||
3. Test locally: `cd neode-ui && npm test`
|
||||
4. Deploy to dev server: `./scripts/deploy-to-target.sh --live`
|
||||
5. Verify on your configured development target
|
||||
6. Commit with conventional format: `feat: add my feature`
|
||||
4. Verify on an Archipelago node
|
||||
5. Commit with conventional format: `feat: add my feature`
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# Multinode / Fleet Testing Plan (separate from the single-node gate)
|
||||
|
||||
> **Scope split (2026-06-22):** the production test gate (
|
||||
> `tests/lifecycle/TESTING.md`) is now a **single-node criterion on .228**. Verifying the same
|
||||
> lifecycle matrix across the rest of the fleet (.198 and the other testers) lives HERE and is run
|
||||
> **after** the .228 single-node gate is green. This is intentionally NOT a blocker on the .228 gate.
|
||||
|
||||
## Why split it out
|
||||
|
||||
The lifecycle gate must be **run ON the node under test** — its bitcoin/companion/orphan/endpoint
|
||||
checks use local `podman`/`systemctl`/`bitcoin-cli`/`curl`, not RPC to a remote host. Running it from
|
||||
one host against another silently tests the *runner*. So "multinode" isn't "point the harness at N
|
||||
hosts" — it's "run the on-node gate on each host," plus the genuinely cross-node concerns (federation,
|
||||
mesh, transport, sync) that a single node can't exercise.
|
||||
|
||||
## How to run the gate on another node
|
||||
|
||||
Bats + jq usually aren't installed on ISO nodes. Bootstrap (one-time per node):
|
||||
|
||||
```
|
||||
# from a host that has them (e.g. .116):
|
||||
dpkg -L bats | grep -E '^/usr/(bin|lib|libexec)' | tar czf /tmp/bats.tgz -P -T - $(which jq)
|
||||
tar czf /tmp/tests.tgz -C <repo> tests/lifecycle
|
||||
scp /tmp/bats.tgz /tmp/tests.tgz <node>:/tmp/
|
||||
# on the node:
|
||||
sudo tar xzf /tmp/bats.tgz -P -C / # bats (jq here is dynamically linked — may need libs)
|
||||
sudo curl -fsSL -o /usr/local/bin/jq \
|
||||
https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 && sudo chmod +x /usr/local/bin/jq
|
||||
mkdir -p /tmp/lifecycle-run && tar xzf /tmp/tests.tgz -C /tmp/lifecycle-run
|
||||
cd /tmp/lifecycle-run/tests/lifecycle
|
||||
ARCHY_HOST=127.0.0.1 ARCHY_SCHEME=https ARCHY_PASSWORD=<node pw> \
|
||||
ARCHY_ALLOW_DESTRUCTIVE=1 ARCHY_ITERATIONS=5 nohup ./run-gate.sh > /tmp/gate.log 2>&1 &
|
||||
```
|
||||
|
||||
## Per-node preconditions (learned on .228)
|
||||
|
||||
- **Bitcoin must be fully synced + archival** (`initialblockdownload:false`, `pruned:false`).
|
||||
test 83 reads the *real* `getblockchaininfo`, not the UI's headers-height. A node mid-IBD will
|
||||
cascade-fail electrumx/lnd/btcpay/mempool even though the apps run.
|
||||
- **Backends should be proper installs** (in `manifest_ids`), not adopted plain-podman left over
|
||||
from ad-hoc `package.start`/cascade churn — otherwise companion self-heal and quadlet checks skew.
|
||||
- **No stale per-app nginx proxy targets.** e.g. `/app/lnd/` must point at the lnd-ui port (18083),
|
||||
not a stale `8081`. Repo code is correct; old node configs may be stale — re-check + regenerate.
|
||||
- **No orphan quadlet units** (e.g. a `home-assistant.container` whose ContainerName ≠ the real
|
||||
`homeassistant` container) — these wedge `systemctl --user` "activating" and fail the quadlet checks.
|
||||
|
||||
## Node roster (carry-over)
|
||||
|
||||
| Node | Role | Notes |
|
||||
|------|------|-------|
|
||||
| .228 | **single-node gate** (primary) | 14-app resilience node; bitcoin synced archival; gate GREEN. |
|
||||
| .198 | fleet verify | was weak/loaded (load ~3–5) + **bitcoin mid-IBD** at split time → must finish syncing first; sshd wedges under concurrent SSH (use ONE session; gate uses HTTPS RPC so fine). |
|
||||
| .5 / .120 | x250 testers (Tailscale) | flaky cellular; SSH via `tailscale nc` ProxyCommand. |
|
||||
| .116 | dev/validation | local repo; its own bitcoin may be mid-IBD — do NOT treat as a gate target unless synced. |
|
||||
|
||||
## Cross-node concerns (only a multinode setup can test)
|
||||
|
||||
- Federation sync (Tor/FIPS transports), DID/contact federation, peer file fetch.
|
||||
- Mesh (Meshtastic/MeshCore) + mesh-AI gating.
|
||||
- Dual-ecash federation validation + networking-sats routing.
|
||||
- DHT / iroh swarm distribution (origin-always-wins) once that dep lands.
|
||||
|
||||
## Sequence
|
||||
|
||||
1. Get the **.228 single-node gate green 5×** (master plan §5/§6) — DONE/in progress.
|
||||
2. THEN: bring each fleet node to the preconditions above; run the on-node gate 5× per node.
|
||||
3. THEN: the cross-node suites (federation/mesh/transport), tracked here.
|
||||
|
||||
This plan does not gate the v1.7.x single-node criterion; it is the next layer.
|
||||
@@ -1,82 +0,0 @@
|
||||
# QR scanner snappiness — research + companion-dev handover
|
||||
|
||||
*2026-07-29. Owner: web side = node repo (this doc's "web" items); native side =
|
||||
companion app dev (Mac). Backlog origin: "optimise
|
||||
companion QR scan (quicker start/decode, low-light)".*
|
||||
|
||||
## Where scanning happens today
|
||||
|
||||
| Path | Stack | Used when |
|
||||
|---|---|---|
|
||||
| Web live scan | nimiq `qr-scanner` 1.4.x over `getUserMedia`, in `WalletScanModal.vue` | HTTPS browsers / secure contexts |
|
||||
| Photo fallback | `<input capture>` photo → `BarcodeDetector` if present, else `qr-scanner.scanImage` multi-pass (`decodePhotoRobust`) | Plain-http (LAN) where `getUserMedia` doesn't exist |
|
||||
| Native scan | `ArchipelagoQr` JS bridge → companion's native scanner (0.5.22 fixed dense invoice QRs) | Inside the companion app |
|
||||
|
||||
## What makes it feel slow (ranked)
|
||||
|
||||
1. **Camera cold-start** — the stream starts only after the user reaches the
|
||||
scan pane; on phones `getUserMedia` + first frame is routinely 600–1500ms,
|
||||
and the native path pays a similar CameraX bind + ML Kit model cold-start.
|
||||
2. **Decode cadence** — web live scan was capped at 4 scans/sec (WebView
|
||||
preview lagged at 10/s when decoding on the JS worker). A hand-held code
|
||||
therefore waits up to 250ms *after* it's already sharp and centered.
|
||||
3. **Low light / focus hunting** — no torch control anywhere; no explicit
|
||||
continuous-focus request. Dense LN invoices need sharpness more than
|
||||
resolution.
|
||||
4. **Dense-QR decode budget** — big bolt11/catalog QRs push the JS decoder
|
||||
hard; the native ML Kit path is far better at these (proven by 0.5.22).
|
||||
|
||||
## Web side (node repo — can be done here)
|
||||
|
||||
- ✅ DONE (2026-07-29): scan at **10/s when `BarcodeDetector` exists** (Chrome/
|
||||
Android WebView decode natively — cheap), keep 4/s only for the JS-worker
|
||||
fallback.
|
||||
- **Pre-warm the camera**: start `getUserMedia` the moment the modal opens
|
||||
(action pane), not when the scan pane is reached — hide the preview until
|
||||
needed. Saves the entire cold-start from the user's perceived timeline.
|
||||
- **Torch toggle**: `qr-scanner` exposes `hasFlash()/turnFlashOn()` — add a 🔦
|
||||
button on the scan pane (it silently no-ops where unsupported).
|
||||
- **Continuous focus + modest resolution**: pass constraints
|
||||
`{ focusMode: 'continuous', width: { ideal: 1280 } }` — 720p-class frames
|
||||
start faster AND decode faster than 1080p+, with no loss for QR density
|
||||
that matters to us.
|
||||
- **Don't stop/start between panes**: returning from amount → scan currently
|
||||
re-inits the scanner; keep the (paused) stream alive while the modal lives.
|
||||
|
||||
## Native side (companion dev handover)
|
||||
|
||||
The `ArchipelagoQr` bridge overlay is the right architecture — these are
|
||||
tuning items inside the native scanner activity:
|
||||
|
||||
1. **Pre-warm CameraX + ML Kit**: bind the camera provider and instantiate
|
||||
`BarcodeScanning.getClient(...)` when the WebView *requests* the overlay —
|
||||
or even when the wallet modal opens (add a `ArchipelagoQr.prewarm()` bridge
|
||||
method; the web side will call it if present). ML Kit's first-inference
|
||||
model load is 100–300ms — pay it before the user aims.
|
||||
2. **Restrict formats**: `BarcodeScannerOptions` with `FORMAT_QR_CODE` only —
|
||||
skipping the other symbologies measurably cuts per-frame latency.
|
||||
3. **Analysis resolution ≈ 1280×720** with
|
||||
`ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST` — never queue stale frames;
|
||||
decode the newest one only.
|
||||
4. **Continuous autofocus + tap-to-focus** on the preview, and a **torch
|
||||
toggle** (low-light was an explicit user complaint).
|
||||
5. **`zoomRatio` nudge for small codes**: if no hit after ~2s, step zoom to
|
||||
1.5× — helps distant/small printed codes without user action.
|
||||
6. **Success haptic + instant dismiss**: vibrate on decode and close the
|
||||
overlay immediately; perceived speed is heavily back-loaded.
|
||||
7. Optional: **ML Kit `enableAllPotentialBarcodes` off** and skip inverted
|
||||
scans unless first pass fails (inverted QRs are rare; halves work).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Cold open → first successful scan of a normal invoice QR in **< 2s** on the
|
||||
companion app, **< 3s** in a mobile browser.
|
||||
- Dense (700+ char) bolt11 QR decodes in **< 1.5s** once framed, both paths.
|
||||
- Dim-room scan succeeds with the torch toggle without leaving the scanner.
|
||||
|
||||
## Verification notes for whoever implements
|
||||
|
||||
- Measure with a timestamp log: overlay-requested → camera-first-frame →
|
||||
decode-success. The three deltas map 1:1 onto items above.
|
||||
- Web `BarcodeDetector` presence differs per WebView/Play-Services build —
|
||||
keep the JS-worker fallback path intact.
|
||||
@@ -1,7 +1,7 @@
|
||||
# The Bitcoin RPC proxy that stayed open after it was fixed
|
||||
|
||||
**Status:** code fix committed (`f6b5245b`); on-node verification recorded below.
|
||||
**Found:** 2026-08-02, archi-dev-box, while verifying `a05956c4` instead of assuming it.
|
||||
**Found:** 2026-08-02, a test node, while verifying `a05956c4` instead of assuming it.
|
||||
**Severity:** critical on any affected node — unauthenticated control of Bitcoin Core RPC
|
||||
through a proxy that injects the node's own credentials.
|
||||
|
||||
@@ -79,7 +79,7 @@ Deliberately narrow:
|
||||
survived. A new regression test pins the whole chain: stale conf in, gate present out,
|
||||
container restarted, nothing created.
|
||||
|
||||
## What actually closed it on archi-dev-box — and what that does NOT prove
|
||||
## What actually closed it on a test node — and what that does NOT prove
|
||||
|
||||
Sequence, from file mtimes, container start times and the daemon journal:
|
||||
|
||||
@@ -104,7 +104,7 @@ Tracked as broken window 15 — **since closed by the controlled test below.**
|
||||
|
||||
## Proving the delivery path on real hardware
|
||||
|
||||
Run on archi-dev-box, 2026-08-02 20:00–20:03 EDT, with operator approval. The point was to
|
||||
Run on a test node, 2026-08-02 20:00–20:03 EDT, with operator approval. The point was to
|
||||
prove the thing the incidental rebuild had made unprovable: that **reconcile itself**
|
||||
repairs this state, unaided.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,224 +0,0 @@
|
||||
# KEY-01 on-node verification — audit item C-6 and the F-01 refusal proof
|
||||
|
||||
**Status: INCOMPLETE — C-6 is NOT yet verified.**
|
||||
**Opened:** 2026-08-02 · **Phase:** 10 (key-material hardening) · **Plan:** 10-02
|
||||
**Probe:** `scripts/security/rpc-exposure-probe.sh`
|
||||
|
||||
This document records on-node evidence for
|
||||
`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` §6 item **C-6** ("Is the RPC endpoint
|
||||
reachable unauthenticated from the LAN?") and for the KEY-01 / F-01 refusal shipped by
|
||||
plan 10-01 (`core/archipelago/src/api/rpc/onboarding_gate.rs`, commit `879de59e`).
|
||||
|
||||
Nothing below is recorded unless it was actually executed and its output observed. Rows
|
||||
marked **NOT MEASURED** are open work, not assumptions. Per threat T-10-13 this document
|
||||
records node **labels** and status codes only — never raw LAN addresses, onion addresses
|
||||
or mesh ULAs, because this repository is being prepared for open-sourcing.
|
||||
|
||||
---
|
||||
|
||||
## Probe-method correction
|
||||
|
||||
**The audit's own C-6 command cannot detect the condition it claims to test. Do not
|
||||
re-derive this; it has now been checked against the code twice.**
|
||||
|
||||
`ENTROPY-SEED-AUDIT-2026-07-31.md:890-901` probes with `seed.status` and declares
|
||||
`200` a failure. But `seed.status` is **not** in `UNAUTHENTICATED_METHODS`
|
||||
(`core/archipelago/src/api/rpc/middleware.rs:5-38`, which lists `seed.generate`,
|
||||
`seed.verify`, `seed.restore` and `seed.save-encrypted` — not `seed.status`). An
|
||||
unauthenticated `seed.status` is therefore rejected at
|
||||
`core/archipelago/src/api/rpc/mod.rs:293` with a **401 by design**. The audit's "Fail:
|
||||
200" criterion can never fire, so the probe would report the unauthenticated surface as
|
||||
closed while F-01's actual door stands open.
|
||||
|
||||
`scripts/security/rpc-exposure-probe.sh` measures the two facts separately:
|
||||
|
||||
| Signal | Method | Why | Reading |
|
||||
|---|---|---|---|
|
||||
| **Exposure** | `auth.isOnboardingComplete` | genuinely unauthenticated (`middleware.rs:9`), read-only, no side effects | `200` = the unauthenticated RPC surface is reachable from this vantage point. This is the honest C-6 result. |
|
||||
| **Session enforcement** | `seed.status` | deliberately *not* allowlisted | `401` = the session check is working. Anything else is a worse finding than C-6 and halts the phase. |
|
||||
|
||||
The probe reports a reachable unauthenticated surface as `EXPOSED`, not `FAIL`: on the LAN
|
||||
this is the current expected posture, and the purpose of C-6 is to **measure** the surface,
|
||||
not to assert it is already closed.
|
||||
|
||||
---
|
||||
|
||||
## C-6 — unauthenticated RPC reachability
|
||||
|
||||
### Result table
|
||||
|
||||
| Transport | Label | `health` | `auth.isOnboardingComplete` (exposure) | `seed.status` (enforcement) | Status |
|
||||
|---|---|---|---|---|---|
|
||||
| Loopback | `loopback` | 200 | **200 — EXPOSED** | **401 — PASS** | measured 2026-08-02 |
|
||||
| Node's own LAN address, probed *from the node itself* | `self-lan-ip` | 200 | **200 — EXPOSED** | **401 — PASS** | measured 2026-08-02 |
|
||||
| LAN, from a second machine | `lan` | — | — | — | **NOT MEASURED** |
|
||||
| Tor onion | `tor` | — | — | — | **NOT MEASURED** |
|
||||
| FIPS mesh ULA, from a peer node | `mesh` | — | — | — | **NOT MEASURED** |
|
||||
|
||||
**`seed.status` returned `401` on every vantage point actually tested.** No
|
||||
stop-the-plan condition was observed.
|
||||
|
||||
### Why the two measured rows are NOT a C-6 result
|
||||
|
||||
Both runs originated **on the node under test**. Packets to the node's own addresses are
|
||||
delivered by the local stack and never traverse the LAN, so neither run exercises the
|
||||
external path an attacker would use, and neither run passes through any host or upstream
|
||||
filtering that applies only to foreign packets. They are recorded because they establish
|
||||
two real facts — the probe works against a live daemon, and session enforcement is intact
|
||||
— but C-6 asks specifically whether a **different machine** can reach the surface, and
|
||||
that question is still open.
|
||||
|
||||
### Verbatim probe output (measured rows)
|
||||
|
||||
```
|
||||
$ bash scripts/security/rpc-exposure-probe.sh --target 127.0.0.1 --scheme http --port 80 --label loopback
|
||||
RPC exposure probe — label=loopback endpoint=http://127.0.0.1:80
|
||||
audit item C-6 · KEY-01 (F-01) · read-only mode
|
||||
|
||||
[loopback] health 200 REACHABLE endpoint answers from this vantage point
|
||||
[loopback] auth.isOnboardingComplete 200 EXPOSED unauthenticated RPC surface IS reachable from here (C-6 result)
|
||||
[loopback] seed.status 401 PASS session enforcement active for non-allowlisted methods
|
||||
[loopback] auth.isOnboardingComplete (/rpc/) 404 NOT-EXPOSED alternate proxy path did not answer 200
|
||||
exit=0
|
||||
```
|
||||
|
||||
```
|
||||
$ bash scripts/security/rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label self-lan-ip
|
||||
RPC exposure probe — label=self-lan-ip endpoint=http://<node-lan-ip>:80
|
||||
audit item C-6 · KEY-01 (F-01) · read-only mode
|
||||
|
||||
[self-lan-ip] health 200 REACHABLE endpoint answers from this vantage point
|
||||
[self-lan-ip] auth.isOnboardingComplete 200 EXPOSED unauthenticated RPC surface IS reachable from here (C-6 result)
|
||||
[self-lan-ip] seed.status 401 PASS session enforcement active for non-allowlisted methods
|
||||
[self-lan-ip] auth.isOnboardingComplete (/rpc/) 404 NOT-EXPOSED alternate proxy path did not answer 200
|
||||
exit=0
|
||||
```
|
||||
|
||||
### Corroborating host state (observed, but NOT a substitute for the LAN measurement)
|
||||
|
||||
Recorded because it predicts the LAN result and tells the operator what to expect:
|
||||
|
||||
- nginx listens on **`0.0.0.0:80` and `[::]:80`** (`ss -ltn`), i.e. on every interface,
|
||||
not on loopback only. The daemon itself is bound loopback-only on `127.0.0.1:5678`, so
|
||||
all external reachability is via nginx.
|
||||
- The host packet filter does **not** block port 80: `iptables -S INPUT` is
|
||||
`-P INPUT ACCEPT` with a single jump into Tailscale's chain, and the `nft` ruleset
|
||||
contains only Tailscale's `ts-input`/`ts-forward` chains — no rule matching tcp/80.
|
||||
|
||||
Together these make an `EXPOSED` LAN result very likely. **That is a prediction, not a
|
||||
measurement, and C-6 stays open until a second machine produces the status code.**
|
||||
|
||||
### Incidental finding — `/rpc/` is not a second door
|
||||
|
||||
`auth.isOnboardingComplete` on nginx's `location /rpc/` block
|
||||
(`image-recipe/configs/nginx-archipelago.conf:192`) returned **404** from both vantage
|
||||
points. The block proxies the full URI to the backend, which only routes `/rpc/v1`, so
|
||||
the unauthenticated surface is reachable through exactly one path. This narrows F-01's
|
||||
exposure surface by one path and should be re-checked if the nginx config changes.
|
||||
|
||||
---
|
||||
|
||||
## KEY-01 refusal check — NOT PERFORMED
|
||||
|
||||
**Requirement:** on a node running 10-01's gate, an unauthenticated `seed.restore`
|
||||
carrying attacker-supplied words is refused, and `identity/node_key` and
|
||||
`identity/nostr_secret` are byte-identical afterwards.
|
||||
|
||||
**Blocker — no node in the fleet is running 10-01's gate yet.** Verified on the dev-box
|
||||
rather than assumed:
|
||||
|
||||
```
|
||||
$ ls -l /usr/local/bin/archipelago
|
||||
-rwxr-xr-x 1 root root 53437536 Aug 2 06:37 /usr/local/bin/archipelago
|
||||
$ git log -1 --format='%H %ci' 879de59e
|
||||
879de59eccb489d590c8e0fca6ae79098df68200 2026-08-02 13:05:35 -0400
|
||||
$ grep -qa "Not supported: this node is already provisioned" /usr/local/bin/archipelago \
|
||||
&& echo PRESENT || echo ABSENT
|
||||
ABSENT
|
||||
```
|
||||
|
||||
The installed binary was built at 06:37; 10-01 landed at 13:05 the same day, and the
|
||||
gate's refusal string is absent from the running binary. A `--destructive` run against
|
||||
this node would therefore **not** be refused — it would replace the node's identity. The
|
||||
dev-box is a live dev-pair deploy target in real use, so the run was not made.
|
||||
|
||||
**This check is blocked on deployment, which the phase brief explicitly excludes from
|
||||
this plan.** It cannot be closed by any amount of work inside the repository.
|
||||
|
||||
---
|
||||
|
||||
## Fresh-node onboarding non-regression — NOT PERFORMED
|
||||
|
||||
**Requirement:** a genuinely un-onboarded instance completes the whole wizard with 10-01's
|
||||
gate in place (the anti-brick proof for correctness trap 1 and the D-03a signal
|
||||
correction), then refuses `seed.restore` immediately afterwards.
|
||||
|
||||
**Blocker — no un-onboarded instance exists.** The intended harness is shape (A) of
|
||||
`.planning/todos/pending/2026-08-01-archi-dev-box-as-fresh-test-node-without-iso.md`
|
||||
(a second daemon under its own `ARCHIPELAGO_DATA_DIR`/`ARCHIPELAGO_BIND`/
|
||||
`ARCHIPELAGO_PORT_OFFSET`), and that todo is still **pending** — the harness has not been
|
||||
built. It would additionally need a binary built from `879de59e` or later, which the
|
||||
running daemon is not.
|
||||
|
||||
Note for whoever builds it: that todo records that several constants ignore
|
||||
`ARCHIPELAGO_DATA_DIR` and point at `/var/lib/archipelago` literally
|
||||
(`bitcoin_rpc.rs:10`, `container/lnd.rs:131`, `electrs_status.rs:15`,
|
||||
`api/rpc/package/pine_ha.rs:34-36`, `bootstrap.rs:242`, `disk_monitor.rs:41`), so a shape-A
|
||||
instance must not install Bitcoin, LND, electrumx or Pine/HA — it would read and write the
|
||||
live node's files. The onboarding walkthrough this check needs does not install apps, so
|
||||
the hazard is avoidable, not blocking.
|
||||
|
||||
---
|
||||
|
||||
## Pre-OTA fleet check carried over from 10-01
|
||||
|
||||
10-01's summary records a state that its gate makes unrecoverable: a node with
|
||||
`onboarding.json = {"complete": true}` but **no** `user.json` can no longer call
|
||||
`auth.setup`, and the recovery path needs a session it cannot create. Recovery is one SSH
|
||||
command (`rm /var/lib/archipelago/onboarding.json`), but the fleet must be checked
|
||||
**before** the OTA ships (D-10).
|
||||
|
||||
| Node label | `user.json` | `onboarding.json` | Verdict |
|
||||
|---|---|---|---|
|
||||
| dev-box | PRESENT | `{"complete": true}` | **safe** — provisioned normally; the gate refuses re-keying, which is the intent |
|
||||
| rest of fleet | — | — | **NOT CHECKED** |
|
||||
|
||||
Command to run per node:
|
||||
|
||||
```bash
|
||||
ls -l /var/lib/archipelago/user.json /var/lib/archipelago/onboarding.json 2>&1
|
||||
cat /var/lib/archipelago/onboarding.json 2>/dev/null
|
||||
```
|
||||
|
||||
A node is at risk only if `onboarding.json` says `complete: true` **and** `user.json` is
|
||||
absent.
|
||||
|
||||
---
|
||||
|
||||
## What is still required to close C-6 and KEY-01
|
||||
|
||||
Every item below needs an operator with fleet access; none can be done from the repository.
|
||||
|
||||
1. **LAN exposure.** From a second machine on the node's LAN:
|
||||
`bash scripts/security/rpc-exposure-probe.sh --target <node-lan-ip> --scheme http --port 80 --label lan`
|
||||
2. **Tor exposure.** `torsocks bash scripts/security/rpc-exposure-probe.sh --target <onion> --scheme http --port 80 --label tor`
|
||||
3. **Mesh exposure.** From a peer node over the FIPS mesh ULA:
|
||||
`bash scripts/security/rpc-exposure-probe.sh --target <fips-ula> --scheme http --port 80 --label mesh`
|
||||
(the peer listener allows `/rpc/v1` — `core/archipelago/src/server.rs:1270-1296` — so a
|
||||
`200` confirms the mesh half of F-01's reachability claim). An unreachable transport is
|
||||
recorded as `UNREACHABLE` with its error, never omitted.
|
||||
4. **Deploy 10-01 to a disposable node**, then, from a second machine:
|
||||
`bash scripts/security/rpc-exposure-probe.sh --target <disposable-node> --destructive --label refusal`
|
||||
with `sudo sha256sum /var/lib/archipelago/identity/node_key /var/lib/archipelago/identity/nostr_secret`
|
||||
captured on the node immediately before and after. The response must carry the
|
||||
`Not supported:` prefix and the two digests must match character for character.
|
||||
5. **Build shape (A)** and walk the wizard end to end on a 10-01 binary
|
||||
(intro → options → path → seed → seed-verify → did → identity → backup → verify → done,
|
||||
then set the password), reloading once on the seed screen to confirm the same 24 words
|
||||
return. No `Not supported:` and no `Rate limit exceeded` may appear at any point. Then
|
||||
re-run step 4 against that same instance to confirm the door closed behind onboarding.
|
||||
6. **Check the remaining fleet** for the `onboarding.json`-without-`user.json` state above.
|
||||
|
||||
Until items 1–3 are done, audit item **C-6 remains UNVERIFIED**. Until item 4 is done, the
|
||||
KEY-01 refusal is proven only by 10-01's unit tests against temp directories, never against
|
||||
a running daemon over HTTP.
|
||||
@@ -1,208 +0,0 @@
|
||||
# KEY-02 — build-host evidence for the rootfs identity strip
|
||||
|
||||
**Audit item:** C-4 of `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` (§868), which
|
||||
belongs to finding **F-03** (fail-open, never-retried first-boot secret regeneration over
|
||||
a fleet-shared rootfs).
|
||||
|
||||
**Status: ⛔ UNVERIFIED — awaiting a run on a real ISO build host.**
|
||||
|
||||
The code change is committed and unit-tested; the tar listing that proves its effect on a
|
||||
real build has not been produced yet, because it requires a build host with podman/docker
|
||||
and enough disk for a full rootfs rebuild. Do not read anything below the "Result" heading
|
||||
as a passing check until it is filled in.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Builder commit (Task 1) | `21043096` — fail-closed first-boot regeneration |
|
||||
| Builder commit (Task 2) | `408b328c` — rootfs identity strip |
|
||||
| Builder commit (follow-up) | single-producer unification, build-time generator assertion, self-heal timer |
|
||||
| Builder file | `image-recipe/_archived/build-auto-installer-iso.sh` (LIVE; `image-recipe/build-debian-iso.sh` execs it) |
|
||||
| Build host | _to be recorded_ |
|
||||
| Date run | _to be recorded_ |
|
||||
| RECIPE_HASH observed | _to be recorded — read it from the stamp file, see the caveat below_ |
|
||||
|
||||
---
|
||||
|
||||
## The expectation is deliberately INVERTED relative to the audit
|
||||
|
||||
This is the single most important thing to understand when comparing this document with the
|
||||
audit, and the reason it is stated before the commands rather than after.
|
||||
|
||||
The audit's C-4 entry says:
|
||||
|
||||
> **Expected:** SSH host keys and the TLS key **present** (they are baked — see
|
||||
> `build-auto-installer-iso.sh:345`, `:463-469`), `random-seed` **absent**, `machine-id`
|
||||
> absent or zero-length. Anything else changes F-03's severity.
|
||||
|
||||
That expectation described the **broken** state the audit found, and recording it was how the
|
||||
audit measured the size of F-03. Phase 10 plan 10-03 Task 2 removed that material. So:
|
||||
|
||||
**After this change, the audit's stated expectation is the FAILURE condition.** If SSH host
|
||||
keys or the TLS private key still appear in the tar, the strip layer did not run — most
|
||||
likely because a cached `archipelago-rootfs.tar` was reused. That is not a regression in the
|
||||
check; it is the check working.
|
||||
|
||||
The two negative findings the audit recorded are unchanged and must still hold:
|
||||
`var/lib/systemd/random-seed` absent, `etc/machine-id` absent or zero-length.
|
||||
|
||||
---
|
||||
|
||||
## Commands to run
|
||||
|
||||
Run all of these **on the build host**, from the repo root, on a checkout that contains
|
||||
commits `21043096` and `408b328c`.
|
||||
|
||||
### 1. Force a full rebuild
|
||||
|
||||
The strip layer lives inside the `RECIPE_HASH` region (between the `# STEP 1: Build complete
|
||||
root filesystem` and `# STEP 2: Build minimal installer` markers), so the hash changes and the
|
||||
cached tar is invalidated automatically. `--rebuild` is passed anyway so that a stale tar
|
||||
cannot mask the result for any reason:
|
||||
|
||||
```bash
|
||||
UNBUNDLED=1 bash image-recipe/build-debian-iso.sh --rebuild
|
||||
```
|
||||
|
||||
`UNBUNDLED=1` is mandatory per `CLAUDE.md` and project memory — the default env silently
|
||||
builds the wrong full-bundle variant.
|
||||
|
||||
### 2. List the identity artefacts in the shipped tar
|
||||
|
||||
`WORK_DIR` is `image-recipe/build/auto-installer`, so:
|
||||
|
||||
```bash
|
||||
tar -tvf image-recipe/build/auto-installer/archipelago-rootfs.tar \
|
||||
| grep -E 'etc/ssh/ssh_host|etc/machine-id|var/lib/systemd/random-seed|archipelago/ssl/archipelago'
|
||||
```
|
||||
|
||||
### 3. Expected result after this plan
|
||||
|
||||
- **no** `etc/ssh/ssh_host_*` entries at all
|
||||
- **no** `etc/archipelago/ssl/archipelago.key` and **no** `archipelago.crt`
|
||||
(the `etc/archipelago/ssl/` **directory** must still be present — the first-boot staging
|
||||
swap needs somewhere to land)
|
||||
- **no** `var/lib/systemd/random-seed`
|
||||
- `etc/machine-id` present with size **0**, or absent. Either satisfies "not shared"; record
|
||||
which one was actually observed rather than generalising.
|
||||
|
||||
Note on the TLS keypair specifically: it is now absent for two independent reasons, not one.
|
||||
The Dockerfile no longer generates it at all (that layer was removed so there is a single
|
||||
producer), *and* the strip layer still deletes it as belt-and-braces in case a future layer
|
||||
starts baking one. Seeing it present therefore means both defences were bypassed.
|
||||
|
||||
### 4. Confirm the provenance file rode along
|
||||
|
||||
```bash
|
||||
tar -tvf image-recipe/build/auto-installer/archipelago-rootfs.tar | grep rootfs-identity-stripped
|
||||
```
|
||||
|
||||
Expected: one entry, `opt/archipelago/rootfs-identity-stripped`. Its absence means the strip
|
||||
layer did not execute and the whole check is void.
|
||||
|
||||
### 5. Confirm the regeneration path and its self-heal timer are still shipped
|
||||
|
||||
This is the brick check, and it is not optional. A stripped rootfs whose first-boot
|
||||
generation script failed to ship would leave every flashed node with no SSH host key and
|
||||
nothing to create one. The timer is part of the same check: without it, a node whose
|
||||
generators fail every in-boot retry has no unattended way back.
|
||||
|
||||
```bash
|
||||
ls -l image-recipe/build/auto-installer/installer-iso/archipelago/scripts/first-boot-secrets.sh \
|
||||
image-recipe/build/auto-installer/installer-iso/archipelago/scripts/archipelago-first-boot-secrets.service \
|
||||
image-recipe/build/auto-installer/installer-iso/archipelago/scripts/archipelago-first-boot-secrets.timer
|
||||
```
|
||||
|
||||
Expected: all three present, `first-boot-secrets.sh` executable.
|
||||
|
||||
### 5b. Confirm the build-time generator assertion actually ran
|
||||
|
||||
The rootfs build fails outright if `openssl` or `ssh-keygen` is missing or non-executable,
|
||||
because that is the one way first-boot generation can fail deterministically — retries and
|
||||
reboots would never fix it, so it must never reach a node. A successful build therefore
|
||||
already proves the generators are present, and the build log says so:
|
||||
|
||||
```bash
|
||||
grep 'first-boot secret generators present' <build log>
|
||||
```
|
||||
|
||||
If you did not capture the log, assert it against the tar instead:
|
||||
|
||||
```bash
|
||||
tar -tvf image-recipe/build/auto-installer/archipelago-rootfs.tar \
|
||||
| grep -E 'usr/bin/(openssl|ssh-keygen)$'
|
||||
```
|
||||
|
||||
Expected: both present and mode `-rwxr-xr-x`.
|
||||
|
||||
### 6. Record the RECIPE_HASH the builder actually used
|
||||
|
||||
```bash
|
||||
cat image-recipe/build/auto-installer/archipelago-rootfs.recipe.sha256
|
||||
```
|
||||
|
||||
**Caveat — do not compute this hash from the repo file.** `image-recipe/build-debian-iso.sh`
|
||||
copies the archived builder to a temp path and rewrites its relative paths before exec'ing it,
|
||||
and `RECIPE_HASH` hashes `"$0"` — the rewritten copy. The hashed region contains 35 such
|
||||
rewritten path expressions, and `SCRIPT_DIR` is substituted with an absolute path, so the hash
|
||||
is specific to the build host and checkout location. For reference, hashing the region of the
|
||||
committed repo file directly gives `d2dc4df5427fe73d48227aab08cdf6debfe8dd554e6b18e3718f8d37ea9d675c`,
|
||||
which is **expected to differ** from the stamp above.
|
||||
|
||||
---
|
||||
|
||||
## Result
|
||||
|
||||
_Paste the raw output of steps 2, 4, 5 and 6 here, then set the status at the top of this
|
||||
document to VERIFIED with the date and build-host label._
|
||||
|
||||
```text
|
||||
(pending — not yet run on a build host)
|
||||
```
|
||||
|
||||
**Verdict:** _pending_
|
||||
|
||||
---
|
||||
|
||||
## What this does and does not prove
|
||||
|
||||
**Proves (once run):** the rootfs tar extracted verbatim onto every disk flashed from the ISO
|
||||
carries no SSH host key, no TLS private key and no populated machine-id — so a first-boot
|
||||
regeneration failure degrades to "no key, the service refuses to start" rather than
|
||||
"fleet-shared key, silently", which is the substance of F-03.
|
||||
|
||||
**Does not prove:** that two nodes flashed from the same ISO actually end up with different
|
||||
keys. That is audit item **C-3** (§779) and needs two physical machines; it remains
|
||||
separately UNVERIFIED. C-4 is a build-host check only.
|
||||
|
||||
### Guidance for C-3: SSH and TLS are now equally sharp signals
|
||||
|
||||
An earlier revision of this document said SSH host keys were the sharper divergence signal for
|
||||
C-3, because the installer had a per-install TLS fallback that would produce a differing cert
|
||||
even if first-boot generation had failed. **That asymmetry no longer exists.**
|
||||
|
||||
There is now exactly one producer of each secret — `gen_tls()` and `gen_ssh()` inside
|
||||
`first-boot-secrets.sh` — and no other code in the ISO build creates either. The Dockerfile no
|
||||
longer bakes a TLS keypair and the installer's "ensure SSL cert exists" block is gone. So for
|
||||
C-3, treat both the same way:
|
||||
|
||||
```bash
|
||||
# on each node
|
||||
ssh-keyscan -t ed25519 localhost 2>/dev/null | ssh-keygen -lf -
|
||||
openssl x509 -in /etc/archipelago/ssl/archipelago.crt -noout -fingerprint -sha256
|
||||
```
|
||||
|
||||
**Pass:** both fingerprints differ between the two nodes. **Fail:** either matches — a matching
|
||||
TLS fingerprint is now exactly as damning as a matching host key, whereas before it could have
|
||||
been explained away by the fallback.
|
||||
|
||||
Also check, on each node, that the run actually succeeded rather than merely being quiet:
|
||||
|
||||
```bash
|
||||
ls -l /var/lib/archipelago/.secrets-regenerated # present on a healthy node
|
||||
cat /var/lib/archipelago/first-boot-secrets.failed 2>&1 # absent on a healthy node
|
||||
systemctl status archipelago-first-boot-secrets.timer # enabled; the self-heal path
|
||||
```
|
||||
|
||||
The audit's original C-3 fail condition — a `WARNING:` line in the log alongside an existing
|
||||
marker — can no longer occur by construction: the marker is only written when both generators
|
||||
succeeded. If you ever see that combination, the fix has been reverted.
|
||||
@@ -1,6 +1,6 @@
|
||||
# KEY-05 — Entropy enforcement: per-site classification and mechanism record
|
||||
|
||||
**Requirement:** ROADMAP `KEY-05`. **Plan:** `.planning/phases/10-key-material-hardening/10-06-PLAN.md`.
|
||||
**Requirement:** ROADMAP `KEY-05`.
|
||||
**Supersedes:** backlog `R-13`. **Absorbs:** `R-05` (duplicate-`rand` visibility) and `R-09`
|
||||
(CSPRNG-readiness record). **Resolves:** `F-10a` in
|
||||
`docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md`, which recorded raw match counts and
|
||||
@@ -44,7 +44,7 @@ they are enabled wrongly. Both are gated behind Task 5, a `gate="blocking-human"
|
||||
|
||||
## Source precedence
|
||||
|
||||
`.planning/phases/10-key-material-hardening/10-CONTEXT.md` (2026-08-01) lists **F-07 / R-05**
|
||||
The Phase 10 hardening work lists **F-07 / R-05**
|
||||
and **F-10 / R-13** under `## Deferred Ideas`. KEY-05 was added to the ROADMAP on
|
||||
**2026-08-02**, after that context was gathered, and explicitly absorbs R-05 and supersedes
|
||||
R-13. The ROADMAP requirement is the later and governing artifact.
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
# Phase 10 — Independent Verification Guide
|
||||
|
||||
**Audience:** third-party security auditors, and the Archipelago team.
|
||||
**Purpose:** verify the Phase 10 security claims *independently*, without trusting the
|
||||
project's own test harness.
|
||||
**Status:** LIVING — sections are marked ✅ verifiable now, ⏳ pending a plan still in
|
||||
execution, or 🔒 hardware-gated. Do not read an unmarked absence as a passing result.
|
||||
|
||||
---
|
||||
|
||||
## 0. How to use this document
|
||||
|
||||
Every claim below follows the same four-part structure, and **all four parts matter**:
|
||||
|
||||
| Part | Why it exists |
|
||||
|---|---|
|
||||
| **Claim** | Stated so it can be falsified. A claim you cannot disprove is not a security claim. |
|
||||
| **Reproduce the defect** | Check out the parent commit and demonstrate the bug. *A test that passes on both the fixed and unfixed code proves nothing.* |
|
||||
| **Verify the fix** | Command + expected output, runnable without our harness wherever possible. |
|
||||
| **Negative control** | Break the fix deliberately; confirm the check goes red on **exactly** that and nothing else. This is what separates verification from demonstration. |
|
||||
|
||||
**Do not skip "Reproduce the defect".** It is the only step that proves the fix addresses
|
||||
something real, and it is the step most often omitted in security theatre.
|
||||
|
||||
### Trust posture
|
||||
|
||||
Where a claim can be checked from *outside* the codebase — an HTTP request from another host,
|
||||
a `tar` listing, a file comparison across two machines — **prefer that over running our tests.**
|
||||
Our tests are offered as convenience and as evidence of intent, not as proof. Every claim below
|
||||
that can be externally checked says so explicitly.
|
||||
|
||||
---
|
||||
|
||||
## 1. Scope
|
||||
|
||||
### In scope — what Phase 10 claims
|
||||
|
||||
| ID | Claim | Severity | Status |
|
||||
|---|---|---|---|
|
||||
| KEY-01 | An already-provisioned node refuses every unauthenticated RPC that can mutate identity or credentials | **Critical** | ⏳ `10-01` in execution |
|
||||
| KEY-02 | First-boot per-device secret generation is fail-closed, retried, self-healing, and has exactly one producer; the shipped rootfs contains no fleet-shared identity material | **High** | ✅ partially landed (`21043096`, `408b328c`), ⏳ single-producer + self-heal in progress |
|
||||
| KEY-03 | The BIP-84 account private key is never imported into Bitcoin Core; the dead import path is deleted | **High** | ⏳ `10-05` in execution |
|
||||
| KEY-04 | On-node evidence for C-3 / C-4 / C-6 | — | 🔒 hardware-gated |
|
||||
| KEY-05 | A defaulted RNG cannot be inherited anywhere in the crate | Medium | ⏳ `10-06` not started |
|
||||
|
||||
### Explicitly NOT claimed
|
||||
|
||||
State these plainly so an auditor is not left inferring them:
|
||||
|
||||
- **Lightning custody is not air-gappable.** Channel, revocation and HTLC keys must sign in real
|
||||
time to answer counterparty commitments. LND remote signing *relocates* those keys; it does not
|
||||
make them cold. Any document implying otherwise is wrong.
|
||||
- **No claim against a compromised kernel CSPRNG**, a malicious dependency in the supply chain,
|
||||
memory disclosure on a running node, or physical access.
|
||||
- **KEY-05 fixes a structural risk, not a live vulnerability.** `rand::random()`/`thread_rng()`
|
||||
are ChaCha12 seeded from `getrandom(2)`; nothing in that finding is exploitable today. The
|
||||
mitigation targets *future silent rebinding* of the entropy source.
|
||||
- **Findings F-04 through F-12 are out of scope** for this phase and remain open. See
|
||||
`ENTROPY-SEED-AUDIT-2026-07-31.md` remediation register (R-05..R-15) and
|
||||
the issue tracker.
|
||||
|
||||
---
|
||||
|
||||
## 2. Provenance
|
||||
|
||||
```bash
|
||||
# The audit that motivated this phase
|
||||
docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md # 103 file:line references
|
||||
|
||||
# The entropy fix that preceded the phase
|
||||
git show 8b51b7e2 # seed.rs — explicit OsRng at the call site
|
||||
|
||||
# Phase 10 plans and locked decisions
|
||||
.planning/phases/10-key-material-hardening/
|
||||
```
|
||||
|
||||
`.planning/` is committed deliberately: an auditor can read *why* each decision was made,
|
||||
including the ones that were reversed. `10-CONTEXT.md` records D-01..D-11 plus three
|
||||
in-flight corrections (D-03a, D-07a/b/c) where our own earlier reasoning was wrong.
|
||||
|
||||
---
|
||||
|
||||
## 3. Tier 0 — verifiable on any checkout, no node required ✅
|
||||
|
||||
No hardware, no deploy. Start here.
|
||||
|
||||
### 3.1 First-boot secrets are fail-closed (KEY-02)
|
||||
|
||||
**Claim.** If per-device secret generation fails, the completion marker is **not** written and
|
||||
the boot does not proceed as if it had succeeded.
|
||||
|
||||
**Reproduce the defect:**
|
||||
```bash
|
||||
git log --oneline -1 21043096 # the fix commit
|
||||
git show 21043096^:image-recipe/_archived/build-auto-installer-iso.sh > /tmp/pre-fix.sh
|
||||
grep -n 'touch .*MARKER' /tmp/pre-fix.sh
|
||||
# Observe: the marker write is NOT inside the success branch — it runs regardless of outcome.
|
||||
```
|
||||
|
||||
**Verify the fix:**
|
||||
```bash
|
||||
bash tests/first-boot-secrets/run-tests.sh
|
||||
# Expect: passed: 3 failed: 0 (more cases once the self-heal work lands)
|
||||
```
|
||||
The harness extracts the heredoc body **from the builder itself**, so it exercises the bytes
|
||||
that ship rather than a copy. Confirm that for yourself:
|
||||
```bash
|
||||
grep -n 'extracted .* lines from the builder' tests/first-boot-secrets/run-tests.sh
|
||||
```
|
||||
|
||||
**Negative control:**
|
||||
```bash
|
||||
# Move `touch "$MARKER"` outside the success branch in the builder, then:
|
||||
bash tests/first-boot-secrets/run-tests.sh
|
||||
# Expect: FAIL: openssl fails every attempt -> MARKER-SET-ON-FAILURE
|
||||
# passed: 2 failed: 1 EXIT=1
|
||||
git checkout image-recipe/_archived/build-auto-installer-iso.sh
|
||||
```
|
||||
It must fail on **that case only**. A negative control that reddens everything is measuring
|
||||
nothing.
|
||||
|
||||
### 3.2 Master-seed entropy is explicit (F-02, shipped)
|
||||
|
||||
**Claim.** Mnemonic generation draws from an explicitly-passed `OsRng`, not a
|
||||
transitive-dependency default, and a test proves the injected RNG is the one consumed.
|
||||
|
||||
```bash
|
||||
git show 8b51b7e2 -- core/archipelago/src/seed.rs # ~6 lines of production change
|
||||
cd core && cargo test -p archipelago seed:: # expect 25 passed; 0 failed
|
||||
```
|
||||
|
||||
**Reproduce the defect:** on `8b51b7e2^`, `MasterSeed::generate` calls
|
||||
`bip39::Mnemonic::generate(24)`, which resolves to `&mut rand::thread_rng()` *inside* the bip39
|
||||
crate — there is no seam to inject through, so the proving test cannot be written at all.
|
||||
|
||||
**Note for auditors:** the test module implements `rand::CryptoRng` for a counter RNG. That is a
|
||||
deliberately false marker-trait promise, confined to `#[cfg(test)]` (`seed.rs:502`). KEY-05
|
||||
retires it. Confirm containment:
|
||||
```bash
|
||||
grep -n 'CountingRng' core/archipelago/src/seed.rs # all hits must be after the cfg(test) at :502
|
||||
```
|
||||
|
||||
### 3.3 Unauthenticated method inventory (KEY-01 context)
|
||||
|
||||
Read the authoritative list rather than trusting prose:
|
||||
```bash
|
||||
sed -n '/UNAUTHENTICATED_METHODS/,/];/p' core/archipelago/src/api/rpc/middleware.rs
|
||||
```
|
||||
Every entry is reachable without a session, RBAC check, or CSRF token. KEY-01's claim is that
|
||||
those which can mutate identity or credentials refuse once the node is provisioned.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tier 1 — requires a running node ⏳
|
||||
|
||||
Pending `10-01` and `10-02`. `10-02` produces `scripts/security/rpc-exposure-probe.sh` and
|
||||
`docs/security/KEY-01-ON-NODE-VERIFICATION.md`.
|
||||
|
||||
**The external check that matters most (C-6).** From a *different host* on the same network,
|
||||
against a node that has completed onboarding:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST http://<node>/rpc \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"seed.restore","params":{"words":["<24 words>"]}}'
|
||||
```
|
||||
|
||||
- **Before the fix:** the node accepts attacker-supplied words and overwrites `node_key`,
|
||||
`nostr_secret` and the FIPS mesh key. This is the Critical finding.
|
||||
- **After the fix:** refused, and the node's identity is byte-identical afterwards.
|
||||
|
||||
Verify byte-identity yourself rather than trusting a log line:
|
||||
```bash
|
||||
sha256sum /var/lib/archipelago/identity/node_key /var/lib/archipelago/identity/nostr_secret
|
||||
# run before and after the request; the hashes must be unchanged
|
||||
```
|
||||
|
||||
> ⚠️ **Do not run the "before" case against a node you care about.** It really does overwrite the
|
||||
> identity. Use a disposable node — see `.planning/todos/pending/2026-08-01-archi-dev-box-as-fresh-test-node-without-iso.md`
|
||||
> for standing up an isolated instance without flashing an ISO.
|
||||
|
||||
**Do not probe with `seed.status`.** The original audit's C-6 command used it; `seed.status` is
|
||||
**not** in `UNAUTHENTICATED_METHODS`, so it returns 401 by design and would report the surface
|
||||
closed while the real door stands open. Probe with a method that is genuinely on the
|
||||
unauthenticated list.
|
||||
|
||||
**Non-regression, equally important:** a *fresh, un-onboarded* node must still complete
|
||||
onboarding. The gate distinguishes provisioned from fresh; a fix that refuses on a fresh node
|
||||
bricks first boot fleet-wide.
|
||||
|
||||
---
|
||||
|
||||
## 5. Tier 2 — ISO build host 🔒
|
||||
|
||||
Full procedure: `docs/security/KEY-02-ROOTFS-EVIDENCE.md` (C-4).
|
||||
|
||||
```bash
|
||||
UNBUNDLED=1 bash image-recipe/build-debian-iso.sh --rebuild
|
||||
# then follow steps 2/4/5/6 in KEY-02-ROOTFS-EVIDENCE.md
|
||||
```
|
||||
|
||||
**Claim.** The shipped rootfs tar contains no SSH host keys, no TLS private key, and no
|
||||
machine-id — so no two nodes flashed from one image can share them.
|
||||
|
||||
**Gotcha, recorded because it will waste your afternoon:** read `RECIPE_HASH` from
|
||||
`image-recipe/build/auto-installer/archipelago-rootfs.recipe.sha256`, **not** by hashing the
|
||||
repo file. The wrapper rewrites 35 path expressions and absolutises `SCRIPT_DIR` before exec,
|
||||
so the hash is host- and checkout-specific.
|
||||
|
||||
**Note the inverted expectation.** The original audit expected these artefacts to be *present*.
|
||||
This check passes when they are *absent*.
|
||||
|
||||
---
|
||||
|
||||
## 6. Tier 3 — two physical nodes 🔒
|
||||
|
||||
**C-3 — host-key uniqueness.** Flash two machines from the *same* ISO, then compare:
|
||||
```bash
|
||||
# on each node
|
||||
sha256sum /etc/ssh/ssh_host_*_key.pub
|
||||
sha256sum /etc/ssl/private/<tls-key> # path per the nginx config
|
||||
cat /etc/machine-id
|
||||
```
|
||||
Every value must differ between the two nodes. Any match is a finding.
|
||||
|
||||
SSH host keys and the TLS key are equally sharp signals once the single-producer work lands
|
||||
(before it, TLS had an installer fallback and SSH did not — see `KEY-02-ROOTFS-EVIDENCE.md`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Tier 4 — pre-release gate
|
||||
|
||||
```bash
|
||||
# ON the node, not over RPC — it uses local podman/systemctl/bitcoin probes
|
||||
ARCHY_ITERATIONS=5 bash tests/lifecycle/run-gate.sh
|
||||
```
|
||||
Install / UI / stop / start / restart / reinstall / reboot-survive /
|
||||
archipelago-restart-survive / uninstall, 5× green. See `tests/lifecycle/TESTING.md`.
|
||||
|
||||
Frontend: `cd neode-ui && npm run test` (vitest) and `npm run build`.
|
||||
Rust: `cd core && cargo test -p archipelago`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Known-accepted risks
|
||||
|
||||
Recorded so an auditor does not have to discover them by reading commit messages.
|
||||
|
||||
| Risk | Decision | Where |
|
||||
|---|---|---|
|
||||
| A node whose first-boot secret generation can never succeed will not serve TLS | Accepted. Mitigated by a build-time assertion on generator binaries, retry-with-backoff, and self-heal on subsequent boots — leaving genuinely-broken hardware as the residual | `10-03` |
|
||||
| Rotating host keys on already-deployed nodes invalidates `known_hosts` fleet-wide | Accepted, rated one-way, gated behind a decision checkpoint | D-06, `10-04` |
|
||||
| KEY-01's fix ships on the next scheduled OTA, not an emergency release | Deliberate. The Critical finding stays live on the fleet until that OTA | D-10 |
|
||||
| `#[cfg(test)]` code implements `rand::CryptoRng` falsely | Accepted until KEY-05 retires it; contained to test builds | `seed.rs:656` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Reporting a finding
|
||||
|
||||
If any check above fails, or you find something not covered: the audit format that produced this
|
||||
work is `docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md` — evidence as `file:line`, an explicit
|
||||
severity, and a stated confidence. Findings that cannot be verified without hardware belong in an
|
||||
UNVERIFIED section rather than being asserted.
|
||||
|
||||
Two corrections in that document are worth reading as calibration, because both were ours: F-10
|
||||
**understated** its scope by a factor of 20, and the correction to it then **overstated** the
|
||||
severity of two files within a day. Both are struck in place rather than rewritten.
|
||||
@@ -39,8 +39,7 @@
|
||||
|
||||
**Provenance rules used throughout.** Every architectural claim is grounded in either (a) a
|
||||
`file:line` from this tree, or (b) RESEARCH.md Part C
|
||||
(`.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-RESEARCH.md`,
|
||||
which cites Bitcoin Core `doc/psbt.md`, `doc/descriptors.md`, `doc/multisig-tutorial.md`, the
|
||||
(which cites Bitcoin Core `doc/psbt.md`, `doc/descriptors.md`, `doc/multisig-tutorial.md`, the
|
||||
Core 30.0 release notes, LND `docs/remote-signing.md` and `docs/psbt.md`). Anything from
|
||||
neither is marked `[UNVERIFIED]`.
|
||||
|
||||
@@ -615,6 +614,5 @@ current security posture and should not wait for the rest.
|
||||
- `docs/hardware-signer-design.md` — the first-party TROPIC01 air-gapped signer; §4.3 above
|
||||
answers two of its open items.
|
||||
- `docs/adr/005-chacha20-backup-encryption.md` — the at-rest envelope §6 reuses.
|
||||
- `.planning/quick/260731-upz-research-coinkite-conkite-low-entropy-ha/260731-upz-RESEARCH.md`
|
||||
— Part C is the source for the Core RPC table, the LND capability matrix, and the air-gap
|
||||
format comparison.
|
||||
|
||||
Reference in New Issue
Block a user