Archipelago — open-source initial import
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
---
|
||||
phase: 10-key-material-hardening
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- core/archipelago/src/api/rpc/bitcoin.rs
|
||||
- core/archipelago/src/api/rpc/dispatcher.rs
|
||||
- core/archipelago/src/seed.rs
|
||||
- core/archipelago/src/api/rpc/lnd/wallet.rs
|
||||
- docs/security/KEY-03-SIGNING-POSTURE.md
|
||||
- docs/security/PSBT-SIGNING-ARCHITECTURE.md
|
||||
autonomous: false
|
||||
requirements: [KEY-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "No code path in the daemon writes the BIP-84 account extended private key into Bitcoin Core (F-13 closed by deletion, per D-07b)"
|
||||
- "The PSBT produced by lnd.create-psbt is inspected for the BIP-32 key-origin data an external signer needs, and the result is reported to the caller instead of assumed"
|
||||
- "The repository states honestly which parts of the fund -> sign-offline -> finalize -> broadcast round trip are covered by tests today and which are not"
|
||||
- "No document this plan touches implies Lightning channel, revocation or HTLC keys can be air-gapped"
|
||||
- "If any fleet node turns out to hold a descriptor wallet this handler created, that is surfaced and stopped on, not silently migrated (D-07b)"
|
||||
artifacts:
|
||||
- path: "core/archipelago/src/api/rpc/lnd/wallet.rs"
|
||||
provides: "psbt_key_origin_report plus its tests, and the key-origin field on the lnd.create-psbt response"
|
||||
contains: "psbt_key_origin_report"
|
||||
- path: "docs/security/KEY-03-SIGNING-POSTURE.md"
|
||||
provides: "The evidence-backed record of the Core deletion, the LND PSBT coverage map, and the air-gap honesty statement"
|
||||
contains: "D-07b"
|
||||
min_lines: 60
|
||||
- path: "docs/security/PSBT-SIGNING-ARCHITECTURE.md"
|
||||
provides: "Status banner recording that its Phase 1 was superseded by deletion rather than conversion"
|
||||
contains: "D-07b"
|
||||
key_links:
|
||||
- from: "core/archipelago/src/api/rpc/lnd/wallet.rs"
|
||||
to: "docs/security/KEY-03-SIGNING-POSTURE.md"
|
||||
via: "the key-origin report is the mechanical form of the doc's external-signer claim"
|
||||
pattern: "psbt_key_origin_report"
|
||||
- from: "core/archipelago/src/api/rpc/dispatcher.rs"
|
||||
to: "core/archipelago/src/api/rpc/bitcoin.rs"
|
||||
via: "the bitcoin.init-wallet-from-seed dispatch arm and its handler are removed together"
|
||||
pattern: "init-wallet-from-seed"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Close F-13 (High) by deleting the code that duplicates the spending key, and make the signing
|
||||
path that actually matters — LND's PSBT round trip — a first-class, tested, honestly documented
|
||||
one (D-07b).
|
||||
|
||||
Purpose: `handle_bitcoin_init_wallet_from_seed` derives the BIP-84 account **xprv**, stringifies
|
||||
it, and imports `wpkh(xprv/0/*)` and `wpkh(xprv/1/*)` into Bitcoin Core's `wallet.dat`
|
||||
(`core/archipelago/src/api/rpc/bitcoin.rs:188-189`, `:203`, `:229-231`, `:278-281`). It has no
|
||||
caller anywhere in the repo, LND is the wallet the UI actually drives, and archi-dev-box shows
|
||||
the wallet it creates has never existed there. Its entire function is to put a second copy of the
|
||||
spending key somewhere with weaker protection than the Argon2 envelope. D-07b: delete it.
|
||||
|
||||
Output: the Core path gone, `lnd.create-psbt` reporting whether its PSBT carries the BIP-32
|
||||
derivation data a hardware signer needs, and a signing-posture document that states plainly what
|
||||
is and is not air-gappable.
|
||||
|
||||
**D-07's parity-proof migration and its one-way checkpoint are withdrawn (D-07b).** There is no
|
||||
wallet to migrate. Do not plan or build migration machinery. If Task 3's census unexpectedly
|
||||
finds a wallet this handler created, that is a finding to stop on — it would mean the endpoint
|
||||
was invoked by hand and that node's spending key is duplicated in Core, which deserves a human
|
||||
decision, not an automated rewrite.
|
||||
|
||||
**What deletion does to D-08 and D-09.** D-08 asked that the spending key exist in exactly one
|
||||
place, with an opt-in air-gapped path; deleting the Core import achieves the first half outright
|
||||
(the only remaining on-node copy of the BIP-84 key is the Argon2 envelope), and the opt-in path
|
||||
is LND's existing PSBT round trip rather than a Core watch-only wallet. D-09 required a
|
||||
`[fingerprint/derivation]` key origin on emitted descriptors so a hardware signer can locate its
|
||||
key; with Core's descriptors deleted there are no Archipelago-emitted descriptors left to
|
||||
annotate, so D-09's actual protection moves to the PSBT itself — Task 2 inspects and reports
|
||||
whether the PSBT `lnd.create-psbt` returns carries the BIP-32 key-origin data a signer needs.
|
||||
Neither decision is dropped; both are satisfied by a different mechanism, and the plan says so
|
||||
rather than letting them lapse.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/10-key-material-hardening/10-CONTEXT.md
|
||||
@docs/security/PSBT-SIGNING-ARCHITECTURE.md
|
||||
@CLAUDE.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Delete the Core wallet path end to end, and record why (D-07b)</name>
|
||||
<reversibility rating="reversible">Deleting an uncalled, password-gated endpoint is recoverable from git in one revert; nothing consumes its output and no on-disk state depends on it (D-07b).</reversibility>
|
||||
<files>core/archipelago/src/api/rpc/bitcoin.rs, core/archipelago/src/api/rpc/dispatcher.rs, core/archipelago/src/seed.rs, docs/security/KEY-03-SIGNING-POSTURE.md</files>
|
||||
<read_first>
|
||||
- core/archipelago/src/api/rpc/bitcoin.rs (lines 155-300 — the whole handler being deleted, including the zeroize calls at :222 and :284 whose careful in-memory handling is worth naming in the record)
|
||||
- core/archipelago/src/api/rpc/dispatcher.rs (lines 105-130 — the bitcoin.* dispatch arms, one of which is being removed)
|
||||
- core/archipelago/src/seed.rs (lines 225-250 — derive_bitcoin_xprv, which loses its only non-test caller)
|
||||
- docs/security/ENTROPY-SEED-AUDIT-2026-07-31.md (finding F-13 and remediation R-04 — the record this task closes)
|
||||
- .planning/phases/10-key-material-hardening/10-CONTEXT.md (D-07a and D-07b — the evidence chain, and D-07c which must stay visible)
|
||||
</read_first>
|
||||
<action>
|
||||
Before deleting anything, re-establish the evidence yourself rather than inheriting it, and paste
|
||||
the raw command output into the SUMMARY. Run a repo-wide search for the method name
|
||||
`bitcoin.init-wallet-from-seed` and for the handler symbol
|
||||
`handle_bitcoin_init_wallet_from_seed` across `core/`, `neode-ui/src`, `scripts/`, `web/`, `apps/`
|
||||
and `tests/`, excluding `core/target`, `node_modules` and `.git`. The expected result is exactly
|
||||
two occurrences of the method name (the dispatcher arm and, if present, a docs mention) and two
|
||||
of the symbol (its definition and the dispatcher call). If the search finds a third caller, STOP
|
||||
and raise a checkpoint — the deletion's premise is that nothing calls it.
|
||||
|
||||
Then:
|
||||
|
||||
1. Delete `handle_bitcoin_init_wallet_from_seed` from `core/archipelago/src/api/rpc/bitcoin.rs`
|
||||
(the whole function including its doc comment) and remove the `"bitcoin.init-wallet-from-seed"`
|
||||
arm from `core/archipelago/src/api/rpc/dispatcher.rs`. Remove any import that becomes unused
|
||||
as a result — `zeroize::Zeroize` is the likely one; let the compiler tell you rather than
|
||||
guessing.
|
||||
2. `crate::seed::derive_bitcoin_xprv` loses its only non-test caller. Do NOT delete it: it is
|
||||
covered by existing tests (`seed.rs:601-602`, `:856`) and it is the derivation D-07c's deferred
|
||||
BDK cold vault will need. Add `#[allow(dead_code)]` with a doc line naming D-07c as the reason
|
||||
it is retained, so the next reader does not delete it as cruft. `archipelago` is a binary crate
|
||||
with no `lib.rs`, so an uncalled `pub fn` does warn and `clippy -D warnings` would fail without
|
||||
this.
|
||||
3. Create `docs/security/KEY-03-SIGNING-POSTURE.md` with a first section
|
||||
`## Bitcoin Core wallet path — deleted (D-07b)` recording: the four evidence points from D-07a
|
||||
with their `file:line`; the search output from this task; that the endpoint was authenticated
|
||||
AND password-gated (`bitcoin.rs:176-180`) so F-13 was never remotely reachable — key-at-rest
|
||||
duplication, not an exposed endpoint; that the in-memory handling of the xprv string was
|
||||
careful (zeroized on both paths) and the defect was which key went into the wallet, not how it
|
||||
was held; and that F-13 is closed by removal rather than by conversion to watch-only.
|
||||
Reference D-07c explicitly so the deferred cold-vault option stays visible rather than being
|
||||
quietly lost with the code.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd core && CARGO_INCREMENTAL=0 cargo build -p archipelago && CARGO_INCREMENTAL=0 cargo clippy -p archipelago -- -D warnings</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `cd core && CARGO_INCREMENTAL=0 cargo build -p archipelago` succeeds and `cargo clippy -p archipelago -- -D warnings` is clean.
|
||||
- `grep -rn 'handle_bitcoin_init_wallet_from_seed' core/ --include=*.rs` returns no matches.
|
||||
- `grep -rn 'init-wallet-from-seed' core/archipelago/src/api/rpc/dispatcher.rs` returns only the `lnd.init-wallet-from-seed` arm, which is a different endpoint and stays.
|
||||
- `cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago seed::` still passes, proving `derive_bitcoin_xprv`'s coverage survived the deletion.
|
||||
- `docs/security/KEY-03-SIGNING-POSTURE.md` exists, cites D-07b and D-07c by name, and contains the pasted no-caller search output.
|
||||
</acceptance_criteria>
|
||||
<done>Nothing in the daemon can write the BIP-84 account private key into Bitcoin Core, the derivation function survives with its tests and a stated reason, and the deletion is documented with its evidence.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Make the LND PSBT path first-class — report the key-origin data an external signer needs</name>
|
||||
<files>core/archipelago/src/api/rpc/lnd/wallet.rs, docs/security/KEY-03-SIGNING-POSTURE.md, docs/security/PSBT-SIGNING-ARCHITECTURE.md</files>
|
||||
<read_first>
|
||||
- core/archipelago/src/api/rpc/lnd/wallet.rs (lines 600-710 for handle_lnd_create_psbt; 705-790 for handle_lnd_finalize_psbt including the broadcast at the end; 790-905 for handle_lnd_create_raw_tx, the auto-signing sibling that must NOT be changed; 1128-1160 for the existing test module and the build_invoice_request_body free-function pattern to mirror)
|
||||
- core/archipelago/src/rate_limit.rs (lines 68-69 — lnd.create-psbt and lnd.finalize-psbt are already limited at 5/300s)
|
||||
- core/archipelago/Cargo.toml (lines 70-95 — `bitcoin = "=0.32.5"` and `base64 = "0.21"` are already present; no new dependency is permitted in this task)
|
||||
- docs/security/PSBT-SIGNING-ARCHITECTURE.md (sections 1.2, 3.1 and 5 — the RPC loop, why key origin is mandatory rather than cosmetic, and the honest LND limits)
|
||||
- neode-ui/src/api/rpc-client.ts (lines 410-435 — createPsbt/finalizePsbt, the client contract the new response field extends)
|
||||
</read_first>
|
||||
<action>
|
||||
Add a pure, testable inspection function to `core/archipelago/src/api/rpc/lnd/wallet.rs`,
|
||||
alongside the existing free function `build_invoice_request_body`:
|
||||
|
||||
`fn psbt_key_origin_report(psbt_base64: &str) -> anyhow::Result<PsbtKeyOriginReport>` returning a
|
||||
struct with `input_count: usize`, `inputs_with_key_origin: usize` and
|
||||
`all_inputs_have_key_origin: bool`. Decode with `base64` and parse with
|
||||
`bitcoin::psbt::Psbt::deserialize`; count an input as carrying key origin when either its
|
||||
`bip32_derivation` map or its `tap_key_origins` map is non-empty. Use only the already-present
|
||||
`bitcoin` and `base64` crates — adding a dependency is out of scope for this plan and the
|
||||
executor must halt rather than add one.
|
||||
|
||||
Wire it into `handle_lnd_create_psbt` after `funded_psbt` is extracted: compute the report
|
||||
best-effort (a decode failure must degrade to `null`, never to an error — a user's send must not
|
||||
fail because an inspection helper could not parse something), add a `key_origin` object to the
|
||||
JSON response carrying the three fields, and `tracing::warn!` with the counts when
|
||||
`all_inputs_have_key_origin` is false, because that is the exact condition under which a hardware
|
||||
signer will refuse the PSBT. Do not change `handle_lnd_finalize_psbt` and do not touch
|
||||
`handle_lnd_create_raw_tx` — the latter deliberately auto-signs with LND's hot keys and is a
|
||||
different flow.
|
||||
|
||||
Add tests in the existing `mod tests`, building the fixtures programmatically with the `bitcoin`
|
||||
crate rather than pasting opaque base64 so the tests explain themselves:
|
||||
`psbt_without_derivations_reports_no_key_origin` (a PSBT built from an unsigned transaction with
|
||||
one input, no `bip32_derivation`) and `psbt_with_derivations_reports_key_origin` (the same PSBT
|
||||
with a `bip32_derivation` entry inserted on input 0). Also
|
||||
`malformed_psbt_is_an_error_not_a_panic` for a non-base64 and a truncated input.
|
||||
|
||||
Then answer, with evidence, the question the report exists to raise, and put the answer in
|
||||
`docs/security/KEY-03-SIGNING-POSTURE.md` under `## LND PSBT round trip — what is covered`:
|
||||
|
||||
- Which steps of fund -> export -> sign offline -> import -> finalize -> broadcast exist in this
|
||||
repo today, with `file:line` for each (`lnd.create-psbt` at `lnd/wallet.rs:605`,
|
||||
`lnd.finalize-psbt` at `:711` including its broadcast to `/v2/wallet/tx`, the client bindings at
|
||||
`neode-ui/src/api/rpc-client.ts:410-435`, the 5/300s limits at `rate_limit.rs:68-69`).
|
||||
- Which of those steps has automated test coverage after this task and which does not. State the
|
||||
uncovered ones plainly; do not describe an untested path as verified.
|
||||
- **The question that decides whether this is a real air gap:** on a default node, LND holds the
|
||||
keys for the inputs `lnd.create-psbt` selects. Determine and record whether an externally-held
|
||||
signer can sign such a PSBT at all without LND first being provisioned watch-only against that
|
||||
signer (`remotesigner.*` / `createwatchonly`, PSBT-SIGNING-ARCHITECTURE §5.1-5.2), and whether
|
||||
any fleet node is so provisioned today. Record the verdict either way with its evidence. The
|
||||
PSBT transport being present is not the same claim as custody being air-gapped, and this
|
||||
document must not let the two blur.
|
||||
- The standing honesty statement, in its own subsection: Lightning channel, revocation and HTLC
|
||||
keys are **not** air-gappable at all — they must sign in real time to answer counterparty
|
||||
commitments; remote signing relocates them to a hardened host, it does not cool them. No
|
||||
wording anywhere in the document may imply otherwise.
|
||||
|
||||
Finally, add a short status banner at the top of `docs/security/PSBT-SIGNING-ARCHITECTURE.md`
|
||||
recording that its Phase 1 ("Descriptor watch-only read path", §8) was **superseded by D-07b**:
|
||||
the Core wallet path was deleted rather than converted, so §0's "single highest-value change" and
|
||||
§2.1's invariant now read against a code path that no longer exists. Point the reader at
|
||||
`docs/security/KEY-03-SIGNING-POSTURE.md` for the current state. Change nothing else in that
|
||||
document — §5.4's honesty table is correct and stays exactly as written.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago psbt -- --nocapture</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago psbt` passes with at least three new tests, including the with-derivations and without-derivations pair.
|
||||
- `cd core && CARGO_INCREMENTAL=0 cargo clippy -p archipelago -- -D warnings` is clean.
|
||||
- `git diff core/archipelago/Cargo.toml` is empty — no dependency was added.
|
||||
- `grep -c 'key_origin' core/archipelago/src/api/rpc/lnd/wallet.rs` is at least 4 (struct, function, response field, warn line).
|
||||
- `handle_lnd_create_raw_tx` is unchanged: `git diff` shows no hunk inside it.
|
||||
- `docs/security/KEY-03-SIGNING-POSTURE.md` contains a per-step coverage table with `file:line` and an explicit tested/untested column, the recorded watch-only verdict, and the Lightning-keys-are-not-air-gappable subsection.
|
||||
- `docs/security/PSBT-SIGNING-ARCHITECTURE.md`'s diff is confined to the added status banner; section 5.4 is byte-identical.
|
||||
</acceptance_criteria>
|
||||
<done>`lnd.create-psbt` reports whether its PSBT carries the BIP-32 key-origin data an external signer needs, backed by tests, and the repository states honestly what the round trip does and does not deliver today.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Fleet census — does any node hold a descriptor wallet this handler created? (D-07b, stop on finding)</name>
|
||||
<precondition>At least one fleet node is reachable with a running Bitcoin Core or Knots container whose `bitcoin-cli` can be invoked (for example via `podman exec`).</precondition>
|
||||
<files>docs/security/KEY-03-SIGNING-POSTURE.md</files>
|
||||
<read_first>
|
||||
- .planning/phases/10-key-material-hardening/10-CONTEXT.md (D-07a point 3 — archi-dev-box is the only known-negative node; the rest of the fleet is UNVERIFIED — and D-07b's stop-on-finding rule)
|
||||
- docs/security/KEY-03-SIGNING-POSTURE.md (Task 1 output — append the census to it)
|
||||
- core/archipelago/src/api/rpc/bitcoin.rs (as it stood before Task 1 — the default wallet_name was `archipelago`, which is what the census looks for)
|
||||
- CLAUDE.md (node access policy; `.228` is remote and in real use)
|
||||
</read_first>
|
||||
<action>
|
||||
Claude prepares the command set; the operator runs it per node and pastes the output; Claude
|
||||
records it into `docs/security/KEY-03-SIGNING-POSTURE.md` under `## Fleet census — Core descriptor
|
||||
wallets`, one row per node, using node labels rather than addresses.
|
||||
|
||||
**Hard constraint on every command in this task: never run `listdescriptors true`.** The `true`
|
||||
argument makes Core return the descriptors including private keys, which would print an xprv to a
|
||||
terminal and into a transcript. `listwallets`, `getwalletinfo` and `listdescriptors` without
|
||||
arguments answer the question completely. If any output unexpectedly contains a string starting
|
||||
with `xprv`, stop immediately, do not paste it, and report only that it occurred.
|
||||
|
||||
If a wallet with `private_keys_enabled: true` is found on any node, that is a **finding**: stop,
|
||||
record it, and raise it as a blocker. It would mean the endpoint was invoked manually before this
|
||||
plan deleted it, and that node's spending key is duplicated outside the Argon2 envelope. Do not
|
||||
migrate, unload or modify it — D-07b withdrew the migration deliberately, and rewriting a wallet
|
||||
that might hold funds is exactly the kind of decision that belongs to a human.
|
||||
</action>
|
||||
<what-built>Deletion of the code path that would create such a wallet, plus the signing-posture record this census completes.</what-built>
|
||||
<how-to-verify>
|
||||
On each reachable fleet node, for the Bitcoin Core (or Knots) container:
|
||||
1. `ls -la /var/lib/archipelago/bitcoin/wallets/ 2>&1` — an absent directory is itself a complete answer for that node; paste it as-is.
|
||||
2. `bitcoin-cli listwallets` (via `podman exec` into the container, matching however that node runs it).
|
||||
3. For each wallet name returned: `bitcoin-cli -rpcwallet=<name> getwalletinfo` and record `private_keys_enabled`, `descriptors`, `walletname` and `balance`.
|
||||
4. For any wallet with `private_keys_enabled: true`: `bitcoin-cli -rpcwallet=<name> listdescriptors` — with NO second argument. Record only the descriptor prefixes (`wpkh(...`), never a full key string.
|
||||
5. Record the node label, the Bitcoin app in use (Core or Knots) and its version from `bitcoin-cli getnetworkinfo | head`.
|
||||
6. Paste the output for every node checked, and list explicitly any fleet node that was NOT checked and why.
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- Every reachable fleet node has a row in the census table with `wallets_dir_present`, `listwallets` output and, per wallet, `private_keys_enabled`. Unreachable nodes are listed as UNCHECKED with the reason, never omitted.
|
||||
- No command run in this task included `listdescriptors true`, and no output containing a key string was pasted. The operator confirms this explicitly.
|
||||
- If any wallet reported `private_keys_enabled: true`, the phase raises a blocker naming the node label and the wallet name, and the plan does NOT proceed to close KEY-03 until that is decided by a human.
|
||||
- If no such wallet is found, `docs/security/KEY-03-SIGNING-POSTURE.md` records F-13 as closed by deletion with no migration required, and names the nodes that evidence it.
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Paste the per-node census output, then type "approved" — or name the node and wallet if a private-key-bearing wallet was found.</resume-signal>
|
||||
<done>The fleet's Core wallet state is recorded rather than assumed, and any private-key-bearing wallet is surfaced as a blocker instead of being silently migrated.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Daemon encrypted envelope -> Bitcoin Core `wallet.dat` | The boundary F-13 crosses. `wallet.dat` lives in the Bitcoin container's data volume with no Argon2 passphrase (the wallet was created with an empty one, `bitcoin.rs:205`). Task 1 removes the only code that crosses it. |
|
||||
| Daemon -> LND REST (`/v2/wallet/psbt/*`) | Macaroon-authenticated, loopback. The PSBT that crosses it is public data; the keys that sign it are LND's. |
|
||||
| Node -> external signer (offline) | The air-gap boundary. Whether it can be crossed at all depends on which party holds the input keys — the question Task 2 must answer with evidence. |
|
||||
| Operator terminal -> census output | `listdescriptors true` would print an xprv into a transcript; the census must not create the exposure it is measuring. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-10-41 | Information disclosure | BIP-84 account xprv persisted in Core's `wallet.dat` outside the Argon2 envelope (F-13) | high | mitigate | Task 1 deletes the only code path that writes it; Task 3 confirms no node already holds such a wallet |
|
||||
| T-10-42 | Information disclosure | A census command prints a private key into a terminal and a transcript | high | mitigate | `listdescriptors true` is banned by name in the task; the acceptance criterion requires the operator to confirm it was not run; a key-looking string aborts the paste |
|
||||
| T-10-43 | Tampering | An automated migration rewrites a wallet that may hold real funds | high | mitigate | D-07b withdrew the migration; Task 3 stops on a finding and escalates to a human instead |
|
||||
| T-10-44 | Spoofing | An external signer refuses a PSBT because it carries no key origin, and the failure surfaces as an opaque error | medium | mitigate | Task 2 inspects the PSBT and reports `all_inputs_have_key_origin` on the response plus a warn log, so the condition is named before the user reaches the signer |
|
||||
| T-10-45 | Repudiation | Documentation claims air-gapped custody the implementation does not deliver | high | mitigate | Task 2 requires a recorded, evidence-backed verdict on whether an external signer can sign a default node's PSBT at all, plus the Lightning-keys honesty subsection; the PSBT-SIGNING-ARCHITECTURE banner records that its Phase 1 was superseded rather than delivered |
|
||||
| T-10-46 | Denial of service | The new PSBT inspection breaks a user's on-chain send | medium | mitigate | The report is computed best-effort and degrades to `null`; `handle_lnd_finalize_psbt` and `handle_lnd_create_raw_tx` are untouched, asserted by a diff-scoped acceptance criterion |
|
||||
| T-10-47 | Elevation of privilege | Deleting a dispatcher arm changes the reachable RPC surface unexpectedly | low | accept | The removed method was authenticated and additionally password-gated and had no caller; the no-caller search is re-run as an acceptance criterion rather than inherited |
|
||||
| T-10-SC | Tampering | npm/pip/cargo installs | low | accept | No dependency is added; an empty `git diff core/archipelago/Cargo.toml` is an acceptance criterion. Executor MUST halt and raise a checkpoint rather than adding a crate to satisfy the PSBT parsing. |
|
||||
</threat_model>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
## Artifacts this plan produces
|
||||
|
||||
**Removed:**
|
||||
|
||||
| Symbol | Kind | Location |
|
||||
|---|---|---|
|
||||
| `handle_bitcoin_init_wallet_from_seed` | async fn | `core/archipelago/src/api/rpc/bitcoin.rs:161-295` |
|
||||
| `"bitcoin.init-wallet-from-seed"` | dispatch arm | `core/archipelago/src/api/rpc/dispatcher.rs:122-124` |
|
||||
|
||||
**New in `core/archipelago/src/api/rpc/lnd/wallet.rs`:**
|
||||
|
||||
| Symbol | Kind | Signature |
|
||||
|---|---|---|
|
||||
| `PsbtKeyOriginReport` | struct | `{ input_count: usize, inputs_with_key_origin: usize, all_inputs_have_key_origin: bool }` |
|
||||
| `psbt_key_origin_report` | fn | `fn psbt_key_origin_report(psbt_base64: &str) -> anyhow::Result<PsbtKeyOriginReport>` |
|
||||
|
||||
**Changed RPC response contract:** `lnd.create-psbt` gains
|
||||
`key_origin: { input_count, inputs_with_key_origin, all_inputs_have_key_origin } | null`.
|
||||
Existing fields (`psbt_base64`, `change_output_index`, `total_amount_sats`,
|
||||
`fee_rate_sat_per_vbyte`) are unchanged; the field is additive.
|
||||
|
||||
**Changed attribute:** `crate::seed::derive_bitcoin_xprv` gains `#[allow(dead_code)]` with a
|
||||
doc line naming D-07c as the reason it is retained.
|
||||
|
||||
**New file:** `docs/security/KEY-03-SIGNING-POSTURE.md` — headings
|
||||
`## Bitcoin Core wallet path — deleted (D-07b)`, `## LND PSBT round trip — what is covered`,
|
||||
`## Fleet census — Core descriptor wallets`.
|
||||
|
||||
**Modified:** `docs/security/PSBT-SIGNING-ARCHITECTURE.md` — status banner only.
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<verification>
|
||||
- `cd core && CARGO_INCREMENTAL=0 cargo test -p archipelago` — no new failures vs. the recorded baseline.
|
||||
- `cd core && CARGO_INCREMENTAL=0 cargo clippy -p archipelago -- -D warnings` — clean.
|
||||
- `git diff core/archipelago/Cargo.toml` is empty.
|
||||
- Task 3's checkpoint resolved with per-node census output.
|
||||
- Commit stages only this plan's six paths explicitly — never `git add -A`.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- No code path in the daemon writes the BIP-84 account private key into Bitcoin Core.
|
||||
- `derive_bitcoin_xprv` survives with its tests and a written reason (D-07c).
|
||||
- `lnd.create-psbt` reports key-origin presence, backed by programmatically-built test fixtures.
|
||||
- The signing-posture document states, with `file:line`, which round-trip steps exist, which are
|
||||
tested, whether an external signer can sign a default node's PSBT at all, and that Lightning
|
||||
channel/revocation/HTLC keys are not air-gappable.
|
||||
- The fleet census is recorded per node, with any private-key-bearing wallet raised as a blocker.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/10-key-material-hardening/10-05-SUMMARY.md` when done, carrying the
|
||||
no-caller search output, the new test names and results, the round-trip coverage table, the
|
||||
watch-only verdict, and the per-node census.
|
||||
</output>
|
||||
Reference in New Issue
Block a user