Skip to content

v0.2.7 — security hot-fix#3

Merged
outputlayer merged 4 commits into
mainfrom
v0.2.7-security
May 8, 2026
Merged

v0.2.7 — security hot-fix#3
outputlayer merged 4 commits into
mainfrom
v0.2.7-security

Conversation

@outputlayer
Copy link
Copy Markdown
Owner

Summary

Security hot-fix release. Three independent hardenings on the wallet/keys path, plus a follow-up fix that keeps Jupiter Z (RFQ) and Ultra gasless flows working under the new verifier.

  • Verify Jupiter swap instructions before signing (crates/ondo/src/wallet/verify.rs). Decodes the base64 transaction Jupiter returns and refuses to sign unless the on-chain instructions match the user's intent: input mint/amount, output mint, and that the wallet pubkey is among the signers. The input-transfer authority check independently confirms the wallet authorized the actual debit.
  • Encryption-by-default for keys generate / keys import. Plaintext key.json requires explicit --allow-plaintext (with stderr warning). Existing plaintext keys remain readable; keys show warns when it sees one.
  • Min 12-char passphrase + rejection of digits-only passphrases. One-time stderr warning when RWA_PASSPHRASE is read from env.
  • Gasless support: the verifier searches for the wallet pubkey across all signer slots [0..num_required_sigs) rather than requiring it at index 0. Jupiter Z makes the market maker the fee payer at index 0; Ultra gasless adds Jupiter as a secondary fee payer. The wallet is a signer at index 1+ in both cases.

Why now

Doc-cross-check against current Jupiter Ultra API caught the gasless regression before merge — the initial verifier would have refused every gasless swap with OwnerMismatch. The follow-up fix adds 2 regression tests (accepts_gasless_with_external_fee_payer, gasless_rejects_when_owner_not_signer).

Test plan

  • cargo test --workspace — 33 (cli) + 199 (ondo lib) + 6 (ondo integration) = 238 tests, all green (was 190 on main, +48)
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo build --release — clean
  • wallet::verify::tests covers 4 reject scenarios + happy path + tolerance + gasless accept + gasless reject (13 tests)
  • Manual smoke after merge: rwa keys generate (creates key.age), rwa keys generate --allow-plaintext (creates key.json + warn), rwa buy TSLAon 10 --dry-run, rwa portfolio

Known limitations (tracked for v0.3)

  • ALT-resolved output ATAs use a soft static-keys fallback. Defense-in-depth via simulateTransaction is documented inline and planned for v0.3.
  • Route-mode awareness (separate strict-AMM / lenient-RFQ paths) is also v0.3.

🤖 Generated with Claude Code

outputlayer and others added 4 commits May 8, 2026 19:02
Decode the base64 transaction Jupiter returns and refuse to sign unless the
on-chain instructions match the user's intent: input mint and amount, output
mint, and signer (fee-payer = wallet pubkey). Compromised Jupiter responses or
MITM tampering on a custom RPC URL can no longer redirect funds.

The verifier tolerates compute-budget and ALT-extend instructions; unknown
extras are allowed but at least one SPL Token transfer/transfer_checked from
our input ATA at the expected amount is required. ALT-resolved hot-pool ATAs
fall back to a best-effort static-keys lookup; full ALT resolution would
require an RPC fetch and is out of scope for this hot-fix.

Adds wallet::sign_jupiter_swap as a thin wrapper around sign_transaction;
sign_transaction itself stays generic so transfer_sol/transfer_spl paths
continue to work unchanged. ExpectedSwap is built once in execute_with_retry
from SwapParams and threaded through execute_order, execute_managed_order,
and execute_metis_order — every path that ends up at Jupiter is guarded.

Tests: 11 parser-level scenarios in wallet::verify::tests (4 reject paths +
happy + tolerance + roundtrips) plus 2 wallet-level integration tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
rwa keys generate and rwa keys import now write key.age (passphrase-encrypted)
by default. --allow-plaintext opts out (with stderr warning); the legacy
--encrypt flag is hidden but still accepted to preserve scripts that opt in
explicitly. rwa keys show prints a deprecation warning when it reads a
plaintext key.json.

prompt_new_passphrase rejects passphrases shorter than 12 characters and
rejects digits-only passphrases — short or low-entropy passphrases were a
trivial offline-brute-force vector against age's scrypt-default KDF when
combined with a stolen key.age file.

When the passphrase is read from RWA_PASSPHRASE, a one-time stderr warning
is printed (gated by OnceLock so multiple call sites — keys.rs and gm/mod.rs
— do not spam the user). The warning explains shell-history, ps -E, and
core-dump leakage paths and points users at interactive entry.

Tests: keys_generate_default_creates_age_file, keys_generate_allow_plaintext_creates_json_file,
passphrase_min_length_enforced, passphrase_digits_only_rejected,
passphrase_strong_ok, passphrase_exactly_12_nondigit_ok.

Breaking-change note (UX, not data): scripts that relied on the previous
default of plaintext output should add --allow-plaintext or migrate to
encrypted keys. Existing key.json files remain readable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump workspace version 0.2.6 -> 0.2.7 and document the security hot-fix in
CHANGELOG.md (Verify Jupiter swap instructions, encryption-by-default,
min passphrase length, RWA_PASSPHRASE env warning).

Test count workspace-wide: 234 -> 236.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The initial v0.2.7 verifier required the wallet pubkey at signer index 0
(fee payer). That broke every gasless route: Jupiter Z (RFQ) puts the
market maker at index 0, and Ultra gasless adds Jupiter as a secondary
fee payer — in both cases the wallet is a signer at index 1+.

Search for the wallet pubkey across all signer slots
[0..num_required_signatures) instead of pinning it to the fee-payer slot.
The input-transfer authority check is unchanged and is what actually
confirms the wallet authorized the debit, so loosening the signer-position
check does not weaken security: a tx where the wallet is not a signer at
all still fails (new test gasless_rejects_when_owner_not_signer).

Tests: 11 -> 13 in wallet::verify::tests. New regression
accepts_gasless_with_external_fee_payer mirrors the Jupiter Z layout
(MM at index 0, owner at index 1, both signers, TransferChecked authority
= owner). Workspace total 236 -> 238.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@outputlayer outputlayer merged commit 7594b6f into main May 8, 2026
3 checks passed
@outputlayer outputlayer deleted the v0.2.7-security branch May 8, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant