Task
The loopback mTLS 9257 listener serves the whole Sage-parity wallet surface with no wallet_authz gate at all, so every custody, spend and master-tier method is dispatched on possession of the shared client cert alone. It is not exploitable today only because the credential cannot be obtained; persisting the cert makes an ungated wallet surface live.
Context
crates/dig-wallet/src/sage/transport.rs:213 build_router mounts POST /:method -> handle -> WalletBackend::dispatch directly. No token is read and no authorization is consulted, on either serve_mtls or serve_http/serve_dual.
crates/dig-node-service/src/server.rs:2032-2043 spawns serve_mtls on 127.0.0.1:9257 on every node start (bind failure is non-fatal), so the route is live in production.
- Authentication is
SharedCertVerifier (transport.rs:96): a client cert is accepted iff its DER equals the server's own. Possession of that cert+key is the entire credential.
crates/dig-wallet/src/sage/service.rs:299 generates the cert fresh in memory each run and never persists or exposes it, so no client can currently present it. Its own comment anticipates that "a real deployment persists this so a client can read it" — that change alone flips this from unreachable to fully open.
- Contrast the two gated planes:
server.rs:1086 (POST /{method}) and server.rs:1467 (/ws) both go through wallet_authz::authorize.
Found while closing the cross-plane escalation on PR #248 (a paired token reaching network::add_peer via the parity plane). Same class, third plane: the tier is a property of the capability, not of the transport it arrived on. #248 fixed the two token-bearing planes; this one uses a different credential and was left out of that PR's claim deliberately rather than silently — see the qualifier on pairing::revoke_paired_token.
Scope
Either apply wallet_authz inside transport.rs's handler (so one gate covers every route into dispatch), or make the absence structural and reviewable — e.g. the listener serves only the read plane. A test asserting that every route into WalletBackend::dispatch is gated is what stops a fourth plane appearing; a per-route test set cannot see this.
Do not treat the unobtainable cert as the control. It is a lockout, not a policy, and the one-line change that makes the transport usable is the same change that makes it exploitable.
Parent
https://github.com/DIG-Network/dig_ecosystem/issues/2870
Task
The loopback mTLS
9257listener serves the whole Sage-parity wallet surface with nowallet_authzgate at all, so every custody, spend and master-tier method is dispatched on possession of the shared client cert alone. It is not exploitable today only because the credential cannot be obtained; persisting the cert makes an ungated wallet surface live.Context
crates/dig-wallet/src/sage/transport.rs:213build_routermountsPOST /:method->handle->WalletBackend::dispatchdirectly. No token is read and no authorization is consulted, on eitherserve_mtlsorserve_http/serve_dual.crates/dig-node-service/src/server.rs:2032-2043spawnsserve_mtlson127.0.0.1:9257on every node start (bind failure is non-fatal), so the route is live in production.SharedCertVerifier(transport.rs:96): a client cert is accepted iff its DER equals the server's own. Possession of that cert+key is the entire credential.crates/dig-wallet/src/sage/service.rs:299generates the cert fresh in memory each run and never persists or exposes it, so no client can currently present it. Its own comment anticipates that "a real deployment persists this so a client can read it" — that change alone flips this from unreachable to fully open.server.rs:1086(POST /{method}) andserver.rs:1467(/ws) both go throughwallet_authz::authorize.Found while closing the cross-plane escalation on PR #248 (a paired token reaching
network::add_peervia the parity plane). Same class, third plane: the tier is a property of the capability, not of the transport it arrived on. #248 fixed the two token-bearing planes; this one uses a different credential and was left out of that PR's claim deliberately rather than silently — see the qualifier onpairing::revoke_paired_token.Scope
Either apply
wallet_authzinsidetransport.rs's handler (so one gate covers every route intodispatch), or make the absence structural and reviewable — e.g. the listener serves only the read plane. A test asserting that every route intoWalletBackend::dispatchis gated is what stops a fourth plane appearing; a per-route test set cannot see this.Do not treat the unobtainable cert as the control. It is a lockout, not a policy, and the one-line change that makes the transport usable is the same change that makes it exploitable.
Parent
https://github.com/DIG-Network/dig_ecosystem/issues/2870