From 95b911808032232988a5742bc7446f6b089f92a8 Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Tue, 17 Dec 2024 14:30:32 +0000 Subject: [PATCH 1/8] Handle grantSession keys as available parameter --- src/internal/accountDelegation.ts | 28 ++++++++++++++++++++++++++- src/internal/provider.ts | 32 +++++++++++++++++++++++-------- src/internal/wagmi/core.ts | 8 ++++++-- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/internal/accountDelegation.ts b/src/internal/accountDelegation.ts index 550dbc63..bcf59aea 100644 --- a/src/internal/accountDelegation.ts +++ b/src/internal/accountDelegation.ts @@ -49,7 +49,7 @@ export type Calls = readonly { data?: Hex.Hex | undefined }[] -export type Key = WebAuthnKey | WebCryptoKey +export type Key = WebAuthnKey | WebCryptoKey | Secp256k1Key export type SerializedKey = { expiry: bigint @@ -66,6 +66,8 @@ export type WebCryptoKey = BaseKey< } > +export type Secp256k1Key = BaseKey<'secp256k1', {}> + //////////////////////////////////////////////////////////////// // Constants //////////////////////////////////////////////////////////////// @@ -73,11 +75,13 @@ export type WebCryptoKey = BaseKey< const keyType = { p256: 0, webauthn: 1, + secp256k1: 2, } as const const keyTypeSerialized = { 0: 'p256', 1: 'webauthn', + 2: 'secp256k1', } as const //////////////////////////////////////////////////////////// @@ -244,6 +248,28 @@ export declare namespace createWebCryptoKey { } } +/** Gets a Secp256k1Key in the required type **/ +export function getSecp256k1Key( + parameters: getSecp256k1Key.Parameters, +): Secp256k1Key { + const { expiry, publicKey } = parameters + return { + expiry, + publicKey, + status: 'locked', + type: 'secp256k1', + } +} + +export declare namespace getSecp256k1Key { + type Parameters = { + /** Expiry for the key. */ + expiry: bigint + /** Public key for the key. */ + publicKey: PublicKey.PublicKey + } +} + /** Executes calls on an Account. */ export async function execute( client: Client, diff --git a/src/internal/provider.ts b/src/internal/provider.ts index a38b3c6c..8bec13f3 100644 --- a/src/internal/provider.ts +++ b/src/internal/provider.ts @@ -236,7 +236,8 @@ export function from< const [ { address, - expiry = Math.floor(Date.now() / 1_000) + 60 * 60, // 1 hour + expiry = Math.floor(Date.now() / 1_000) + 60 * 60, // 1 hour, + keys, }, ] = (params as RpcSchema.ExtractParams< Schema.Schema, @@ -250,14 +251,25 @@ export function from< : state.accounts[0] if (!account) throw new ox_Provider.UnauthorizedError() - const key = await AccountDelegation.createWebCryptoKey({ - expiry: BigInt(expiry), - }) + let keysToDelegate: AccountDelegation.Key[] = [] + if (keys) { + keysToDelegate = keys.map((key) => + AccountDelegation.getSecp256k1Key({ + expiry: BigInt(expiry), + publicKey: PublicKey.fromHex(key.publicKey), + }), + ) + } else { + const key = await AccountDelegation.createWebCryptoKey({ + expiry: BigInt(expiry), + }) + keysToDelegate.push(key) + } // TODO: wait for tx to be included? await AccountDelegation.authorize(state.client, { account, - keys: [key], + keys: keysToDelegate, rpId: keystoreHost, }) @@ -270,20 +282,24 @@ export function from< ...x, accounts: x.accounts.map((account, i) => i === index - ? { ...account, keys: [...account.keys, key] } + ? { ...account, keys: [...account.keys, ...keysToDelegate] } : account, ), } }) emitter.emit('message', { - data: getActiveSessionKeys([...account.keys, key]), + data: getActiveSessionKeys([...account.keys, ...keysToDelegate]), type: 'sessionsChanged', }) + const id = Hex.concat( + ...keysToDelegate.map((key) => PublicKey.toHex(key.publicKey)), + ) + return { expiry, - id: PublicKey.toHex(key.publicKey), + id, } satisfies RpcSchema.ExtractReturnType< Schema.Schema, 'experimental_grantSession' diff --git a/src/internal/wagmi/core.ts b/src/internal/wagmi/core.ts index ba4fe59f..dcbe347e 100644 --- a/src/internal/wagmi/core.ts +++ b/src/internal/wagmi/core.ts @@ -252,7 +252,7 @@ export async function grantSession( config: config, parameters: grantSession.Parameters, ): Promise { - const { address, chainId, connector, expiry } = parameters + const { address, chainId, connector, expiry, keys } = parameters const client = await getConnectorClient(config, { account: address, @@ -268,7 +268,7 @@ export async function grantSession( ReturnType: RpcSchema.ExtractReturnType }>({ method, - params: [{ address, expiry }], + params: [{ address, expiry, keys }], }) } @@ -277,6 +277,10 @@ export declare namespace grantSession { ConnectorParameter & { address?: Address | undefined expiry?: number | undefined + keys?: { + algorithm: 'p256' | 'secp256k1' + publicKey: Hex + }[] | undefined } type ReturnType = { From 1696ebfe4c058ab56684bf66b6b737cad1124640 Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Tue, 17 Dec 2024 14:42:21 +0000 Subject: [PATCH 2/8] Add logic for returning correct key type --- src/internal/accountDelegation.ts | 22 ++++++++++++++++++++++ src/internal/provider.ts | 13 +++++++++---- src/internal/wagmi/core.ts | 10 ++++++---- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/internal/accountDelegation.ts b/src/internal/accountDelegation.ts index bcf59aea..6fa7abf4 100644 --- a/src/internal/accountDelegation.ts +++ b/src/internal/accountDelegation.ts @@ -248,6 +248,28 @@ export declare namespace createWebCryptoKey { } } +/** Gets a WebCrypto-P256 key in the required type **/ +export function getWebCryptoKey( + parameters: getWebCryptoKey.Parameters, +): WebCryptoKey { + const { expiry, publicKey } = parameters + return { + expiry, + publicKey, + status: 'locked', + type: 'p256', + } +} + +export declare namespace getWebCryptoKey { + type Parameters = { + /** Expiry for the key. */ + expiry: bigint + /** Public key for the key. */ + publicKey: PublicKey.PublicKey + } +} + /** Gets a Secp256k1Key in the required type **/ export function getSecp256k1Key( parameters: getSecp256k1Key.Parameters, diff --git a/src/internal/provider.ts b/src/internal/provider.ts index 8bec13f3..7a815c28 100644 --- a/src/internal/provider.ts +++ b/src/internal/provider.ts @@ -254,10 +254,15 @@ export function from< let keysToDelegate: AccountDelegation.Key[] = [] if (keys) { keysToDelegate = keys.map((key) => - AccountDelegation.getSecp256k1Key({ - expiry: BigInt(expiry), - publicKey: PublicKey.fromHex(key.publicKey), - }), + key.algorithm === 'secp256k1' + ? AccountDelegation.getSecp256k1Key({ + expiry: BigInt(expiry), + publicKey: PublicKey.fromHex(key.publicKey), + }) + : AccountDelegation.getWebCryptoKey({ + expiry: BigInt(expiry), + publicKey: PublicKey.fromHex(key.publicKey), + }), ) } else { const key = await AccountDelegation.createWebCryptoKey({ diff --git a/src/internal/wagmi/core.ts b/src/internal/wagmi/core.ts index dcbe347e..946d1b0c 100644 --- a/src/internal/wagmi/core.ts +++ b/src/internal/wagmi/core.ts @@ -277,10 +277,12 @@ export declare namespace grantSession { ConnectorParameter & { address?: Address | undefined expiry?: number | undefined - keys?: { - algorithm: 'p256' | 'secp256k1' - publicKey: Hex - }[] | undefined + keys?: + | { + algorithm: 'p256' | 'secp256k1' + publicKey: Hex + }[] + | undefined } type ReturnType = { From 38edc4983b1619eedbb58708380262d328d86038 Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Tue, 17 Dec 2024 15:18:07 +0000 Subject: [PATCH 3/8] Update contract and add ecdsa verify function --- .../src/account/ExperimentalDelegation.sol | 6 +++++- contracts/src/utils/ECDSA.sol | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/contracts/src/account/ExperimentalDelegation.sol b/contracts/src/account/ExperimentalDelegation.sol index 6fe312ba..14bbe213 100644 --- a/contracts/src/account/ExperimentalDelegation.sol +++ b/contracts/src/account/ExperimentalDelegation.sol @@ -20,7 +20,8 @@ contract ExperimentalDelegation is Receiver, MultiSendCallOnly { /// @notice The type of key. enum KeyType { P256, - WebAuthnP256 + WebAuthnP256, + Secp256k1 } /// @notice A Key that can be used to authorize calls. @@ -206,6 +207,9 @@ contract ExperimentalDelegation is Receiver, MultiSendCallOnly { WebAuthnP256.Metadata memory metadata = abi.decode(wrappedSignature.metadata, (WebAuthnP256.Metadata)); if (WebAuthnP256.verify(digest, metadata, wrappedSignature.signature, key.publicKey)) return success; } + if (key.keyType == KeyType.Secp256k1 && ECDSA.verify(digest, wrappedSignature.signature, key.publicKey)) { + return success; + } } // If the signature is not valid, return the failure magic value. diff --git a/contracts/src/utils/ECDSA.sol b/contracts/src/utils/ECDSA.sol index 17a9381c..cfa555fc 100644 --- a/contracts/src/utils/ECDSA.sol +++ b/contracts/src/utils/ECDSA.sol @@ -12,4 +12,21 @@ library ECDSA { uint256 s; uint8 yParity; } + + /// @notice Verifies an ECDSA signature. + /// @param digest 32 bytes of the signed data hash + /// @param signature Signature of the signer + /// @param publicKey Public key of the signer + /// @return success Represents if the operation was successful + function verify(bytes32 digest, Signature memory signature, PublicKey memory publicKey) internal pure returns (bool) { + address signer = ecrecover(digest, signature.v, signature.r, signature.s); + + if (signer == address(0)) return false; + + bytes memory publicKeyBytes = abi.encodePacked(bytes1(0x04), publicKey.x, publicKey.y); + + bytes32 publicKeyHash = keccak256(publicKeyBytes); + + return signer == address(uint160(uint256(publicKeyHash))); + } } From 07c76dc6ffa058a4474c35f04b4e8e18f6734a8a Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Tue, 17 Dec 2024 15:26:42 +0000 Subject: [PATCH 4/8] Add correct types --- contracts/src/utils/ECDSA.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/src/utils/ECDSA.sol b/contracts/src/utils/ECDSA.sol index cfa555fc..821936ad 100644 --- a/contracts/src/utils/ECDSA.sol +++ b/contracts/src/utils/ECDSA.sol @@ -18,8 +18,16 @@ library ECDSA { /// @param signature Signature of the signer /// @param publicKey Public key of the signer /// @return success Represents if the operation was successful - function verify(bytes32 digest, Signature memory signature, PublicKey memory publicKey) internal pure returns (bool) { - address signer = ecrecover(digest, signature.v, signature.r, signature.s); + function verify(bytes32 digest, Signature memory signature, PublicKey memory publicKey) + internal + pure + returns (bool) + { + uint8 v = signature.yParity + 27; + bytes32 r = bytes32(signature.r); + bytes32 s = bytes32(signature.s); + + address signer = ecrecover(digest, v, r, s); if (signer == address(0)) return false; From efb1b4ba2cce5c4c14d4c469c7018f9a3628fb57 Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Tue, 17 Dec 2024 15:41:11 +0000 Subject: [PATCH 5/8] USe clearer var name --- src/internal/provider.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/internal/provider.ts b/src/internal/provider.ts index 7a815c28..b9bb65e1 100644 --- a/src/internal/provider.ts +++ b/src/internal/provider.ts @@ -251,9 +251,9 @@ export function from< : state.accounts[0] if (!account) throw new ox_Provider.UnauthorizedError() - let keysToDelegate: AccountDelegation.Key[] = [] + let keysToAuthorize: AccountDelegation.Key[] = [] if (keys) { - keysToDelegate = keys.map((key) => + keysToAuthorize = keys.map((key) => key.algorithm === 'secp256k1' ? AccountDelegation.getSecp256k1Key({ expiry: BigInt(expiry), @@ -268,13 +268,13 @@ export function from< const key = await AccountDelegation.createWebCryptoKey({ expiry: BigInt(expiry), }) - keysToDelegate.push(key) + keysToAuthorize.push(key) } // TODO: wait for tx to be included? await AccountDelegation.authorize(state.client, { account, - keys: keysToDelegate, + keys: keysToAuthorize, rpId: keystoreHost, }) From a9a1be47fc9d8300a16973b36cd3f26bd70f98b8 Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Tue, 17 Dec 2024 15:47:46 +0000 Subject: [PATCH 6/8] Fix broken vars --- src/internal/provider.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/internal/provider.ts b/src/internal/provider.ts index b9bb65e1..0ee94550 100644 --- a/src/internal/provider.ts +++ b/src/internal/provider.ts @@ -287,19 +287,19 @@ export function from< ...x, accounts: x.accounts.map((account, i) => i === index - ? { ...account, keys: [...account.keys, ...keysToDelegate] } + ? { ...account, keys: [...account.keys, ...keysToAuthorize] } : account, ), } }) emitter.emit('message', { - data: getActiveSessionKeys([...account.keys, ...keysToDelegate]), + data: getActiveSessionKeys([...account.keys, ...keysToAuthorize]), type: 'sessionsChanged', }) const id = Hex.concat( - ...keysToDelegate.map((key) => PublicKey.toHex(key.publicKey)), + ...keysToAuthorize.map((key) => PublicKey.toHex(key.publicKey)), ) return { From 12f8d49348fa2b3c874302ee43f186b4843fcec4 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Dec 2024 06:53:21 +0200 Subject: [PATCH 7/8] chore: tweaks --- contracts/src/utils/ECDSA.sol | 2 +- .../test/account/ExperimentalDelegation.t.sol | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/contracts/src/utils/ECDSA.sol b/contracts/src/utils/ECDSA.sol index 821936ad..f6112ccb 100644 --- a/contracts/src/utils/ECDSA.sol +++ b/contracts/src/utils/ECDSA.sol @@ -31,7 +31,7 @@ library ECDSA { if (signer == address(0)) return false; - bytes memory publicKeyBytes = abi.encodePacked(bytes1(0x04), publicKey.x, publicKey.y); + bytes memory publicKeyBytes = abi.encodePacked(publicKey.x, publicKey.y); bytes32 publicKeyHash = keccak256(publicKeyBytes); diff --git a/contracts/test/account/ExperimentalDelegation.t.sol b/contracts/test/account/ExperimentalDelegation.t.sol index cc36541f..b7d20d56 100644 --- a/contracts/test/account/ExperimentalDelegation.t.sol +++ b/contracts/test/account/ExperimentalDelegation.t.sol @@ -88,6 +88,31 @@ contract ExperimentalDelegationTest is Test { assertEq(expiry, 0); } + function test_authorize_withSecp256k1Key() public { + vm.pauseGasMetering(); + + VmSafe.Wallet memory wallet = vm.createWallet("wallet"); + + ExperimentalDelegation.Key[] memory keys = new ExperimentalDelegation.Key[](1); + keys[0] = ExperimentalDelegation.Key( + 0, ExperimentalDelegation.KeyType.Secp256k1, ECDSA.PublicKey(wallet.publicKeyX, wallet.publicKeyY) + ); + + vm.expectRevert(); + delegation.keys(0); + + vm.prank(address(delegation)); + vm.resumeGasMetering(); + delegation.authorize(keys); + vm.pauseGasMetering(); + + (uint256 expiry, ExperimentalDelegation.KeyType keyType, ECDSA.PublicKey memory authorizedPublicKey) = + delegation.keys(0); + assertEq(authorizedPublicKey.x, wallet.publicKeyX); + assertEq(authorizedPublicKey.y, wallet.publicKeyY); + assertEq(expiry, 0); + } + function test_authorize_withAuthorizedKey() public { vm.pauseGasMetering(); @@ -322,4 +347,27 @@ contract ExperimentalDelegationTest is Test { bytes4(keccak256("isValidSignature(bytes32,bytes)")) ); } + + function test_isValidSignature_forAuthorizingSecp256k1Key() public { + bytes32 hash = keccak256(abi.encodePacked(delegation.nonce(), keccak256("0xdeadbeef"))); + VmSafe.Wallet memory wallet = vm.createWallet("wallet"); + + ExperimentalDelegation.Key[] memory keys = new ExperimentalDelegation.Key[](1); + keys[0] = ExperimentalDelegation.Key( + 0, ExperimentalDelegation.KeyType.Secp256k1, ECDSA.PublicKey(wallet.publicKeyX, wallet.publicKeyY) + ); + + vm.prank(address(delegation)); + delegation.authorize(keys); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(wallet.privateKey, hash); + ExperimentalDelegation.WrappedSignature memory wrappedSignature = ExperimentalDelegation.WrappedSignature( + 0, ECDSA.Signature(uint256(r), uint256(s), v == 27 ? 0 : 1), false, "0x" + ); + + assertEq( + delegation.isValidSignature(hash, abi.encode(wrappedSignature)), + bytes4(keccak256("isValidSignature(bytes32,bytes)")) + ); + } } From 7c3e0c9e4bb74905c0317e1167ec86cb49bb57db Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Dec 2024 07:32:13 +0200 Subject: [PATCH 8/8] chore: tweaks --- README.md | 3 +- src/internal/accountDelegation.ts | 52 +++++-------------------------- src/internal/provider.ts | 18 +++++------ src/internal/rpcSchema.ts | 2 +- src/internal/wagmi/core.ts | 3 +- 5 files changed, 20 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 50052f5f..47335b76 100644 --- a/README.md +++ b/README.md @@ -257,9 +257,10 @@ Grants a session on the account. expiry?: number // The keys to grant on the session. + // Defaults to a Web Crypto P256 key. keys?: { - algorithm: 'p256' | 'secp256k1', publicKey: `0x${string}`, + type: 'p256' | 'secp256k1' | 'webauthn', }[] }] } diff --git a/src/internal/accountDelegation.ts b/src/internal/accountDelegation.ts index 6fa7abf4..e94772e0 100644 --- a/src/internal/accountDelegation.ts +++ b/src/internal/accountDelegation.ts @@ -49,7 +49,11 @@ export type Calls = readonly { data?: Hex.Hex | undefined }[] -export type Key = WebAuthnKey | WebCryptoKey | Secp256k1Key +export type Key = + | WebAuthnKey + | WebCryptoKey + | Secp256k1Key + | BaseKey<'unknown', unknown> export type SerializedKey = { expiry: bigint @@ -73,10 +77,12 @@ export type Secp256k1Key = BaseKey<'secp256k1', {}> //////////////////////////////////////////////////////////////// const keyType = { + unknown: -1, p256: 0, webauthn: 1, secp256k1: 2, } as const +export type KeyType = keyof typeof keyType const keyTypeSerialized = { 0: 'p256', @@ -248,50 +254,6 @@ export declare namespace createWebCryptoKey { } } -/** Gets a WebCrypto-P256 key in the required type **/ -export function getWebCryptoKey( - parameters: getWebCryptoKey.Parameters, -): WebCryptoKey { - const { expiry, publicKey } = parameters - return { - expiry, - publicKey, - status: 'locked', - type: 'p256', - } -} - -export declare namespace getWebCryptoKey { - type Parameters = { - /** Expiry for the key. */ - expiry: bigint - /** Public key for the key. */ - publicKey: PublicKey.PublicKey - } -} - -/** Gets a Secp256k1Key in the required type **/ -export function getSecp256k1Key( - parameters: getSecp256k1Key.Parameters, -): Secp256k1Key { - const { expiry, publicKey } = parameters - return { - expiry, - publicKey, - status: 'locked', - type: 'secp256k1', - } -} - -export declare namespace getSecp256k1Key { - type Parameters = { - /** Expiry for the key. */ - expiry: bigint - /** Public key for the key. */ - publicKey: PublicKey.PublicKey - } -} - /** Executes calls on an Account. */ export async function execute( client: Client, diff --git a/src/internal/provider.ts b/src/internal/provider.ts index 0ee94550..a65a2bfc 100644 --- a/src/internal/provider.ts +++ b/src/internal/provider.ts @@ -253,16 +253,14 @@ export function from< let keysToAuthorize: AccountDelegation.Key[] = [] if (keys) { - keysToAuthorize = keys.map((key) => - key.algorithm === 'secp256k1' - ? AccountDelegation.getSecp256k1Key({ - expiry: BigInt(expiry), - publicKey: PublicKey.fromHex(key.publicKey), - }) - : AccountDelegation.getWebCryptoKey({ - expiry: BigInt(expiry), - publicKey: PublicKey.fromHex(key.publicKey), - }), + keysToAuthorize = keys.map( + (key) => + ({ + expiry: BigInt(expiry), + publicKey: PublicKey.fromHex(key.publicKey), + status: 'unlocked', + type: key.type as 'unknown', + }) as const, ) } else { const key = await AccountDelegation.createWebCryptoKey({ diff --git a/src/internal/rpcSchema.ts b/src/internal/rpcSchema.ts index d3bdb6a6..c69ff262 100644 --- a/src/internal/rpcSchema.ts +++ b/src/internal/rpcSchema.ts @@ -99,8 +99,8 @@ export type GrantSessionParameters = { expiry?: number | undefined keys?: | readonly { - algorithm: 'p256' | 'secp256k1' publicKey: Hex.Hex + type: AccountDelegation.KeyType }[] | undefined } diff --git a/src/internal/wagmi/core.ts b/src/internal/wagmi/core.ts index 946d1b0c..effe4264 100644 --- a/src/internal/wagmi/core.ts +++ b/src/internal/wagmi/core.ts @@ -21,6 +21,7 @@ import { disconnect as wagmi_disconnect, } from 'wagmi/actions' +import type * as AccountDelegation from '../accountDelegation.js' import type { CreateAccountParameters, GrantSessionParameters, @@ -279,8 +280,8 @@ export declare namespace grantSession { expiry?: number | undefined keys?: | { - algorithm: 'p256' | 'secp256k1' publicKey: Hex + type: AccountDelegation.KeyType }[] | undefined }