-
Notifications
You must be signed in to change notification settings - Fork 30
shadow-signers: support for EVM #1474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: wallets-v1
Are you sure you want to change the base?
Changes from all commits
2033ee9
8a5417a
a69a978
8089157
c675932
b642c2c
41e35a2
eb5c28e
1be9a9f
cfde33f
d59062a
c1ea37e
6b8b3f3
3e6d5c2
133dc92
5bd3ce4
34f6f16
e0397c1
9276bcf
f890560
b04e71d
43c64d8
77faeca
6891aa5
ee8195b
199d2ab
1b0eba3
a1afbb5
05fbb32
75a3e8f
c23d2b6
3d92392
dbaf09d
1351429
050c6be
6cd46ee
c66cb23
14914bc
b588a95
e5516f0
8b024c7
4da2a7f
9c31e78
69a147a
c34c4e2
8f2aca8
13f4369
55e291f
6aace1b
a550bb8
16f5f1b
bc49080
28682a3
af82106
3b85db9
adaee17
ea04677
89be46d
740f37e
33e7177
7a345c5
1d7a55f
0df29c4
4d2b684
87c6e61
450a323
e9550dc
6c4f679
ee6c518
ccacf80
c42de5f
ad35f5b
0b19137
ff117e4
bf218e0
30827d6
ad5c5d5
099b456
12a693c
5e65a97
6e8fe56
bbdc645
b45a082
e2669a0
42c359b
4cf0b97
2111f54
15ea8ed
6b6d371
cf3a19d
2af0201
2ce1972
35595aa
7368af6
dabde79
6fbf44b
a23e334
1757b14
f6dd7d3
7e6c42e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "@crossmint/wallets-sdk": minor | ||
| "@crossmint/client-sdk-react-native-ui": patch | ||
| "@crossmint/common-sdk-base": patch | ||
| --- | ||
|
|
||
| Support EVM Shadow Signers |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import type { Signer, P256KeypairInternalSignerConfig } from "./types"; | ||
| import { keccak256, sha256, toHex } from "viem"; | ||
|
|
||
| export class P256KeypairSigner implements Signer { | ||
| type = "p256-keypair" as const; | ||
| private _address: string; | ||
| private _locator: string; | ||
| private onSignTransaction: (publicKeyBase64: string, data: Uint8Array) => Promise<Uint8Array>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need the publicKey as an input in this callback! Isn't it kept as an attribute of the class? Isn't it the value in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes but onSignTransaction is defined as a config passed in the constructor, when defined it doesn't have access to this |
||
| private readonly STUB_ORIGIN = "https://crossmint.com"; | ||
|
|
||
| constructor(config: P256KeypairInternalSignerConfig) { | ||
| this._address = config.address; | ||
| this._locator = config.locator; | ||
| this.onSignTransaction = config.onSignTransaction; | ||
| } | ||
|
|
||
| address() { | ||
| return this._address; | ||
| } | ||
|
|
||
| locator() { | ||
| return this._locator; | ||
| } | ||
|
|
||
| async signMessage(message: string): Promise<{ signature: string }> { | ||
| return await this.createWebAuthnSignature(message); | ||
| } | ||
|
|
||
| async signTransaction(transaction: string): Promise<{ signature: string }> { | ||
| return await this.createWebAuthnSignature(transaction); | ||
| } | ||
|
|
||
| private async createWebAuthnSignature(challenge: string): Promise<{ signature: string }> { | ||
| const STUB_ORIGIN = "https://crossmint.com"; | ||
|
|
||
| // 1. Create clientDataJSON with base64url encoded challenge | ||
| const challengeHex = challenge.replace("0x", ""); | ||
| const challengeBase64 = Buffer.from(challengeHex, "hex").toString("base64"); | ||
| const challengeBase64url = challengeBase64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); | ||
|
|
||
| const clientDataJSON = JSON.stringify({ | ||
| type: "webauthn.get", | ||
| challenge: challengeBase64url, | ||
| origin: STUB_ORIGIN, | ||
| crossOrigin: false, | ||
| }); | ||
|
|
||
| // 2. Create authenticatorData | ||
| // IMPORTANT: Use keccak256 for rpIdHash to match backend (line 182 in backend) | ||
| const originBytes = new TextEncoder().encode(STUB_ORIGIN); | ||
| const rpIdHash = keccak256(toHex(originBytes)); | ||
|
|
||
| // flags: 0x05 = User Present (0x01) + User Verified (0x04) | ||
| const flags = "05"; | ||
|
|
||
| // signCount: 4 bytes, all zeros | ||
| const signCount = "00000000"; | ||
|
|
||
| const authenticatorData = (rpIdHash + flags + signCount) as `0x${string}`; | ||
|
|
||
| // 3. Create signature message: authenticatorData + sha256(clientDataJSON) | ||
| // This matches what the backend expects and what WebAuthn spec requires | ||
| const clientDataJSONBytes = new TextEncoder().encode(clientDataJSON); | ||
| const clientDataHash = sha256(toHex(clientDataJSONBytes)); | ||
|
|
||
| const signatureMessage = (authenticatorData + clientDataHash.slice(2)) as `0x${string}`; | ||
|
|
||
| // 4. Sign with P256 private key | ||
| // Web Crypto API will internally do: sign(sha256(signatureMessage)) | ||
| const signatureMessageBytes = new Uint8Array(Buffer.from(signatureMessage.slice(2), "hex")); | ||
| const signatureBytes = await this.onSignTransaction(this.address(), signatureMessageBytes); | ||
|
|
||
| // 5. Return r + s as hex string | ||
| const rHex = Buffer.from(signatureBytes.slice(0, 32)).toString("hex").padStart(64, "0"); | ||
| const sHex = Buffer.from(signatureBytes.slice(32, 64)).toString("hex").padStart(64, "0"); | ||
|
|
||
| return { signature: "0x" + rHex + sHex }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { ShadowSigner } from "./shadow-signer"; | ||
| import type { EVMSmartWalletChain } from "@/chains/chains"; | ||
| import type { P256KeypairInternalSignerConfig } from "../types"; | ||
| import type { ShadowSignerData } from "./utils"; | ||
| import { P256KeypairSigner } from "../p256-keypair"; | ||
|
|
||
| export class EVMShadowSigner extends ShadowSigner< | ||
| EVMSmartWalletChain, | ||
| P256KeypairSigner, | ||
| P256KeypairInternalSignerConfig | ||
| > { | ||
| protected getWrappedSignerClass() { | ||
| return P256KeypairSigner; | ||
| } | ||
|
|
||
| getShadowSignerConfig(shadowData: ShadowSignerData): P256KeypairInternalSignerConfig { | ||
| return { | ||
| type: "p256-keypair", | ||
| address: shadowData.publicKeyBase64, | ||
| locator: `p256-keypair:${shadowData.publicKeyBase64}`, | ||
| onSignTransaction: async (pubKey: string, data: Uint8Array) => { | ||
| return await this.storage.sign(pubKey, data); | ||
| }, | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we know this by checking if this.shadowSigner is not nullish?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shadowSignerEnabled comes from the wallets config from the args. It could happen that a wallet was created with a shadow signer, but now the client is disabling its use through the config