Skip to content

sec: the mTLS 9257 wallet transport dispatches custody and master-tier methods with no authz gate #257

Description

@MichaelTaylor3d

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:maintenancereal work that changes nothing a user can perceive

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions