Skip to content

Validate auth entries before signing#2530

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

Validate auth entries before signing#2530
mootz12 wants to merge 11 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 on lines +103 to +117
// Before we attempt to sign, validate the auth entry is strict
match validation::classify_auth_invocation(&body.host_function, &raw_auth.root_invocation) {
validation::AuthStyle::Strict => {}
validation::AuthStyle::NonStrict => {
if !skip_approval {
confirm_non_strict_authorization(raw_auth)?;
}
}
validation::AuthStyle::Invalid => {
return Err(Error::InvalidAuthEntry {
reason: "authorization entry is not expected for the transaction".to_string(),
auth_entry_str: format_auth_entry(raw_auth),
});
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With --force, non-strict entries are signed silently — no audit trail of what got approved. confirm_non_strict_authorization is the only place that prints the entry, and it's gated behind !skip_approval. So a user running with --force (or in CI) sees nothing about which auth entries failed the strict check before being signed.

Suggest always logging the non-strict entry at warn level, and only adding the interactive prompt when not forced. That way --force users still get a record in their logs.

validation::AuthStyle::NonStrict => {
    let print = Print::new(false);
    print.warnln("Authorization entry does not match the current contract call:");
    print.println(format_auth_entry(raw_auth));
    if !skip_approval {
        confirm_non_strict_authorization_prompt(&print)?;
    } else {
        print.warnln("Signing anyway because --force was provided.");
    }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is good feedback! I like having a warning message.

Comment on lines 144 to 149
let mut signer: Option<&Signer> = None;
for s in signers {
if needle == &s.get_public_key()?.0 {
if auth_address_bytes == &s.get_public_key()?.0 {
signer = Some(s);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Two minor issues here:

  1. No break after assigning the signer — the loop keeps iterating and re-calling get_public_key()? on remaining signers.
  2. get_public_key() can hit external systems for Ledger / SecureStore signers, so iterating all of them per auth entry is more expensive than necessary.

Suggest:

let signer = signers
    .iter()
    .find(|s| s.get_public_key().ok().map(|pk| pk.0) == Some(*auth_address_bytes));

or just add a break after assignment. (You'll lose the ? propagation on get_public_key errors; if that matters, keep the explicit loop but break once matched.)

}

fn confirm_non_strict_authorization(auth: &SorobanAuthorizationEntry) -> Result<(), Error> {
let print = Print::new(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Print::new(false) hard-codes quiet=false, so the safety warning ignores the global --quiet flag. That's probably the intended behavior for a safety prompt (you want the warning to show even in quiet mode), but it's inconsistent with the rest of the CLI's print plumbing — worth a one-line comment justifying the choice so a future reader doesn't "fix" it by threading quiet through.

Comment on lines +63 to +66
SorobanAuthorizedFunction::CreateContractHostFn(_)
| SorobanAuthorizedFunction::CreateContractV2HostFn(_) => {
let _ = writeln!(result, "{prefix} CreateContract");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the prompt is shown for a non-strict CreateContract / CreateContractV2 auth entry, the user just sees the word CreateContract with no further detail (no wasm hash, no preimage, no constructor args). That's the same information they'd need to evaluate whether to approve.

Since CreateContractArgsV2 carries contract_id_preimage, executable, and constructor_args, it's worth at least formatting the wasm hash and constructor args here (mirroring how ContractFn is printed). Otherwise the approval prompt for a create-contract auth is essentially "do you want to sign this?" with no context.

Comment on lines +38 to +57
let is_strict = match (source_host_fn, &auth_invocation.function) {
(HostFunction::InvokeContract(op), SorobanAuthorizedFunction::ContractFn(args)) => {
args == op
}
(
HostFunction::CreateContract(op),
SorobanAuthorizedFunction::CreateContractHostFn(args),
) => args == op,
(
HostFunction::CreateContractV2(op),
SorobanAuthorizedFunction::CreateContractV2HostFn(args),
) => args == op,
_ => false,
};

if is_strict {
AuthStyle::Strict
} else {
AuthStyle::NonStrict
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Echoing the earlier Copilot comment now that I see the full classifier: with (HostFunction::CreateContract(_), SorobanAuthorizedFunction::ContractFn(_)) or any other cross-variant pair, this falls into the _ => false arm and returns NonStrict. The doc string says NonStrict is "the same kind of operation but doesn't match exactly" — a ContractFn auth attached to a CreateContract host fn isn't that; it's structurally wrong.

You acknowledged in the earlier thread that "it is valid, just not strict." That's defensible, but currently the prompt the user sees will say "Authorization entry does not match the current contract call" without distinguishing "same op, different args" from "completely wrong op kind." A user might reasonably approve the former and never the latter. Worth either:

  • Returning Invalid on cross-variant mismatch (clearer error), or
  • Surfacing the distinction in the warning text so users have a real signal.

Comment on lines +200 to +202
client
.verify_network_passphrase(Some(&network.network_passphrase))
.await?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This verify_network_passphrase addition (also in restore.rs) is unrelated to the rest of this PR's auth-validation theme. It looks like a good change on its own, but the PR description doesn't mention it. Consider splitting it into a separate PR (or at least calling it out in the description) so it doesn't get lost under the auth-validation review and so it can be reverted independently if needed.

Comment on lines +73 to 76
/// Skip confirmation prompts when signing
#[arg(long, help_heading = HEADING_SIGNING)]
pub force: bool,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--force is now attached to sign_with::Args, which flattens into ~50 subcommands per the diff in FULL_HELP_DOCS.md — including tx sign, tx send, etc. where there are no Soroban auth confirmation prompts. The flag is therefore a silent no-op on most of the commands that advertise it.

Two small suggestions:

  • Tighten the help text so users know when it actually does something, e.g. Skip auth-entry approval prompts (only applies to commands that invoke Soroban contracts).
  • Consider whether the flag belongs on sign_with::Args at all vs. only on the commands that actually call sign_soroban_authorizations.

Not blocking, but worth a thought before merge so the help output stays honest.

Copy link
Copy Markdown
Member

@fnando fnando May 13, 2026

Choose a reason for hiding this comment

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

I wonder if we should use a better more explicit name for this option. --force can have different meaning on different commands. Maybe --confirm-signing?

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