feat(wallet): NEP-641 w_resolve_auth off-chain authorization resolution#319
feat(wallet): NEP-641 w_resolve_auth off-chain authorization resolution#319frol wants to merge 11 commits into
Conversation
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
8d92bd4 to
35de6a1
Compare
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesNEP-641 wallet authorization
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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
0fe37c7 to
f199b81
Compare
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
…urrent_contract_code fix" This reverts commit f199b81.
29a4815 to
ab2b724
Compare
ab2b724 to
e139a19
Compare
There was a problem hiding this comment.
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 withAuthMessage/SignedAuthMessageandAuthorizationResolutiontypes (SignerId + Code bindings). - Refactors
SignatureSchemato verify a 32-byte digest viaverify_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.
e99ff9e to
5b430dd
Compare
…_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
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
contracts/defuse/core/src/public_key.rscontracts/wallet/src/contract.rscrates/wallet/sdk/src/client.rstests/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
712bce6 to
2ab3189
Compare
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).
2ab3189 to
597e4f9
Compare
Implement the NEP-641
w_resolve_auth(purpose, recipient, authorization)view method on all wallet contract variants:AuthMessageenvelope (domainNEAR_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: conventionalSignerIdbinding, plus aCodebinding to the account's NEP-616StateInit: the contract reconstructs the initial state from the envelope config + its stored public key, derives the deterministic account id and verifies it equalsenv::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, alwaysSignatureSchema::verify_hash()refactor: schemas verify a 32-byte domain-separated digest;verify(RequestMessage)is now a provided method (existing mainnet test vectors unchanged)current_contract_code)WalletSigner::sign_auth_msg()(incl.WalletEd25519SignerandMockWalletWebauthnSigner),Wallet::sign_auth()+auth_message()/auth_message_code_binding()builders,w_resolve_authclient bindingsClaude-Session: https://claude.ai/code/session_011ETHQdJcc4Rz4VTzhMaC3C
Summary by CodeRabbit