fix(security): gate the wallet transport and make the no-mint rule structural - #452
Draft
MichaelTaylor3d wants to merge 1 commit into
Draft
fix(security): gate the wallet transport and make the no-mint rule structural#452MichaelTaylor3d wants to merge 1 commit into
MichaelTaylor3d wants to merge 1 commit into
Conversation
This was referenced Aug 31, 2026
MichaelTaylor3d
force-pushed
the
loop/batch-seckey
branch
from
August 31, 2026 15:00
77ad817 to
105fb20
Compare
MichaelTaylor3d
force-pushed
the
loop/batch-seckey
branch
from
August 31, 2026 15:26
105fb20 to
7ec9be6
Compare
…ructural Six key-custody, pairing and authorization defects, one branch. #257 — the Sage-parity mTLS wallet transport dispatched the whole surface, custody and master-tier included, on possession of the shared client cert alone. `build_router` now REQUIRES an `Arc<dyn WalletCallGate>`, there is one handler behind `POST /:method`, and it authorizes before the body is interpreted. The only gate the transport crate ships is `DenyAll`. `dig-node-service` supplies `NodeWalletGate`, delegating to the same `wallet_authz::authorize` the HTTP and `/ws` planes call, so a third plane cannot form its own opinion. #255 — `control.config.setUpstream` was ordinary-tier, persisted an attacker-chosen RPC upstream, and survived `pairing.revoke`. Moved to master tier via a single declared, strictly-widening, self-retiring overlay rather than a restated string match; the refined rule (outlives the token AND confers authority on a principal) is applied to the whole control surface, with `cache.setCap` and `log.setLevel` judged and deliberately left ordinary. The upstream is now validated as a well-formed URL with no userinfo, cleartext confined to loopback. #345 — the no-mint rule was carried by a `?` in `read_legacy`; swapping it for `.ok()` minted over a present-but-locked seed and then deleted it. `read_legacy` now returns a three-valued `LegacySeed` whose `ConfirmedAbsent` is constructed only from a determined absence, and `load_or_create` matches with both arms named. #346 — `client_name` reached the operator's approval prompt verbatim with an unmarked truncation. Over-long names are now REFUSED at ingest; the render goes through the new `untrusted_text`, which marks clips in-band, charges the budget on rendered width, and neutralises control, Cf and bidi characters visibly. The stored value stays byte-verbatim. #343 — the derived BLS peer key is at rest UNSEALED, so sealing the seed does not make `peer_id` copy-resistant. Sealing it is a dig-tls/dig-peer-protocol cascade, so the CLAIM is corrected instead: every protection summary now names the uncovered derived key, in one place. #355 — the token-squat guard asserted nothing under root and printed ok. Both root-skipped guards now assert the complementary observable — ownership, which only root can manipulate — so every branch can fail. SPEC.md gains §7.12's structural-gate obligation and §18.25a-d. Closes #345 Closes #343 Closes #257 Closes #255 Closes #346 Closes #355 Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
force-pushed
the
loop/batch-seckey
branch
from
August 31, 2026 15:38
7ec9be6 to
a05d062
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DRAFT — the orchestrator gates. This batch expects the full security gate given the custody content.
Six key-custody, pairing and authorization defects. One branch, one squashed commit.
Closes #345 · Closes #343 · Closes #257 · Closes #255 · Closes #346 · Closes #355
Per-ticket detail, including the revert-proofs, is commented on each ticket. This body carries the
blast radius, what each fix makes structurally impossible versus merely checked, and the gaps.
What each fix makes IMPOSSIBLE, versus merely checked
WalletBackend::dispatchwithout an authorization gate —build_routertakesArc<dyn WalletCallGate>as a required parameter and there is one handler behindPOST /:method. An allow-everything policy is#[cfg(test)]-only; the crate ships onlyDenyAll.wallet_authz's table, unchanged.LegacySeed::ConfirmedAbsentis constructed in exactly one place, fromPresence::Absent. The delete needsLegacySeed::Found(_)in the pattern.setUpstreamis master tier at all — that is a dig-node-side overlay until the contract adopts it (below).render_untrustedreturns ONEStringcontaining the marker; there is no companionboolto drop, which was the dig-app CRITICAL. Refusing over-long input at ingest removes the ingest truncation entirely.seed_protection_summaryis split out), so an added tier arm cannot forget it.Blast radius checked
.gitnexusis stale (the registered index points at the primary checkout, not this worktree),and
impacton a stale index returns a false-safeimpactedCount: 0. So this was done by grep +direct read, and that is stated rather than implied.
Symbols whose signature or contract changed, and every call site found and updated:
dig_wallet::sage::transport::{build_router, build_cors_router, serve_mtls, serve_http, serve_dual}— all gained a
gateparameter. Callers:wallet_mtls::spawn(production), and 6 in-crate tests.serve_dualhas no production call site (loopback_bind_guard.rssays so). Breaking fordig-wallet's public API → minor bump on a0.x.wallet_mtls::spawn— one call site,server.rs:2188.machine_key::read_legacy— private, one caller (mint_or_migrate, new). Return type changedOption<_>→LegacySeed.machine_key::protection_summary— public, one consumer (dig-node-core/src/lib.rs:4460,dignstdout). Output string CHANGED (caveat appended); no parser consumes it.control::requires_master_token— behaviour widened by one method. Consumers:wallet_authz::master_tier_control_equivalent,server.rscontrol gate. The existing locksteptest caught the widening immediately, which is the check working.
pairing::request— now refusesclient_name> 64 chars. A behaviour change on an OPENmethod; a client sending a longer label gets
INVALID_PARAMSinstead of a silent clip.pair::format_list— display only.New dependency:
unicode-width = "0.2"ondig-node-service, already inCargo.locktransitively. Needed to charge the display budget on rendered width rather than code points.
Evidence
dig-node-service --libdig-node-core --lib machine_keydig-wallet --lib sage::transportFull suites on the FINAL tree, each run alone:
dig-node-core --libdig-node-service --libdig-wallet --libCounts read from the
test result:line, not fromok, and every filtered run matched a non-zeronumber of tests.
One caveat worth stating: a
--workspacerun launched while a previous cargo process still held theshared
target/reportederror: test failed, -p dig-node-core. Re-run alone it is 1027/0. Theper-crate numbers above are the ones taken without a concurrent cargo, which is the discipline
dig-node#345 asks for.
Revert-proofs — each fix reverted alone, from committed state, via file copies (never
git checkout <path>):9 passed; 3 failed— all three #257 testsErr(_) => Ok(LegacySeed::ConfirmedAbsent)(the ticket's own.ok()exploit)22 passed; 2 failed662 passed; 2 failedrender_untrustedfrom the pending prompt3 passed; 2 failedA FALSE GREEN I shipped, found, and fixed — please check this one hardest
My first
a_client_name_cannot_forge_an_extra_line_in_the_approval_promptpassed with the fixreverted. It asserted a line count and the absence of the forged quotation, and
{:?}quotingalone satisfies both — Debug renders a newline as the two characters
\n, which neither adds aline nor leaves a quote unescaped. It pinned the QUOTING and said nothing about neutralisation.
The discriminator Debug cannot supply is the visible U+FFFD, so the test now asserts that and
asserts no escaped-but-surviving
\n. Separately, the clip test read only the TOKEN list while themutation hit the PENDING list — two different
format!arms — so it now exercises both.It was found only by running the revert-proof. Both were re-measured after the fix.
Two things NOT done, deliberately, and why
node.keyis not sealed. It is written bydig_tls::NodeCert::load_or_generateinanother repo and read back by path by a third crate (
dig_peer_protocol::load_ssl_cert), sosealing it is a release-first cascade across dig-tls + dig-peer-protocol, not a single-writer
change this lane can make.
peer_idmust also not change while fixing it. The ticket's secondoption — state the gap plainly — is what landed, in
protection_summaryand SPEC §18.25a(including the recovery hazard: any sealing MUST use the SAME device key, or a device-key loss
bricks the identity instead of being recoverable).
setUpstreamas ordinary. The durable fix is indig-node-control-interface, another lane's repo. The overlay here is a single declared,strictly-widening bridge with a test that FAILS once the contract adopts the name, so it cannot
quietly become a second opinion. Filed as
sec: control.config.setUpstream belongs on the master tier -- its effect outlives the token AND names a principal dig-node-control-interface#40 — the one issue this lane
opens (six closed, one opened). Adopting it there is what DELETES the overlay here.
§2.4b deferred: this PR does not sweep
dig-*/chia-*deps to latest. The batch issecurity-scoped across three crates and a dep cascade would dwarf it; flagging rather than silently
skipping.
Verification gaps, stated
#[cfg(unix)]tests in test(security): the token-squat guard asserts nothing under root and prints ok — CI containers commonly run as root #355 executed locally — neitherthe fixed branches nor the new pure-rule test. A
--target x86_64-unknown-linux-gnutype-checkwas attempted and failed in a vendored OpenSSL build (
perlmissingLocale::Maketext::Simple), unrelated to this change. CI's Linux leg runs them first. I havenot run the ticket's "break the guard as root and watch it still pass" demonstration.
node.keyto another host and prove it fails to authenticate" needs thetwo-host e2e fleet, not a unit test. Not run.
device.keystub message) is not addressed — fail-closed andrecoverable, and rewording it is a separate non-security change.
§908
Untouched. Nothing here signs on the user's behalf, and no fix moves a user spend key toward the
node. #257 restricts what the node's wallet transport will serve; it grants nothing.