feat(module): let the module hold the key, over confidential calls - #23
Conversation
Adds DeriveAccount, SignTransaction and ExportKey. Each takes a recovery phrase and may only be reached by a confidential call, which tinybus delivers to a module whose artifact the host hashed against a digest an operator asserted, or to nobody. The existing BuildUnsigned/AttachSignature pair is unchanged and not deprecated. It is still the right shape whenever the backend is reachable across a transport, where the bus cannot say what is on the other end. The new set is for a loaded backend, where it is strictly better: the host stops linking the derivation and signing stack at all. This is admission control, not isolation. A loaded module shares the host's address space and never needed the bus to reach a secret; what the rule buys is that the bus will not deliver one to code nobody allowlisted. Notes: - SecretMaterial and ExportedKey implement Debug by hand and print <redacted>. A derived one would put a live recovery phrase into every log line and panic message that formatted a request. - SignTransaction refuses a request whose derivation chain disagrees with its transaction's chain, which would otherwise sign from an address the user never saw. - The phrase is wiped after each call. That bounds this module's own copy; it does not reach the JSON frame the bus decoded from, and the doc says so rather than implying more. - module_export!'s method list is hand-maintained and unchecked by the compiler, so the three new methods are added there too and the E2E's drift guard extended to match. Co-authored-by: Medulla <medulla@tinyhumans.ai>
|
Warning Review limit reached
Next review available in: 38 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What changes
Three methods, each reachable only by a confidential call:
BuildUnsigned/AttachSignatureare unchanged and not deprecated.Why both flows stay
The existing split — host holds the key, module returns digests, host signs — is
the right shape whenever the backend is reachable: a service in its own
process, across a socket. There the bus cannot say what is on the other end, so
the key must not go.
tinybus can now say for a loaded backend. A confidential message goes to a
module whose artifact the host hashed against a digest an operator asserted, or
to nobody: never to a transport peer, never fanned out, never printed by
monitor. For a host that loads this module the new flow is strictly better —it stops linking the derivation and signing stack entirely.
This is admission control, not isolation. A loaded module shares the host's
address space and can read host memory directly; it never needed the bus to
reach a secret. What the rule buys is that the bus will not be the delivery
mechanism for code nobody allowlisted. A backend whose compromise must not
reach a key belongs in a separate process — where this design makes it
ineligible, and where the original pair is the one to use.
The parts worth reviewing
Debugis hand-written onSecretMaterialandExportedKey, printing<redacted>. A derived one would put a live recovery phrase into every logline, panic message and error report that ever formatted a request — the exact
leak this arrangement prevents, arriving through the back door. Asserted, not
left to review.
SignTransactionrefuses a request whosesecret.chaindisagrees with its transaction's chain. Without it a Solanaphrase walked with EVM rules would sign a real EVM transaction from an address
the user has never seen, and nothing downstream would object.
Schemearm refuses rather than guesses.Schemeis#[non_exhaustive], so a future variant compiles against this build andarrives at that match. Falling back to either arm would sign real bytes with
the wrong scheme.
bounds this module's copy — keeping it out of core dumps and swap — and does
not reach the JSON frame the bus decoded from or reallocations
Stringmadewhile decoding. The doc says exactly that rather than implying more.
wirestays dependency-free. The new types are serde-only;cargo check -p tinywallet --no-default-features --features wirestill passes, so a hostcan take the contract without linking a chain library. That is also why
SecretMaterialcannot implementDropitself —zeroizeis a dependency.A drift trap this surfaced
module_export!'smethods = [...]is hand-maintained and unchecked by thecompiler — a method missing there is simply not advertised to a host. The E2E's
EXPECTED_METHODSguard compares it against another hand-written list, soneither caught three new interface methods. Both lists are updated and the
coupling is now written down in both places.
Tests
7 new unit tests plus an E2E case against the real
dlopen'd module.The two that carry the argument:
the_one_shot_path_agrees_with_the_split_path_byte_for_byte— signing fromthe phrase must equal the split flow exactly, or moving the key has changed
what gets broadcast. Repeated for Solana, because it is the ed25519 arm and the
only chain whose payload is the message rather than a digest, so the secp256k1
case says nothing about it.
refuses_a_confidential_call_to_an_unattested_module(E2E) — the module isloaded from a bare path with no
modules.toml, so nobody vouched for it andthe broker refuses to carry the phrase. The same call unflagged is then
carried and answered, so the test cannot pass by the method being broken or
unadvertised. This is the negative half; a positive result alone would be
equally consistent with a broker that hands secrets to anyone.
Also covered: the published BIP-39 vector address, that
DeriveAccountreturns apublic key and not the secret, and that a rejected phrase is not quoted back in
the error.
Follow-up
Needs a release before openhuman can use it, and openhuman then drops its
tinywallet feature set from 71 packages to 15.