fix: pin SS58 prefix to 189 and harden Rust error handling (L5, L6)#581
fix: pin SS58 prefix to 189 and harden Rust error handling (L5, L6)#581n13 wants to merge 3 commits into
Conversation
🔍 Review — L5, L6 (SS58 prefix pinning & Rust error hardening)Verdict: 🟡 Approve with non-blocking comments The core L5/L6 remediation is correct and cleanly executed: What it does
Strengths
Findings
VerificationAdequate for the primary finding. The added Dart test directly asserts that a foreign-prefix (42) address and garbage both throw, which is the crux of L5 at the fixed entry point, and I independently confirmed the prefix-189 replacement address is byte-equivalent to the original. PR-body verification claims ( 🤖 AI-assisted review generated with Claude Code |
Resolve Cargo.toml/Cargo.lock conflicts by taking main's updated dependency set (qp-wormhole-* 3.1.0, sp-core 43, etc.) and dropping the now-unused nam-tiny-hderive dep that this branch removed. Cargo.lock regenerated; `cargo check` passes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Summary
Addresses findings L5 and L6 of the 2026-07-22 mobile wallet security audit.
L5 — Foreign SS58 network prefixes accepted as valid recipients
ss58_to_account_id(quantus_sdk/rust/src/api/crypto.rs) now decodes withAccountId32::from_ss58check_with_versionand rejects any prefix other than the Quantus prefix 189 (verified against the chain runtime'sSs58AddressFormat::custom(189)andAppConstants.ss58prefix). Previously any registered prefix (Polkadot 0, Kusama 2, generic 42, ...) validated and decoded to an uncontrolledAccountId32, burning funds.isValidSS58Addressand all otherss58ToAccountIdcall sites keep working unchanged: the FRB Dart signature is stillUint8List ss58ToAccountId(...), it now throws on invalid/foreign input instead of aborting on a Rust panic. Foreign prefixes are now rejected.L6 — Rust API panic style on user-controlled input
ss58_to_account_id:unwrap→Result<Vec<u8>, String>(matches the existingResult<T, String>pattern used throughoutwormhole.rs/first_hash_to_address).generate_keypair:expect→Result<Keypair, HDLatticeError>(same error type already used bygenerate_derived_keypair/derive_wormhole).wormhole_compute_output_amount: checked arithmetic (checked_subfor thefee_bps > 10000underflow,checked_mul,u32::try_from) →Result<u32, String>.decode_leaf_amount: silentas u32truncation → checkedu32::try_fromwith an error. Kept theu32return type (consistent withProofInputamount fields) so the Dart signature is unchanged.derive_hd_pathFRB export — verified zero Dart callers outside the generated bindings (greppedquantus_sdk/lib,mobile-app/lib,miner-app/lib,cold-wallet-app/lib). Its only dependency,nam-tiny-hderive, was removed fromCargo.toml.Bindings regeneration
FRB bindings were regenerated with
flutter_rust_bridge_codegen generate(v2.12.0, matching the pinnedflutter_rust_bridge = "=2.12.0"). The public Dart API is source-compatible: all changed functions keep their Dart return types and now throw instead of panicking; all existing call sites already wrap these calls in try/catch or propagate exceptions.Note: the local Dart formatter (SDK 3.12.0) re-wrapped parts of the regenerated files; purely mechanical formatting churn in
frb_generated.*. Formatting-only changes to the untouchedapi/multisig.dartandapi/ur.dartwere reverted to keep the diff minimal.Test fixes
generate_keys_test.dart: the wormhole test compared against a generic Substrate (prefix 42) address; replaced with the prefix-189 encoding of the same account bytes, and added a test assertingss58ToAccountIdrejects the foreign-prefix and garbage inputs.recovery_proxy_encoding_test.dart:setUpAllnow callssetDefaultSs58Prefix(prefix: 189)(it only ranRustLib.init(), sotoAccountIdencoded with the sp-core default prefix 42, which is now rejected on decode).Verification
cargo check— cleancargo clippy— clean (one pre-existingneedless_range_loopwarning in untouchedgenerate_proofcode)cargo test --lib— 13/13 passeddart analyze(quantus_sdk) — no issuesflutter test(quantus_sdk, full suite, incl. native tests against a release build of the Rust lib) — 134/134 passedScope notes
rust-transaction-parserwas inspected per the audit scope and left alone — no SS58 decoding there.expects on internally-generated key material (to_ml_dsa,generate_keypair_from_seed,sign_message) were left as-is; the audit flags only the user-controlled entry points fixed here.