fix: audit remediations for external signatures#34
Open
0xLeo-sqds wants to merge 10 commits into
Open
Conversation
1. Bind async creation messages to payload hash create_transaction_message, create_settings_transaction_create_message, and create_batch_add_transaction_message now include a hash of the instruction payload. Without this, a malicious relayer could take an external signer's signature and attach it to a different payload (different transaction content or settings actions) while keeping the same consensus account and transaction index. The sync path already included payload hashes — this brings the async creation path to parity. 2. Strip instructions sysvar before is_active() in async handlers When a Policy uses SettingsState expiration, is_active() expects the Settings account at remaining_accounts[0]. Precompile verification expects the instructions sysvar at remaining_accounts[0]. These conflict when both are needed. Fixed by stripping the sysvar (if present) before passing accounts to is_active(). verify_signer handles the sysvar internally via split_instructions_sysvar(). Applied to: transaction_execute, transaction_create, proposal_create, proposal_vote, transaction_create_from_buffer. 3. Move evaluate_instruction_constraints after pre-hook execution Constraints were validated against pre-hook account state, but inner instructions execute against post-hook state. Moved constraint evaluation after pre-hook in both execute_payload_async and execute_payload_sync in ProgramInteractionPolicy. 4. Bounds check num_signer in validate_synchronous_consensus num_signer is user-provided (u8 from instruction args) and was used directly as a slice bound without validation. If num_signer exceeds remaining_accounts length, the program panics. Added explicit require!() check.
…tripping - Generalize create_session_key and revoke_session_key from Account<Settings> to InterfaceAccount<ConsensusAccount> so session keys work on both Settings and Policy accounts. Add signers_mut() to the Consensus trait for mutable signer access. - Fix policy sync signer stripping (program_interaction.rs): was using canonical SmartAccountSigner::key() for outer_signer_keys, which misses session keys whose raw address differs from the parent's canonical key. Now collects raw AccountInfo keys, matching the pattern already used in transaction_execute_sync.rs.
c900c58 to
b526c33
Compare
Previously MigrateToV2 silently no-oped force_v2() but still ran invalidate_prior_transactions(), nuking all in-flight proposals for no reason. Now rejects with InvalidInstructionArgs if signers are already V2.
Prevents reverse session-key collision where a new signer's canonical key matches an active session key, which could cause identity confusion in verify_signer/resolve_signer_key.
Previously resolve_signer_key matched session keys without checking expiration, allowing expired session keys to resolve to the parent signer's canonical key in buffer close and event logging paths.
External signers can't pay for proposal realloc since their AccountInfo is not a native tx signer. Fall back to the last remaining account (must be signer + writable) when the signer itself can't pay.
External signers have canonical keys derived from non-ed25519 curve material — lamports sent there are permanently locked. Store the original rent_payer as rent_collector at buffer creation time and close to it instead of creator. Creator remains the authorization authority for closing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On-chain fixes for security issues identified during the external signatures audit.
Fixes
1. Payload hash binding in async creation messages
Issue:
create_transaction_v2,create_settings_transaction_v2, andadd_transaction_to_batch_v2construct signed messages that don't include the payload. A malicious relayer can take an external signer's signature and attach it to a completely different payload.Fix: Added
payload_hashparameter tocreate_transaction_message,create_settings_transaction_create_message, andcreate_batch_add_transaction_message. Callers hash the instruction args before constructing the message. This matches what the sync path already does.Files:
messages.rs,transaction_create.rs,settings_transaction_create.rs,batch_add_transaction.rs2. SettingsState expiration + precompile sysvar conflict
Issue: In the async path,
is_active()expects the Settings account atremaining_accounts[0]for SettingsState expiration, while precompile verification expects the instructions sysvar atremaining_accounts[0]. External signers using precompile verification are blocked from participating in policies with SettingsState expiration.Fix: Strip the instructions sysvar from
remaining_accountsbefore passing tois_active(), matching the sync flow pattern.verify_signerhandles the sysvar internally viasplit_instructions_sysvar().Files:
transaction_execute.rs,transaction_create.rs,proposal_create.rs,proposal_vote.rs,transaction_create_from_buffer.rs3. Constraint evaluation ordering relative to pre-hook
Issue:
evaluate_instruction_constraintsruns before pre-hook execution. Constraints validate account state that exists before the pre-hook runs, but inner instructions execute against post-hook state.Fix: Moved
evaluate_instruction_constraints()after pre-hook execution in bothexecute_payload_asyncandexecute_payload_sync.Files:
program_interaction.rs4. num_signer bounds check in synchronous consensus
Issue:
num_signer(user-provided u8) is used as a slice bound onremaining_accountswithout validation. Ifnum_signer > remaining_accounts.len(), the program panics.Fix: Added
require!((num_signer as usize) <= remaining_accounts.len())before the slice access.Files:
context_validation.rs5. Sync session-key signer privilege leak in inner CPIs
Issue:
SynchronousTransactionMessage::new_validatedstrippedis_signerfrom inner CPI accounts by matching against canonical signer keys (SmartAccountSigner::key()). Session keys have a raw address that differs from the parent's canonical key, so they were never matched — retainingis_signer=truein inner CPI calls.Fix: Collect raw
AccountInfokeys from the outer signer accounts instead of canonical keys from the signer list.Files:
transaction_execute_sync.rs,transaction_execute_sync_legacy.rs,synchronous_transaction_message.rs6. Policy sync signer stripping (same class as #5)
Issue:
execute_payload_syncin the policy execution engine had the same canonical-key stripping bug as #5. Session keys authenticated against a Policy could retain signer privilege in inner CPIs.Fix: Same pattern — collect raw
AccountInfokeys filtered byis_signerinstead of canonicalSmartAccountSigner::key()values.Files:
program_interaction.rs7. Session key create/revoke generalized to ConsensusAccount
Issue:
create_session_keyandrevoke_session_keywere hardcoded toAccount<Settings>. The Consensus trait already supports session keys on Policies (find_signer_by_session_key,verify_signer), but there was no instruction to create or revoke session keys on Policy accounts — leaving policy session keys with no lifecycle management.Fix: Generalized both instructions to
InterfaceAccount<ConsensusAccount>withcheck_derivationconstraint. Addedsigners_mut()to the Consensus trait for mutable signer access through the interface.Files:
create_session_key.rs,revoke_session_key.rs,consensus_trait.rs,consensus.rs,settings.rs,policy.rs8. MigrateToV2 on already-V2 accounts silently nukes proposals
Issue:
MigrateToV2callsforce_v2()(no-op if already V2) theninvalidate_prior_transactions()unconditionally. On an already-V2 account, this does nothing useful but invalidates all in-flight proposals as a side effect.Fix: Added
require!(matches!(self.signers, SmartAccountSignerWrapper::V1(_)))guard — rejects withInvalidInstructionArgsif signers are already V2.Files:
settings.rs9.
add_signerreverse session key collisionIssue:
create_session_keyprevents forward collisions (session key != any signer key), butadd_signerdoes NOT check the reverse: whether the new signer's canonical key collides with an existing session key. If a collision exists, the session key holder authenticates as the wrong signer (with potentially different permissions) — a governance threshold bypass.Fix: Added
has_session_key_assigned(&signer.key())check inadd_signer, mirroring the forward check increate_session_key.Files:
wrapper.rs10.
resolve_signer_keydoes not check session key expirationIssue:
resolve_signer_keymatched session keys viaget_session_key_data_if_matcheswithout checking expiration. An expired session key could still resolve to the parent signer's canonical key, affecting buffer close authorization and event logging accuracy.Fix: Added
Clock::get()expiration check in the session key match path — expired session keys now fail withNotASigner.Files:
consensus_trait.rs11. Cancel realloc fails for external signers
Issue:
cancel_proposal_innerusessigner.to_account_info()as rent payer forrealloc_if_needed. External signers' AccountInfo is not a native tx signer and may lack lamports, causing the system transfer CPI to fail.Fix: If the signer is not a native tx signer (not
is_signer && is_writable), fall back to the lastremaining_account(must beis_signer && is_writable).Files:
proposal_vote.rs12. Buffer
close = creatorsends lamports to unspendable addressIssue:
TransactionBufferstorescreatoras the canonical governance key. For P256/secp256k1 signers, this is derived from non-ed25519 curve material — nobody holds the private key. Anchor'sclose = creatorpermanently locks lamports at that address.Fix: Added
rent_collector: Pubkeyfield toTransactionBuffer, set torent_payer.key()at creation time. Changedclose = creatortoclose = rent_collectorwithhas_one = rent_collectorin bothCloseTransactionBufferandCreateTransactionFromBuffer. Creator remains the authorization authority.Files:
transaction_buffer.rs,transaction_buffer_create.rs,transaction_buffer_close.rs,transaction_create_from_buffer.rsNote: we know this is a breaking changes but there are zero buffer opened on mainnet currently so this is okay to do so.