Skip to content

feat(wallet): NEP-641 w_resolve_auth off-chain authorization resolution#319

Open
frol wants to merge 11 commits into
near:mainfrom
frol-ai:feat/wallet-nep641-resolve-auth
Open

feat(wallet): NEP-641 w_resolve_auth off-chain authorization resolution#319
frol wants to merge 11 commits into
near:mainfrom
frol-ai:feat/wallet-nep641-resolve-auth

Conversation

@frol

@frol frol commented Jul 16, 2026

Copy link
Copy Markdown

Implement the NEP-641 w_resolve_auth(purpose, recipient, authorization) view method on all wallet contract variants:

  • New AuthMessage envelope (domain NEAR_WALLET_CONTRACT_AUTH/V1, SHA3-256, no nonce — replay protection is layered via dApp payload freshness + recipient/purpose/chain_id/signer bindings + validity window)
  • AuthSignerBinding: conventional SignerId binding, plus a Code binding to the account's NEP-616 StateInit: the contract reconstructs the initial state from the envelope config + its stored public key, derives the deterministic account id and verifies it equals env::current_account_id(). This pins exactly one account while staying constructible client-side before a WebAuthn ceremony reveals which passkey answers, and survives post-creation config mutations — single ceremony on re-login, always
  • SignatureSchema::verify_hash() refactor: schemas verify a 32-byte domain-separated digest; verify(RequestMessage) is now a provided method (existing mainnet test vectors unchanged)
  • near-sdk 5.28.3 -> 5.29.0 (to pull the fix for current_contract_code)
  • SDK: WalletSigner::sign_auth_msg() (incl. WalletEd25519Signer and MockWalletWebauthnSigner), Wallet::sign_auth() + auth_message()/auth_message_code_binding() builders, w_resolve_auth client bindings
  • Tests: unit vectors (both bindings, both WebAuthn curves via the mock webauthn signer), sandbox suite (initial-state binding survives mutations, mutated-config envelopes rejected, subwallet isolation, CodeHash vs AccountId code identities, signature-disabled, no-sign), and shared JSON fixtures (tests/fixtures/nep641-auth.json) consumed by the near-connect-passkey executor to lock the wire format cross-repo

Claude-Session: https://claude.ai/code/session_011ETHQdJcc4Rz4VTzhMaC3C

Summary by CodeRabbit

  • New Features
    • Added NEP-641 off-chain authorization messages with hash-based signing/verification, deterministic signer bindings, and replay-protection fields.
    • Wallet contracts now resolve authorization requests into Resolved or Invalid outcomes via a new view method.
    • Extended the wallet SDK with client support for authorization resolution.
  • Security
    • Signature verification now uses canonical, domain-separated authorization hashes.
    • Updated WebAuthn behavior notes: assertions don’t strictly require the UV flag (improving compatibility with common passkey sign-in flows).

Implement the NEP-641 `w_resolve_auth(purpose, recipient, authorization)`
view method on all wallet contract variants:

- New `AuthMessage` envelope (domain `NEAR_WALLET_CONTRACT_AUTH/V1`,
  SHA3-256, no nonce — replay protection is layered via dApp payload
  freshness + recipient/purpose/chain_id/signer bindings + validity window)
- `AuthSignerBinding`: conventional `SignerId` binding, plus a `Code`
  binding to the account's NEP-616 `StateInit`. The envelope commits only
  to the *initial* state config: the contract reconstructs
  `StateInit { code: env::current_global_contract_id(), data: State {
  ..config, public_key } }` — taking the code identity from the code it
  currently runs under and the public key from its own storage — and
  verifies the derived deterministic account id equals
  `env::current_account_id()`. A match proves the envelope was intended
  for this exact account. The binding is constructible client-side before
  a WebAuthn ceremony reveals which passkey (or even which curve / wallet
  variant) answers, and survives post-creation config mutations — single
  ceremony on re-login, always
- `SignatureSchema::verify_hash()` refactor: schemas verify a 32-byte
  domain-separated digest; `verify(RequestMessage)` is now a provided
  method (existing mainnet test vectors unchanged)
- near-sdk 5.28.3 -> 5.29.0: required for the StateInit reconstruction —
  5.29.0 fixes `current_contract_code()` returning the current account's
  own id (instead of the global contract's account id) for
  GlobalByAccount deployments
- SDK: `WalletSigner::sign_auth_msg()` (incl. `WalletEd25519Signer` and
  `MockWalletWebauthnSigner`), `Wallet::sign_auth()` +
  `auth_message()`/`auth_message_code_binding()` builders,
  `w_resolve_auth` client bindings
- Tests: unit vectors (both bindings, both WebAuthn curves via the mock
  webauthn signer), sandbox suite (initial-state binding survives
  mutations, mutated-config envelopes rejected, subwallet isolation,
  CodeHash and AccountId code identities, same-key sibling accounts under
  different code accept the same blob by design, signature-disabled,
  no-sign), and shared JSON fixtures (tests/fixtures/nep641-auth.json)
  consumed by the near-connect-passkey executor to lock the wire format
  cross-repo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ETHQdJcc4Rz4VTzhMaC3C
@frol-ai
frol-ai force-pushed the feat/wallet-nep641-resolve-auth branch from 8d92bd4 to 35de6a1 Compare July 16, 2026 23:53
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds NEP-641 authorization message types, canonical hashing, signer binding validation, wallet resolution APIs, hash-based signature verification, SDK bindings, and shared test fixtures. WebAuthn documentation clarifies that sign-in assertions may omit the UV flag.

Changes

NEP-641 wallet authorization

Layer / File(s) Summary
Authorization contracts and canonical hashing
contracts/wallet/src/auth.rs, contracts/wallet/src/lib.rs, contracts/wallet/Cargo.toml, contracts/wallet/tests/*
Defines authorization messages, signer bindings, resolution states, errors, signed payloads, canonical hashes, serialization behavior, feature wiring, and shared hash fixtures.
Hash-based signature adapters and validation
contracts/wallet/src/schema.rs, contracts/wallet/signatures/*
Adds verify_hash, updates Ed25519, no-sign, and WebAuthn implementations, and adds verification coverage and WebAuthn compatibility documentation.
On-chain authorization resolution
contracts/wallet/src/contract.rs, crates/wallet/sdk/src/client.rs
Adds wallet authorization resolution with binding, validity, deterministic-account, and signature checks, generated contract wiring, and SDK request bindings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant WalletImpl
  participant StateInit
  participant SignatureSchema
  Caller->>WalletImpl: w_resolve_auth(purpose, recipient, authorization)
  WalletImpl->>WalletImpl: Deserialize and validate authorization
  WalletImpl->>StateInit: Reconstruct deterministic signer account
  StateInit-->>WalletImpl: Derived account identity
  WalletImpl->>SignatureSchema: verify_hash(public_key, hash, proof)
  SignatureSchema-->>WalletImpl: Verification result
  WalletImpl-->>Caller: AuthorizationResolution
Loading

Possibly related PRs

  • near/intents#270: Both update wallet signature verification around canonical request hashes and SignatureSchema.

Suggested reviewers: mitinarseny

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding NEP-641 w_resolve_auth off-chain authorization resolution to wallet contracts.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ontract_code fix

near-sdk 5.29.0's generated wasm imports three gas-key host functions
that are not yet live on mainnet:

  - promise_batch_action_add_gas_key_with_full_access
  - promise_batch_action_add_gas_key_with_function_call
  - promise_batch_action_transfer_to_gas_key

A 5.29.0-built wallet contract therefore fails to link at call time:
`LinkError { msg: "unknown or invalid import" }` (reproduced on
near-sandbox 2.12 / protocol 84; the 5.28.x-built wasm links and runs).

Stay on near-sdk 5.28.x until that protocol change ships, and vendor the
`current_contract_code` fix that 5.29.0 would otherwise provide:

- `contracts/wallet/src/auth.rs`: `current_global_contract_id()` — a local
  copy of near/near-sdk-rs#1601. near-sdk 5.28.x reads the current account
  id (not the host-filled register) for the `GlobalByAccount` case, so it
  returns the wallet's own account id instead of the global contract's.
  The vendored version imports only `current_contract_code` (live on
  mainnet) via `near-sys` and reads the register directly.
- `contracts/wallet/src/contract.rs`: `w_resolve_auth` calls the vendored
  function instead of `env::current_global_contract_id()`.
- lockfile pinned to origin's known-good near-* set (near-global-contracts
  0.2.2, near-sys 0.2.12, near-sdk-env 0.1.4); newer versions pull a second
  near-primitives-core (0.37) that breaks near-sdk 5.28.x's unit-testing mock.

REVERT this commit — drop the vendored function, switch back to
`env::current_global_contract_id()`, and move to near-sdk 5.29.x — once the
gas-key host functions are live on mainnet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ETHQdJcc4Rz4VTzhMaC3C
@frol-ai
frol-ai force-pushed the feat/wallet-nep641-resolve-auth branch from 0fe37c7 to f199b81 Compare July 17, 2026 09:23
frol-ai and others added 4 commits July 18, 2026 18:51
Both deployed WebAuthn wallet variants (p256, ed25519) used
IgnoreUserVerification, so w_execute_signed / w_resolve_auth accepted
assertions with only the UP (user-present) flag — a bare touch, no
biometric/PIN. The passkey is the sole key over funds; the client
requests userVerification:"required" and re-checks the UV flag, but a
proof submitted directly to the relayer bypasses that. The contract must
not accept a user-presence-only assertion.

Switch both variants to RequireUserVerification: check_flags now requires
the UV bit. Trade-off: PIN-less FIDO U2F/CTAP1 keys can no longer sign;
platform passkeys (Apple/Google/Windows) always perform UV.

Account ids are unchanged (global-by-account code identity is stable
across wasm upgrades), so this needs no migration — redeploying the
factory wasm applies it to all existing instances.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
The `Code` binding committed only to config, so a single signed AuthMessage
was accepted by any account keyed by the passkey under any code identity —
the sibling-account ambiguity. It could not commit the code id because the
executor builds the envelope before the discovery ceremony reveals the
curve (and thus which per-curve factory answers).

Fix: commit BOTH canonical factory ids (curve-independent constants known
before the ceremony) as `allowed_factory_ids`, and have each per-curve
`resolve_auth` require that its own running code is in the set. Because a
signature only verifies under its own curve's code, the accepting set
collapses to exactly one account per curve — the code-id binding restored
without unifying the contracts or a second ceremony.

`allowed_factory_ids` MUST list at most one factory per curve, else the
same signed message resolves against two accounts (cross-account replay);
documented on the field and pinned by test_resolve_auth_factory_allow_list.
Code binding now requires a by-account (canonical factory) deployment;
code-hash deployments must use the SignerId binding.

Wire format change: new field in AuthSignerBinding::Code; hash vectors and
the shared nep641-auth.json fixture recomputed (Rust and the executor
encoder agree). SDK `auth_message_code_binding` takes the allow-list.

Verified: wallet lib + fixture tests pass, contract wasm builds, SDK and
integration tests compile. The wallet auth integration tests need Docker
(near-kit sandbox), unavailable here, so were not executed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
Make AuthSignerBinding deserialization strict. This lets a future
wallet-contract variant of the SAME signature curve be added to a Code
binding's allowed_factory_ids without re-introducing cross-account replay,
provided its Code binding carries a different (added, required) field: a
message shaped for the new variant fails to parse on the old contract
(unknown field), and a message for the old variant fails on the new one
(missing required field), so no single signed message resolves under both.

Without deny_unknown_fields serde silently ignores the extra field and the
old contract would accept the new variant's message — the replay the
per-curve invariant guards against.

Verified serde actually enforces it here (it has historically ignored
deny_unknown_fields on internally-tagged enums): binding_denies_unknown_field
rejects an extra field on both variants. Serialization/borsh/hashes are
unchanged, so the wire format and the executor are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG
@frol-ai
frol-ai force-pushed the feat/wallet-nep641-resolve-auth branch 2 times, most recently from 29a4815 to ab2b724 Compare July 21, 2026 09:54
@frol-ai
frol-ai force-pushed the feat/wallet-nep641-resolve-auth branch from ab2b724 to e139a19 Compare July 21, 2026 10:17
@frol
frol marked this pull request as ready for review July 21, 2026 10:28
Copilot AI review requested due to automatic review settings July 21, 2026 10:28

Copilot AI left a comment

Copy link
Copy Markdown

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 adds NEP-641 off-chain authorization resolution to the wallet contract family and corresponding SDK/client support, including a refactor of signature verification to operate over a domain-separated 32-byte digest.

Changes:

  • Introduces w_resolve_auth(purpose, recipient, authorization) view method with AuthMessage/SignedAuthMessage and AuthorizationResolution types (SignerId + Code bindings).
  • Refactors SignatureSchema to verify a 32-byte digest via verify_hash, and updates signature schema implementations (ed25519, webauthn, no-sign).
  • Updates wallet SDK with AuthMessage builders/signing helpers, adds fixtures/tests, and bumps near-sdk to 5.29.0.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/src/tests/wallet/mod.rs Registers the new auth test module.
tests/src/tests/wallet/auth.rs Adds end-to-end sandbox coverage for NEP-641 resolution flows and bindings.
crates/wallet/sdk/src/signer.rs Extends WalletSigner with sign_auth_msg for NEP-641 messages.
crates/wallet/sdk/src/lib.rs Adds AuthMessage builders and sign_auth, and tracks extensions in the SDK wallet handle.
crates/wallet/sdk/src/client.rs Adds w_resolve_auth client binding and WResolveAuthArgs.
crates/wallet/sdk/Cargo.toml Adds serde_json to the near-kit feature surface for auth arg construction.
contracts/wallet/tests/nep641_fixtures.rs Adds shared fixture-based test vectors for AuthMessage hashing + JSON round-trip.
contracts/wallet/tests/fixtures/nep641-auth.json Adds shared cross-repo NEP-641 AuthMessage vectors and expected hashes.
contracts/wallet/src/schema.rs Refactors signature verification API to verify_hash and provides verify() default.
contracts/wallet/src/lib.rs Exposes the new auth module/types from the wallet crate.
contracts/wallet/src/contract.rs Implements w_resolve_auth and Code/SignerId binding validation on-chain.
contracts/wallet/src/auth.rs Adds NEP-641 AuthMessage envelope, binding types, resolution result types, and error mapping.
contracts/wallet/signatures/webauthn/src/p256.rs Adds NEP-641 signing/verification test for WebAuthn P-256.
contracts/wallet/signatures/webauthn/src/mock.rs Adds sign_auth_msg support to the mock WebAuthn signer.
contracts/wallet/signatures/webauthn/src/lib.rs Updates WebAuthn schema to verify_hash and adds an AuthMessage test helper.
contracts/wallet/signatures/webauthn/src/ed25519.rs Adds NEP-641 signing/verification test for WebAuthn Ed25519.
contracts/wallet/signatures/webauthn/p256/src/lib.rs Requires UV on-chain for the P-256 passkey wallet variant.
contracts/wallet/signatures/webauthn/ed25519/src/lib.rs Requires UV on-chain for the Ed25519 passkey wallet variant.
contracts/wallet/signatures/no-sign/src/lib.rs Updates no-sign schema to verify_hash.
contracts/wallet/signatures/ed25519/src/signer.rs Adds sign_auth_msg to the Ed25519 signer implementation.
contracts/wallet/signatures/ed25519/src/lib.rs Updates Ed25519 schema to verify_hash.
contracts/wallet/Cargo.toml Enables deps/features needed for auth resolution (serde_json, deterministic account IDs).
Cargo.toml Bumps near-sdk to 5.29.0.
Cargo.lock Updates resolved dependency graph after the SDK bump (includes aws-lc-rs changes).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread contracts/wallet/src/contract.rs Outdated
Comment thread tests/src/tests/wallet/auth.rs Outdated
Comment thread crates/wallet/sdk/src/client.rs Outdated
@frol-ai
frol-ai force-pushed the feat/wallet-nep641-resolve-auth branch from e99ff9e to 5b430dd Compare July 21, 2026 14:57
…_SIGNATURE

test_resolve_auth_no_sign expected INVALID_SIGNATURE, but the no-sign wallet
sets signature_enabled=false and holds self as its only extension, so
is_signature_allowed() (signature_enabled || extensions.is_empty()) is false.
w_resolve_auth short-circuits at SignatureDisabled → INVALID_INPUT before ever
reaching signature verification — the same result as a SetSignatureMode(false)
wallet (test_resolve_auth_signature_disabled).

That short-circuit is required for safety: a signature-disabled wallet must be
rejected even if the signature would verify. So the contract is correct and
the test expectation was stale. Align it to INVALID_INPUT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMqbwtkoA9PwXo9iVsEDbG

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@contracts/defuse/core/src/public_key.rs`:
- Line 210: Update the NearPublicKey conversion match handling
NearPublicKey::MlDsa65 and NearPublicKey::MlDsa65Hash so it no longer calls
todo!(); either return the established explicit unsupported-key error through
the fallible conversion path, or map both variants to their supported
destination representation if ML-DSA keys are intended to be accepted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d03675ce-4cc6-43fb-babf-ba937f0401d9

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3c7dc and 1594cf8.

📒 Files selected for processing (4)
  • contracts/defuse/core/src/public_key.rs
  • contracts/wallet/src/contract.rs
  • crates/wallet/sdk/src/client.rs
  • tests/src/tests/wallet/auth.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/wallet/sdk/src/client.rs
  • tests/src/tests/wallet/auth.rs
  • contracts/wallet/src/contract.rs

Comment thread contracts/defuse/core/src/public_key.rs Outdated
@frol-ai
frol-ai force-pushed the feat/wallet-nep641-resolve-auth branch 2 times, most recently from 712bce6 to 2ab3189 Compare July 23, 2026 10:35
origin/main refactored the wallet SDK (renamed WalletSigner::sign_request_msg
-> sign_wallet_msg, moved the relayer into the SDK crate, changed the Wallet
struct, added MPC support, switched to env::chain_id()/p256_verify()) and
removed the wallet integration tests (tests/src/tests/wallet/*).

Conflict resolution:
- Contract-side NEP-641 (auth.rs, contract.rs) auto-merged and is preserved in
  full: w_resolve_auth, AuthSignerBinding with allowed_factory_ids +
  deny_unknown_fields, the resolve_auth factory allow-list check, and the p256/
  ed25519 RequireUserVerification variants. Fixed one auto-merge artifact:
  resolve_auth now uses env::chain_id() (utils::chain_id() was removed on main).
- Took main's refactored SDK (signer.rs, lib.rs, Cargo.toml) and dropped the
  SDK-side NEP-641 *convenience* helpers that collided with the refactor
  (sign_auth_msg on the signers, auth_message/auth_message_code_binding/sign_auth
  on Wallet, WResolveAuthArgs::from_signed). These were only consumed by the
  now-deleted wallet integration tests; the deployed contract feature is
  unaffected. They should be re-added on the new signer/MPC/relayer API as a
  follow-up.
- Accepted main's deletion of tests/src/tests/wallet/*. Unrelated conflicts
  (root Cargo.toml versions, defuse core public_key.rs, Cargo.lock) took main.

Verified: full `cargo check --workspace` passes; defuse-wallet lib + NEP-641
fixture tests pass; clippy -D warnings clean on the wallet crates; p256 wasm
builds. Wallet integration tests need Docker (not run here).
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.

3 participants