Skip to content

Validate auth entries before signing#2530

Open
mootz12 wants to merge 13 commits into
mainfrom
validate-auth-entries
Open

Validate auth entries before signing#2530
mootz12 wants to merge 13 commits into
mainfrom
validate-auth-entries

Conversation

@mootz12
Copy link
Copy Markdown
Contributor

@mootz12 mootz12 commented Apr 29, 2026

What

The CLI currently relies on the RPC to check that no non-root auths are included in simulation results. This PR adds an explicit, per-entry validation step inside sign_soroban_authorizations that classifies every Address-credential auth entry against the transaction's host function before signing. Entries that don't match the host function exactly require approval. This approval can be bypassed with a --force flag.

Example output:

$ stellar contract invoke --source alice --id CA3WF5KPVE2TXQQSOEQPVD3J6GIZ7G74UA2H7BNQMHBQPOON6XV4PHT4 -- diff_auth_sub_auth --addr bob --val "Test" --subcall CAXDPLG2XWFA3LI3SUDG7AIQ7MF7ZJMFBEQYRGTZIGLT7OLZ243IU3FE
ℹ️  Simulating transaction…
⚠️  Authorization entry does not match the current contract call, and needs approval:
  Auth Entry:
    Signer: GCFEAQ5A5BOO4NYTCDN63TF2UX2DS3T3KLNZDHPDPC3IVGCPR37RSRCC
    Invocation:
      Contract: CA3WF5KPVE2TXQQSOEQPVD3J6GIZ7G74UA2H7BNQMHBQPOON6XV4PHT4
      Fn: diff_auth_sub_auth
      Args:
        "1"
        "2"
      Sub-invocation #0:
        Contract: CAXDPLG2XWFA3LI3SUDG7AIQ7MF7ZJMFBEQYRGTZIGLT7OLZ243IU3FE
        Fn: do_auth
        Args:
          "GCFEAQ5A5BOO4NYTCDN63TF2UX2DS3T3KLNZDHPDPC3IVGCPR37RSRCC"
          Test

⚠️  Sign this authorization entry? (y/N)
y
ℹ️  Signing transaction: ebf3db4651a6cd77ed8a9f2782938eaedd1603e4942a9ceab99b467dd8c04f40
🌎 Sending transaction…
✅ Transaction submitted successfully!
"Test"

If the CLI is invoked in a non-interactive location and the force flag is not preset, it will fail:

$ echo | stellar contract invoke --source alice --id CA3WF5KPVE2TXQQSOEQPVD3J6GIZ7G74UA2H7BNQMHBQPOON6XV4PHT4 -- diff_auth_sub_auth --addr bob --val "Test" --subcall CAXDPLG2XWFA3LI3SUDG7AIQ7MF7ZJMFBEQYRGTZIGLT7OLZ243IU3FE 
ℹ️  Simulating transaction…
⚠️  Authorization entry does not match the current contract call, and needs approval:
  Auth Entry:
    Signer: GCFEAQ5A5BOO4NYTCDN63TF2UX2DS3T3KLNZDHPDPC3IVGCPR37RSRCC
    Invocation:
      Contract: CA3WF5KPVE2TXQQSOEQPVD3J6GIZ7G74UA2H7BNQMHBQPOON6XV4PHT4
      Fn: diff_auth_sub_auth
      Args:
        "1"
        "2"
      Sub-invocation #0:
        Contract: CAXDPLG2XWFA3LI3SUDG7AIQ7MF7ZJMFBEQYRGTZIGLT7OLZ243IU3FE
        Fn: do_auth
        Args:
          "GCFEAQ5A5BOO4NYTCDN63TF2UX2DS3T3KLNZDHPDPC3IVGCPR37RSRCC"
          Test

❌ error: An authorization entry requires confirmation, but stdin is not interactive. Rerun with --force to sign anyway.

Why

The CLI eagerly signs authorization entries returned from the user-specified RPC. If an unsafe auth entry is included, the user might unexpectedly sign for something they did not intend. This check ensures everything the CLI signs automatically is bound to the exact host function invocation in the transaction.

Close https://github.com/stellar/stellar-cli-internal/issues/50

Known limitations

require_auth_for_args for non-source accounts

The check flags contracts that use require_auth_for_args(custom_args) at the root for non-source accounts. The auth tree's root carries custom_args, not the host function's args, so the strict-match check fails even though the auth is genuinely rooted at the operation. A tampered auth entry with the same custom args at root could otherwise be signed and replayed. Source-account auth via SorobanCredentials::SourceAccount is unaffected.

Copilot AI review requested due to automatic review settings April 29, 2026 16:43
@github-project-automation github-project-automation Bot moved this to Backlog (Not Ready) in DevX Apr 29, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens soroban-cli transaction signing by explicitly validating each Address-credential Soroban auth entry against the transaction’s host function before signing, rejecting entries that are unsafe to replay or malformed, and improving the error output by rendering the offending auth entry inline.

Changes:

  • Added a host-function vs auth-root-invocation classifier and enforced “strict” auth validation in sign_soroban_authorizations.
  • Introduced pretty-print formatting for auth entries to improve CLI error messages.
  • Added coverage via a new auth fixture contract + integration tests, and added missing RPC network-passphrase verification in extend/restore commands.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
cmd/soroban-cli/src/tx.rs Updates comment to reflect new validation behavior prior to signing.
cmd/soroban-cli/src/signer/validation.rs Adds auth root-invocation classification logic + unit tests.
cmd/soroban-cli/src/signer/mod.rs Enforces strict validation before signing; adds new errors and source-account credential guard; adds unit tests.
cmd/soroban-cli/src/log/auth.rs Replaces prior debug helper with structured formatting for auth entries.
cmd/soroban-cli/src/commands/contract/restore.rs Adds verify_network_passphrase call on RPC client.
cmd/soroban-cli/src/commands/contract/extend.rs Adds verify_network_passphrase call on RPC client.
cmd/crates/soroban-test/tests/it/integration/util.rs Adds AUTH fixture constant.
cmd/crates/soroban-test/tests/it/integration/auth.rs Adds integration coverage for strict vs non-strict/non-root auth scenarios.
cmd/crates/soroban-test/tests/it/integration.rs Wires in the new auth integration test module.
cmd/crates/soroban-test/tests/fixtures/test-wasms/auth/src/lib.rs Adds a Soroban test contract to generate various auth-tree shapes.
cmd/crates/soroban-test/tests/fixtures/test-wasms/auth/Cargo.toml Adds the new test fixture crate manifest.
Cargo.lock Records the new test_auth fixture crate dependency.

Comment thread cmd/soroban-cli/src/log/auth.rs
Comment thread cmd/soroban-cli/src/signer/validation.rs
Comment thread cmd/soroban-cli/src/signer/mod.rs
Copy link
Copy Markdown
Member

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Copy Markdown
Member

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Copy Markdown
Member

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Copy Markdown
Member

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Copy Markdown
Member

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copy link
Copy Markdown
Member

@leighmcculloch leighmcculloch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't finished reviewing this, but it's not obvious to me that this is the appropriate action, it's rather restrictive, especially in the context of the known limitations in the pr description.

I think given the stellar-cli is a developer tool it would be more appropriate to do the following instead of rejecting outright:

  • validate the safe case and auto sign in that case (most commands other than invoke should have relatively predictable auth entries required)

  • identify the unsafe cases and display those to the user to confirm if they wish to continue

And for a tool that has narrower use, such as a wallet cli rather than a developer focused cli, to provide narrower restrictions like this to protect its use case.

Comment thread cmd/soroban-cli/src/signer/mod.rs
Comment thread cmd/soroban-cli/src/signer/validation.rs Outdated
@mootz12
Copy link
Copy Markdown
Contributor Author

mootz12 commented Apr 30, 2026

I think given the stellar-cli is a developer tool it would be more appropriate to do the following instead of rejecting outright:

  • validate the safe case and auto sign in that case (most commands other than invoke should have relatively predictable auth entries required)
  • identify the unsafe cases and display those to the user to confirm if they wish to continue

Thanks for the feedback @leighmcculloch !

This PR is basically step 1, identify "safe" auth. In my mind, I defined this as any auth entry that is tied to the root invocation exactly. Since the idea of this is not to limit user actions, we can assume the user input the contract invocation as intended. Thus, even if the auth could be detached, the only way its valid is if the exact contract invocation the user intended was the root invocation.

To add, I'd be shocked if this was actually restrictive. The only use case it blocks is non-source accounts signing require_auth_for_args, when the only way to provide additional signers is via arguments as it stands.


Note - updated to add a bypass flag and approval mechanism.

Created a follow up issue to support non-root auth: #2574

@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

Claude encountered an error —— View job


Claude PR Review

Reviewing the auth entry validation changes…

  • Explore PR diff and changed files
  • Review signer/validation.rs (new classifier)
  • Review signer/mod.rs (validation/signing flow + approval UX)
  • Review log/auth.rs (pretty-print)
  • Review extend/restore network passphrase checks
  • Review integration tests + fixture contract
  • Post findings
    · Branch: validate-auth-entries

Comment thread cmd/soroban-cli/src/signer/mod.rs
Comment thread cmd/soroban-cli/src/signer/mod.rs
Comment thread cmd/soroban-cli/src/signer/mod.rs
Comment thread cmd/soroban-cli/src/log/auth.rs Outdated
Comment thread cmd/soroban-cli/src/signer/validation.rs
Comment thread cmd/soroban-cli/src/commands/contract/extend.rs
Comment thread cmd/soroban-cli/src/config/sign_with.rs Outdated
@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @mootz12's task in 2m 50s —— View job


Claude PR Review

I went through the auth-validation flow end-to-end. The classifier is sound and the test coverage of the strict/non-strict/source-account/upload-wasm matrix is solid. Most of my concerns are about the prompt/approval UX and a few sharp edges in sign_soroban_authorizations that haven't shifted since my last pass.

  • Read CLAUDE.md and explore PR diff
  • Review signer/validation.rs (new classifier)
  • Review signer/mod.rs (validation/signing flow + approval UX)
  • Review log/auth.rs (pretty-print)
  • Review config/sign_with.rs (--force flag)
  • Review extend/restore network passphrase checks
  • Review integration tests + fixture contract
  • Post findings

Blocking issues

1. --force signs non-strict entries with no audit trailcmd/soroban-cli/src/signer/mod.rs:107

validation::AuthStyle::NonStrict => {
    if !skip_approval {
        confirm_non_strict_authorization(raw_auth)?;
    }
}

confirm_non_strict_authorization is the only place where the auth entry is printed. With --force, the user (or CI) gets zero output about what was signed under the relaxed policy — the entire reason this PR exists is that signing unintended auth entries is dangerous, and --force should not silently restore the pre-PR footgun.

fnando already +1'd this in the earlier round. Suggested shape: always render the warning + entry; only gate the interactive prompt on skip_approval. Fix this →

2. todo!() panics on RPC-controlled inputscmd/soroban-cli/src/signer/mod.rs:122-124

ScAddress::MuxedAccount(_) => todo!("muxed accounts are not supported"),
ScAddress::ClaimableBalance(_) => todo!("claimable balance not supported"),
ScAddress::LiquidityPool(_) => todo!("liquidity pool not supported"),

leighmcculloch flagged this earlier and you agreed but deferred. Since this PR's whole premise is "the RPC might return adversarial auth entries", leaving three panic paths reachable from RPC output is at odds with the threat model. A malformed credential address from a hostile simulation result panics the CLI instead of failing gracefully. Returning Error::ContractAddressAreNotSupported (or a similarly worded variant) is one line per arm and removes the regression risk. Fix this →


Worth fixing before merge

3. Signer lookup keeps iterating after a matchcmd/soroban-cli/src/signer/mod.rs:144-149

let mut signer: Option<&Signer> = None;
for s in signers {
    if auth_address_bytes == &s.get_public_key()?.0 {
        signer = Some(s);
    }
}

No break. For SignerKind::SecureStore (or Ledger without a cached PK), get_public_key() hits the OS keychain / device. With multiple signers in the slice, you do that work unnecessarily for every entry after the match. Drop in a break (or rewrite as signers.iter().find(...)). Fix this →

4. CreateContract auth entries show no detail in the promptcmd/soroban-cli/src/log/auth.rs:63-67

SorobanAuthorizedFunction::CreateContractHostFn(_)
| SorobanAuthorizedFunction::CreateContractV2HostFn(_) => {
    let _ = writeln!(result, "{prefix}  CreateContract");
}

If partial_auth_sub_auth-style logic ever attaches a non-strict create-contract auth, the user is asked to approve a line that just says CreateContract. They can't tell what wasm or constructor args are being authorized. Print at least the wasm hash and constructor args (mirroring the ContractFn branch), otherwise the prompt is effectively unsigned consent. Fix this →

5. --force lives on sign_with::Args — silently no-op on ~50 subcommandscmd/soroban-cli/src/config/sign_with.rs:73-75

FULL_HELP_DOCS.md confirms this flag now appears on tx sign, tx send, every key/identity command, etc. — but the prompt it bypasses only fires inside sign_soroban_authorizations, which only runs for Soroban invoke flows. Two concrete improvements:

  • Rename as fnando suggested. --force is heavily overloaded across CLI tools. --confirm-signing=false / --auto-approve / --no-confirm would all be clearer. (Personally lean --auto-approve.)
  • Scope the flag to commands that actually call sign_soroban_authorizations, or at minimum tighten the help text so users know when it does anything.

Nits / lower priority

6. Cross-variant mismatch returns NonStrict, not Invalidcmd/soroban-cli/src/signer/validation.rs:38-57

A ContractFn auth attached to a CreateContract host fn falls into the _ => false arm and surfaces as "Authorization entry does not match the current contract call". A user reading that prompt can't distinguish "right kind of op, different args" from "structurally wrong op kind". You told copilot it's "valid, just not strict" — defensible, but the prompt UX doesn't reflect that distinction. Either return Invalid on cross-variant pairs, or differentiate the warning text so users have real signal to decide on.

7. Print::new(false) hardcoded in approval promptcmd/soroban-cli/src/signer/mod.rs:192

This ignores --quiet. Probably intentional (a safety prompt should bypass quiet mode), but worth a one-line comment so a future contributor doesn't "fix" it by threading quiet through.

8. extend.rs / restore.rs network-passphrase additions are unrelatedcmd/soroban-cli/src/commands/contract/extend.rs:201, restore.rs:170

Good change on its own, but it's not mentioned in the PR description and doesn't belong to the auth-validation theme. Either call it out in the description or split into a follow-up PR — if the auth changes get reverted later, you don't want to lose the passphrase checks with them.


Things I liked

  • The classifier is small, well-tested, and the Invalid / NonStrict / Strict taxonomy is the right shape.
  • The source-account-credential-as-auth-address guard at signer/mod.rs:137 is a nice belt-and-suspenders against malformed simulation output.
  • Test fixture contract covers the interesting auth-tree shapes (root, sub, partial-args, source vs non-source).
  • The auths_modified short-circuit at line 175 correctly preserves the "no changes ⇒ Ok(None)" contract.
    · Branch: validate-auth-entries

@claude
Copy link
Copy Markdown

claude Bot commented May 15, 2026

Claude finished @mootz12's task in 2m 23s —— View job


Claude PR Review

Re-reviewed against the latest push (fd63e08). Most of last round's feedback was addressed — the renaming to --auto-sign, the audit-trail under skip-approval, the break in the signer loop, and the richer CreateContract rendering are all in. Two findings from the previous review remain.

  • Diff against origin/main and identify what changed since previous review
  • Re-check signer/mod.rs (validation/signing flow + --auto-sign handling)
  • Re-check signer/validation.rs (classifier)
  • Re-check log/auth.rs (auth entry rendering)
  • Re-check config/sign_with.rs (--auto-sign flag)
  • Verify whether earlier review findings were addressed
  • Post updated findings

Resolved since last review

Prior finding Resolution
1. --force signed non-strict entries with no audit trail handle_non_strict_authorization (signer/mod.rs:195) now logs the entry via Print when skip_approval is true.
3. Signer lookup kept iterating after match break; added at signer/mod.rs:147.
4. CreateContract prompt was empty format_create_contract now renders preimage (From/Salt or Asset), wasm hash, and constructor args (log/auth.rs:98-137).
5. --force overloaded name Renamed to --auto-sign; help text now scopes the behavior (config/sign_with.rs:73-75).
7. Print::new(false) magic One-line justification comment added at signer/mod.rs:210.

Still outstanding (blocking)

todo!() panics on RPC-controlled inputscmd/soroban-cli/src/signer/mod.rs:121-123

ScAddress::MuxedAccount(_) => todo!("muxed accounts are not supported"),
ScAddress::ClaimableBalance(_) => todo!("claimable balance not supported"),
ScAddress::LiquidityPool(_) => todo!("liquidity pool not supported"),

I called this out last round and the comment was acknowledged but deferred to #2534. With this PR's whole premise being "the RPC might return adversarial auth entries", I think it's worth promoting this from "follow-up" to "blocking" — the rest of the PR now guards against unexpected RPC output (strict-match check, source-account-as-credential guard, force-disabled non-interactive failure), but a malicious or buggy simulation response that includes one of these three address kinds still panics the CLI with a Rust backtrace instead of erroring out. The fix is mechanical: each arm becomes one line returning an Error::ContractAddressAreNotSupported-style variant. Leaving todo!() in the same function that the PR exists to harden is a regression risk that doesn't match the threat model. Fix this →


Worth fixing before merge

--auto-sign --quiet silences the new audit trailcmd/soroban-cli/src/signer/mod.rs:200-203

handle_non_strict_authorization calls print.warnln(...) + print.println(format_auth_entry(auth)), and Print (print.rs:21-32) suppresses both under --quiet. So stellar contract invoke … --auto-sign --quiet signs non-strict entries with no record of what was approved — the user did opt into both flags, but the combination quietly recreates the pre-PR footgun that this branch exists to close.

Two options, either is fine:

  • Use a hard-coded Print::new(false) inside handle_non_strict_authorization (mirroring how confirm_non_strict_authorization deliberately ignores quiet).
  • Document the combination explicitly in --auto-sign's help text so the user knows audit output is suppressed when --quiet is also passed.

I'd lean toward the first — the help text already says --auto-sign skips approval, not audit logging, so a user combining it with --quiet likely doesn't expect the warning to disappear. Fix this →


Nits / lower priority

Cross-variant mismatch still surfaces as NonStrictcmd/soroban-cli/src/signer/validation.rs:50

Carried over from prior round. You've already defended the choice ("valid, just not strict") and I think that's defensible, but the user-facing prompt text doesn't differentiate "same op, different args" from "structurally wrong op kind". Not a merge blocker — flagging here for visibility in case the warning text is revisited.

extend.rs / restore.rs network-passphrase additions are still mixed incommands/contract/extend.rs:201, restore.rs:170

Unchanged from last round. Good change on its own, just not part of the auth-validation theme. If the auth changes ever get reverted, you don't want to lose the passphrase checks with them.


Things I liked in this revision

  • handle_non_strict_authorization separation is clean and the doc comment makes the audit-vs-prompt split obvious.
  • Renaming to --auto-sign + scoped help text addresses the prior overloaded-flag concern without changing the surface area.
  • format_create_contract renders enough detail (preimage source/salt, wasm hash, constructor args) that an approval prompt for a create-contract auth is now actually informative.
  • The signer-loop break is a tiny change but meaningful for Ledger/SecureStore signers.
    · Branch: validate-auth-entries

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog (Not Ready)

Development

Successfully merging this pull request may close these issues.

4 participants