Skip to content

feat: add non-custodial integration test and Snap docs (#135)#155

Open
salindne wants to merge 1 commit intofeature/111-d-registrationfrom
feature/111-e-integration-test
Open

feat: add non-custodial integration test and Snap docs (#135)#155
salindne wants to merge 1 commit intofeature/111-d-registrationfrom
feature/111-e-integration-test

Conversation

@salindne
Copy link
Contributor

@salindne salindne commented Mar 9, 2026

Summary

Sub-issue E (#135) of the non-custodial signing feature (#111).

  • Integration test (scripts/testing/test-prepare-execute.go):
    • Happy path: register external user → mint → prepare → sign locally → execute → verify
    • Expired transfer: prepare → wait past TTL → execute → 410
    • Replay prevention: prepare → execute → execute again → 404
    • Wrong fingerprint: prepare → execute with wrong key → 403
    • Custodial rejection: custodial user → prepare → 400
    • Uses local Canton keypair to simulate MetaMask Snap signing
  • Documentation (docs/non-custodial-snap-testing.md):
    • Cryptographic requirements (EIP-191, DER signing, fingerprints)
    • Complete API reference for all 4 endpoints
    • Error reference tables
    • Snap RPC methods to implement
    • Full JavaScript dapp integration example
    • curl examples for manual testing
  • Config: Update e2e-local config with test user addresses

Test plan

  • go build ./... — compiles
  • Bootstrap local: ./scripts/testing/bootstrap-local.sh --clean
  • Run integration test: go run scripts/testing/test-prepare-execute.go -config config.docker.yaml -test-config config.e2e-test.yaml -local -skip-docker
  • Existing interop still works: go run scripts/testing/interop-demo.go

Stacked PR 4/4 for #111 — depends on #154

Integration test script (test-prepare-execute.go) covering:
- Happy path: register external user, mint, prepare, sign, execute
- Expired transfer (past TTL → 410)
- Replay prevention (double execute → 404)
- Wrong fingerprint (→ 403)
- Custodial rejection (→ 400)

Add docs/non-custodial-snap-testing.md with complete Snap integration
guide: cryptographic requirements, API reference, error tables, RPC
methods, JavaScript dapp example, and curl testing commands.

Update e2e-local config with test user addresses.
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly advances the non-custodial signing feature by providing robust integration testing and thorough documentation. It ensures the two-step transfer flow for external signers functions correctly under various conditions and offers a complete guide for developers to integrate MetaMask Snaps, thereby enhancing the system's security and external wallet compatibility.

Highlights

  • Non-Custodial Integration Test: Added a comprehensive Go integration test (scripts/testing/test-prepare-execute.go) for the non-custodial prepare/execute transfer API, covering happy path, expired transfers, replay prevention, wrong fingerprint, and custodial rejection scenarios.
  • MetaMask Snap Documentation: Introduced detailed documentation (docs/non-custodial-snap-testing.md) for integrating with MetaMask Snaps for non-custodial transfers, including cryptographic requirements (EIP-191, DER signing, Canton Key Fingerprint), API reference, error codes, Snap RPC methods, Dapp integration flow, and curl examples.
  • Configuration Update: Updated config.e2e-local.yaml to include new domain_id, relayer_party, and instrument_admin values, likely reflecting changes in the local bootstrap environment for test user addresses.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • config.e2e-local.yaml
    • Updated Canton configuration parameters for domain ID, relayer party, and instrument admin.
  • docs/non-custodial-snap-testing.md
    • Added comprehensive documentation for testing non-custodial transfers with MetaMask Snap, detailing cryptographic requirements, API reference, error handling, and Dapp integration.
  • scripts/testing/test-prepare-execute.go
    • Added a new Go integration test script to validate the non-custodial prepare/execute transfer API, covering various scenarios like happy path, expired transfers, and replay prevention.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces an integration test for the non-custodial transfer flow and adds comprehensive documentation for MetaMask Snap integration. However, the changes reveal a critical security vulnerability in the registration process: the registration endpoints lack replay protection and message content validation. This could allow an attacker to hijack a user's account by associating a malicious Canton key with their EVM address using any valid signature from that user. This issue must be remediated in the API server implementation and the documentation updated accordingly. Additionally, I've provided a couple of suggestions to improve clarity in both the new documentation and the test script.


The server rejects messages where the timestamp differs from server time by more than **5 minutes**.

Registration endpoints (`/register/*`) do **not** enforce this format.
Copy link
Contributor

Choose a reason for hiding this comment

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

security-critical critical

The documentation explicitly states that "Registration endpoints (/register/*) do not enforce this format," referring to the timed message format {action}:{unix_timestamp_seconds} used for replay protection in other endpoints. This indicates a critical security flaw in the registration process.

Without enforcing a specific message format and a timestamp (replay protection), the registration endpoints are vulnerable to authentication bypass. An attacker who obtains any valid EIP-191 signature from a user (e.g., a signature for a login message on a different service) can use that signature and message to register an arbitrary canton_public_key to the user's EVM address. Once a Canton key is registered, the attacker can use it to sign and execute non-custodial transfers on behalf of the user, leading to a total loss of funds.

Recommendation:

  1. Modify the server implementation (specifically in pkg/user/service/http.go) to enforce the timed message format for all registration endpoints.
  2. Validate that the message content is specific to the registration action and includes the fingerprint of the Canton public key being registered (e.g., register-external:{fingerprint}:{unix_timestamp_seconds}).
  3. Update this documentation to reflect the mandatory security requirements.

}
```

Store the `fingerprint` — it's needed for execute requests (as `signed_by`). The `public_key_fingerprint` from step 1a is the Canton key fingerprint used for `signed_by`.
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The explanation here is a bit confusing. It first mentions storing the fingerprint from the /register response for use in signed_by, but then correctly clarifies that it's the public_key_fingerprint from the /register/prepare-topology response that should be used. This could lead to confusion for developers implementing a client.

To improve clarity, I suggest rephrasing this to distinguish between the two fingerprint fields more clearly. For example:

"The public_key_fingerprint returned from the prepare-topology step (1a) must be used as the signed_by field in the execute request (step 2b). Note that the register response (1b) also contains a fingerprint field, but this is an internal user identifier and should not be used for signing."

Comment on lines +450 to +451
_ = hash // SignHashDER expects the pre-hashed 32 bytes
derSig, err := kp.SignHashDER(hash[:])
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The line _ = hash is a no-op since hash is used on the next line. It appears to be a leftover from development and can be removed to improve code clarity.

Suggested change
_ = hash // SignHashDER expects the pre-hashed 32 bytes
derSig, err := kp.SignHashDER(hash[:])
derSig, err := kp.SignHashDER(hash[:])

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.

1 participant