diff --git a/local-tests/test.ts b/local-tests/test.ts index b71cd6be1c..ee10a982f1 100644 --- a/local-tests/test.ts +++ b/local-tests/test.ts @@ -110,6 +110,11 @@ import { testSignMessageWithSolanaEncryptedKey } from './tests/wrapped-keys/test import { testSignTransactionWithSolanaEncryptedKey } from './tests/wrapped-keys/testSignTransactionWithSolanaEncryptedKey'; import { testBatchGeneratePrivateKeys } from './tests/wrapped-keys/testBatchGeneratePrivateKeys'; import { testFailBatchGeneratePrivateKeysAtomic } from './tests/wrapped-keys/testFailStoreEncryptedKeyBatchIsAtomic'; +import { testEthereumSignTypedDataWrappedKey } from './tests/wrapped-keys/testEthereumSignTypedDataWrappedKey'; +import { testEthereumSignTypedDataGeneratedKey } from './tests/wrapped-keys/testEthereumSignTypedDataGeneratedKey'; +import { testFailEthereumSignTypedDataWrappedKeyWithMissingParam } from './tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithMissingParam'; +import { testFailEthereumSignTypedDataWrappedKeyWithInvalidParam } from './tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithInvalidParam'; +import { testFailEthereumSignTypedDataWrappedKeyInvalidDecryption } from './tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyInvalidDecryption'; import { setLitActionsCodeToLocal } from './tests/wrapped-keys/util'; import { testUseEoaSessionSigsToRequestSingleResponse } from './tests/testUseEoaSessionSigsToRequestSingleResponse'; @@ -140,6 +145,10 @@ setLitActionsCodeToLocal(); testEthereumBroadcastTransactionWrappedKey, testEthereumBroadcastWrappedKeyWithFetchGasParams, + // -- typed data signing + testEthereumSignTypedDataWrappedKey, + testEthereumSignTypedDataGeneratedKey, + // -- generate wrapped keys testGenerateEthereumWrappedKey, testGenerateSolanaWrappedKey, @@ -160,6 +169,11 @@ setLitActionsCodeToLocal(); testFailEthereumSignTransactionWrappedKeyInvalidDecryption, testFailBatchGeneratePrivateKeysAtomic, + // -- typed data signing invalid cases + testFailEthereumSignTypedDataWrappedKeyWithMissingParam, + testFailEthereumSignTypedDataWrappedKeyWithInvalidParam, + testFailEthereumSignTypedDataWrappedKeyInvalidDecryption, + // -- import wrapped keys testFailImportWrappedKeysWithSamePrivateKey, testFailImportWrappedKeysWithEoaSessionSig, diff --git a/local-tests/tests/wrapped-keys/testEthereumSignTypedDataGeneratedKey.ts b/local-tests/tests/wrapped-keys/testEthereumSignTypedDataGeneratedKey.ts new file mode 100644 index 0000000000..5ad0b6ad96 --- /dev/null +++ b/local-tests/tests/wrapped-keys/testEthereumSignTypedDataGeneratedKey.ts @@ -0,0 +1,121 @@ +import { log } from '@lit-protocol/misc'; +import { ethers } from 'ethers'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; +import { api } from '@lit-protocol/wrapped-keys'; +import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; +import { deriveAddressFromGeneratedPublicKey } from './util'; + +const { generatePrivateKey, signTypedDataWithEncryptedKey } = api; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testEthereumSignTypedDataGeneratedKey + * ✅ NETWORK=datil-test yarn test:local --filter=testEthereumSignTypedDataGeneratedKey + * ✅ NETWORK=custom yarn test:local --filter=testEthereumSignTypedDataGeneratedKey + */ +export const testEthereumSignTypedDataGeneratedKey = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const pkpSessionSigs = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const { pkpAddress, id, generatedPublicKey } = await generatePrivateKey({ + pkpSessionSigs, + network: 'evm', + litNodeClient: devEnv.litNodeClient, + memo: 'Test key', + }); + + const alicePkpAddress = alice.authMethodOwnedPkp.ethAddress; + if (pkpAddress !== alicePkpAddress) { + throw new Error( + `Received address: ${pkpAddress} doesn't match Alice's PKP address: ${alicePkpAddress}` + ); + } + + const aliceWrappedKeyAddress = + deriveAddressFromGeneratedPublicKey(generatedPublicKey); + + const pkpSessionSigsSigning = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + // Test EIP-712 typed data with different structure + const typedData = { + domain: { + name: 'TestApp', + version: '2', + chainId: 1, + verifyingContract: '0x1234567890123456789012345678901234567890', + }, + types: { + Transaction: [ + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'data', type: 'bytes' }, + { name: 'operation', type: 'uint8' }, + { name: 'safeTxGas', type: 'uint256' }, + { name: 'baseGas', type: 'uint256' }, + { name: 'gasPrice', type: 'uint256' }, + { name: 'gasToken', type: 'address' }, + { name: 'refundReceiver', type: 'address' }, + { name: 'nonce', type: 'uint256' }, + ], + }, + value: { + to: '0x1234567890123456789012345678901234567890', + value: '0', + data: '0x', + operation: 0, + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: '0x0000000000000000000000000000000000000000', + refundReceiver: '0x0000000000000000000000000000000000000000', + nonce: 0, + }, + }; + + const signature = await signTypedDataWithEncryptedKey({ + pkpSessionSigs: pkpSessionSigsSigning, + network: 'evm', + messageToSign: typedData, + litNodeClient: devEnv.litNodeClient, + id, + }); + + if (!ethers.utils.isHexString(signature)) { + throw new Error(`signature isn't hex: ${signature}`); + } + + // Verify the signature is valid by recovering the address + const recoveredAddress = ethers.utils.verifyTypedData( + typedData.domain, + typedData.types, + typedData.value, + signature + ); + + if ( + recoveredAddress.toLowerCase() !== aliceWrappedKeyAddress.toLowerCase() + ) { + throw new Error( + `Recovered address: ${recoveredAddress} doesn't match Wrapped Key address: ${aliceWrappedKeyAddress}` + ); + } + + log('✅ testEthereumSignTypedDataGeneratedKey'); + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/testEthereumSignTypedDataWrappedKey.ts b/local-tests/tests/wrapped-keys/testEthereumSignTypedDataWrappedKey.ts new file mode 100644 index 0000000000..0a5c7b486e --- /dev/null +++ b/local-tests/tests/wrapped-keys/testEthereumSignTypedDataWrappedKey.ts @@ -0,0 +1,123 @@ +import { log } from '@lit-protocol/misc'; +import { ethers } from 'ethers'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; +import { api } from '@lit-protocol/wrapped-keys'; +import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; +import { deriveAddressFromGeneratedPublicKey } from './util'; + +const { importPrivateKey, signTypedDataWithEncryptedKey } = api; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testEthereumSignTypedDataWrappedKey + * ✅ NETWORK=datil-test yarn test:local --filter=testEthereumSignTypedDataWrappedKey + * ✅ NETWORK=custom yarn test:local --filter=testEthereumSignTypedDataWrappedKey + */ +export const testEthereumSignTypedDataWrappedKey = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const pkpSessionSigs = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const aliceWrappedKey = ethers.Wallet.createRandom(); + const privateKey = aliceWrappedKey.privateKey; + const aliceWrappedKeyAddress = aliceWrappedKey.address; + + const { pkpAddress, id } = await importPrivateKey({ + pkpSessionSigs, + privateKey, + litNodeClient: devEnv.litNodeClient, + publicKey: aliceWrappedKeyAddress, + keyType: 'K256', + memo: 'Test key', + }); + + const alicePkpAddress = alice.authMethodOwnedPkp.ethAddress; + if (pkpAddress !== alicePkpAddress) { + throw new Error( + `Received address: ${pkpAddress} doesn't match Alice's PKP address: ${alicePkpAddress}` + ); + } + + const pkpSessionSigsSigning = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + // Test EIP-712 typed data + const typedData = { + domain: { + name: 'TestApp', + version: '1', + chainId: 1, + verifyingContract: '0x1234567890123456789012345678901234567890', + }, + types: { + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + value: { + from: { + name: 'Alice', + wallet: alice.wallet.address, + }, + to: { + name: 'Bob', + wallet: '0x1234567890123456789012345678901234567890', + }, + contents: 'Hello, Bob!', + }, + }; + + const signature = await signTypedDataWithEncryptedKey({ + pkpSessionSigs: pkpSessionSigsSigning, + network: 'evm', + messageToSign: typedData, + litNodeClient: devEnv.litNodeClient, + id, + }); + + // console.log('signature'); + // console.log(signature); + + if (!ethers.utils.isHexString(signature)) { + throw new Error(`signature isn't hex: ${signature}`); + } + + // Verify the signature is valid by recovering the address + const recoveredAddress = ethers.utils.verifyTypedData( + typedData.domain, + typedData.types, + typedData.value, + signature + ); + + if ( + recoveredAddress.toLowerCase() !== aliceWrappedKeyAddress.toLowerCase() + ) { + throw new Error( + `Recovered address: ${recoveredAddress} doesn't match PKP address: ${pkpAddress}` + ); + } + + log('✅ testEthereumSignTypedDataWrappedKey'); + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyInvalidDecryption.ts b/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyInvalidDecryption.ts new file mode 100644 index 0000000000..b52e6e7d9d --- /dev/null +++ b/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyInvalidDecryption.ts @@ -0,0 +1,107 @@ +import { log } from '@lit-protocol/misc'; +import { ethers } from 'ethers'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; +import { api } from '@lit-protocol/wrapped-keys'; +import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; + +const { importPrivateKey, signTypedDataWithEncryptedKey } = api; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyInvalidDecryption + * ✅ NETWORK=datil-test yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyInvalidDecryption + * ✅ NETWORK=custom yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyInvalidDecryption + */ +export const testFailEthereumSignTypedDataWrappedKeyInvalidDecryption = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const pkpSessionSigs = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const aliceWrappedKey = ethers.Wallet.createRandom(); + const privateKey = aliceWrappedKey.privateKey; + const aliceWrappedKeyAddress = aliceWrappedKey.address; + + const { pkpAddress, id } = await importPrivateKey({ + pkpSessionSigs, + privateKey, + litNodeClient: devEnv.litNodeClient, + publicKey: aliceWrappedKeyAddress, + keyType: 'K256', + memo: 'Test key', + }); + + const alicePkpAddress = alice.authMethodOwnedPkp.ethAddress; + if (pkpAddress !== alicePkpAddress) { + throw new Error( + `Received address: ${pkpAddress} doesn't match Alice's PKP address: ${alicePkpAddress}` + ); + } + + // Use a different user's session sigs to try to decrypt the key + const bob = await devEnv.createRandomPerson(); + const bobPkpSessionSigs = await getPkpSessionSigs( + devEnv, + bob, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const typedData = { + domain: { + name: 'TestApp', + version: '1', + chainId: 1, + verifyingContract: '0x1234567890123456789012345678901234567890', + }, + types: { + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + value: { + from: alice.wallet.address, + to: '0x1234567890123456789012345678901234567890', + contents: 'Hello, Bob!', + }, + }; + + try { + const _res = await signTypedDataWithEncryptedKey({ + pkpSessionSigs: bobPkpSessionSigs, // Using Bob's session sigs to try to decrypt Alice's key + network: 'evm', + messageToSign: typedData, + litNodeClient: devEnv.litNodeClient, + id, // Alice's key ID + }); + } catch (e: any) { + if (e.message.includes('Could not find')) { + console.log('✅ THIS IS EXPECTED: ', e); + console.log(e.message); + console.log( + '✅ testFailEthereumSignTypedDataWrappedKeyInvalidDecryption is expected to have an error' + ); + } else { + console.log('ERROR', e.message); + throw e; + } + } + + log('✅ testFailEthereumSignTypedDataWrappedKeyInvalidDecryption'); + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithInvalidParam.ts b/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithInvalidParam.ts new file mode 100644 index 0000000000..8786b7001c --- /dev/null +++ b/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithInvalidParam.ts @@ -0,0 +1,101 @@ +import { log } from '@lit-protocol/misc'; +import { ethers } from 'ethers'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; +import { api } from '@lit-protocol/wrapped-keys'; +import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; + +const { importPrivateKey, signTypedDataWithEncryptedKey } = api; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyWithInvalidParam + * ✅ NETWORK=datil-test yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyWithInvalidParam + * ✅ NETWORK=custom yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyWithInvalidParam + */ +export const testFailEthereumSignTypedDataWrappedKeyWithInvalidParam = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const pkpSessionSigs = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const privateKey = ethers.Wallet.createRandom().privateKey; + + const { pkpAddress, id } = await importPrivateKey({ + pkpSessionSigs, + privateKey, + litNodeClient: devEnv.litNodeClient, + publicKey: '0xdeadbeef', + keyType: 'K256', + memo: 'Test key', + }); + + const alicePkpAddress = alice.authMethodOwnedPkp.ethAddress; + if (pkpAddress !== alicePkpAddress) { + throw new Error( + `Received address: ${pkpAddress} doesn't match Alice's PKP address: ${alicePkpAddress}` + ); + } + + const pkpSessionSigsSigning = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + try { + const _res = await signTypedDataWithEncryptedKey({ + pkpSessionSigs: pkpSessionSigsSigning, + network: 'evm', + messageToSign: { + domain: { + name: 'TestApp', + version: '1', + chainId: 'invalid-chain-id', // Invalid: should be number + verifyingContract: 'invalid-address', // Invalid: should be valid address + }, + types: { + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + }, + value: { + name: 'Alice', + wallet: 'invalid-address', // Invalid: should be valid address + }, + }, + litNodeClient: devEnv.litNodeClient, + id, + }); + } catch (e: any) { + if ( + e.message.includes('invalid address') || + e.message.includes('Invalid typed data') || + e.message.includes('chainId') || + e.message.includes('verifyingContract') || + e.message.includes('wallet') + ) { + console.log('✅ THIS IS EXPECTED: ', e); + console.log(e.message); + console.log( + '✅ testFailEthereumSignTypedDataWrappedKeyWithInvalidParam is expected to have an error' + ); + } else { + console.log('ERROR', e.message); + throw e; + } + } + + log('✅ testFailEthereumSignTypedDataWrappedKeyWithInvalidParam'); + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithMissingParam.ts b/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithMissingParam.ts new file mode 100644 index 0000000000..7c31f46dc1 --- /dev/null +++ b/local-tests/tests/wrapped-keys/testFailEthereumSignTypedDataWrappedKeyWithMissingParam.ts @@ -0,0 +1,90 @@ +import { log } from '@lit-protocol/misc'; +import { ethers } from 'ethers'; +import { TinnyEnvironment } from 'local-tests/setup/tinny-environment'; +import { api } from '@lit-protocol/wrapped-keys'; +import { getPkpSessionSigs } from 'local-tests/setup/session-sigs/get-pkp-session-sigs'; + +const { importPrivateKey, signTypedDataWithEncryptedKey } = api; + +/** + * Test Commands: + * ✅ NETWORK=datil-dev yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyWithMissingParam + * ✅ NETWORK=datil-test yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyWithMissingParam + * ✅ NETWORK=custom yarn test:local --filter=testFailEthereumSignTypedDataWrappedKeyWithMissingParam + */ +export const testFailEthereumSignTypedDataWrappedKeyWithMissingParam = async ( + devEnv: TinnyEnvironment +) => { + const alice = await devEnv.createRandomPerson(); + + try { + const pkpSessionSigs = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + const privateKey = ethers.Wallet.createRandom().privateKey; + + const { pkpAddress, id } = await importPrivateKey({ + pkpSessionSigs, + privateKey, + litNodeClient: devEnv.litNodeClient, + publicKey: '0xdeadbeef', + keyType: 'K256', + memo: 'Test key', + }); + + const alicePkpAddress = alice.authMethodOwnedPkp.ethAddress; + if (pkpAddress !== alicePkpAddress) { + throw new Error( + `Received address: ${pkpAddress} doesn't match Alice's PKP address: ${alicePkpAddress}` + ); + } + + const pkpSessionSigsSigning = await getPkpSessionSigs( + devEnv, + alice, + null, + new Date(Date.now() + 1000 * 60 * 10).toISOString() + ); // 10 mins expiry + + try { + const _res = await signTypedDataWithEncryptedKey({ + pkpSessionSigs: pkpSessionSigsSigning, + network: 'evm', + messageToSign: { + domain: { + name: 'TestApp', + // Missing required fields: version, chainId, verifyingContract + }, + types: {}, + value: {}, + }, + litNodeClient: devEnv.litNodeClient, + id, + }); + } catch (e: any) { + if ( + e.message.includes('Missing required field') || + e.message.includes('Invalid typed data') || + e.message.includes('domain') || + e.message.includes('types') || + e.message.includes('value') + ) { + console.log('✅ THIS IS EXPECTED: ', e); + console.log(e.message); + console.log( + '✅ testFailEthereumSignTypedDataWrappedKeyWithMissingParam is expected to have an error' + ); + } else { + throw e; + } + } + + log('✅ testFailEthereumSignTypedDataWrappedKeyWithMissingParam'); + } finally { + devEnv.releasePrivateKeyFromUser(alice); + } +}; diff --git a/local-tests/tests/wrapped-keys/util.ts b/local-tests/tests/wrapped-keys/util.ts index f03f78d420..2ef84ba305 100644 --- a/local-tests/tests/wrapped-keys/util.ts +++ b/local-tests/tests/wrapped-keys/util.ts @@ -26,6 +26,10 @@ const emptyLitActionRepository: LitActionCodeRepository = { evm: '', solana: '', }, + signTypedData: { + evm: '', + solana: '', + }, generateEncryptedKey: { evm: '', solana: '', @@ -104,3 +108,20 @@ export function getBaseTransactionForNetwork({ ), }; } + +/** + * Derives an Ethereum address from a generated public key + * @param generatedPublicKey The public key string (with or without 0x prefix, with or without 0x04 prefix) + * @returns The derived Ethereum address + */ +export function deriveAddressFromGeneratedPublicKey( + generatedPublicKey: string +): string { + const sanitizedPublicKey = generatedPublicKey.slice( + generatedPublicKey.startsWith('0x04') ? 4 : 2 + ); + const addressHash = ethers.utils.keccak256(`0x${sanitizedPublicKey}`); + return ethers.utils.getAddress( + `0x${addressHash.substring(addressHash.length - 40)}` + ); +} diff --git a/packages/wrapped-keys-lit-actions/esbuild.config.js b/packages/wrapped-keys-lit-actions/esbuild.config.js index 311ebc338e..df12c64f8d 100644 --- a/packages/wrapped-keys-lit-actions/esbuild.config.js +++ b/packages/wrapped-keys-lit-actions/esbuild.config.js @@ -56,6 +56,7 @@ module.exports = { './src/lib/self-executing-actions/solana/generateEncryptedSolanaPrivateKey.ts', './src/lib/self-executing-actions/ethereum/signTransactionWithEncryptedEthereumKey.ts', './src/lib/self-executing-actions/ethereum/signMessageWithEncryptedEthereumKey.ts', + './src/lib/self-executing-actions/ethereum/signTypedDataWithEncryptedEthereumKey.ts', './src/lib/self-executing-actions/ethereum/generateEncryptedEthereumPrivateKey.ts', './src/lib/self-executing-actions/common/exportPrivateKey.ts', './src/lib/self-executing-actions/common/batchGenerateEncryptedKeys.ts', diff --git a/packages/wrapped-keys-lit-actions/src/index.ts b/packages/wrapped-keys-lit-actions/src/index.ts index 5e8f5427b0..93f950deed 100644 --- a/packages/wrapped-keys-lit-actions/src/index.ts +++ b/packages/wrapped-keys-lit-actions/src/index.ts @@ -3,6 +3,7 @@ import * as exportPrivateKey from './generated/common/exportPrivateKey'; import * as generateEncryptedEthereumPrivateKey from './generated/ethereum/generateEncryptedEthereumPrivateKey'; import * as signMessageWithEthereumEncryptedKey from './generated/ethereum/signMessageWithEncryptedEthereumKey'; import * as signTransactionWithEthereumEncryptedKey from './generated/ethereum/signTransactionWithEncryptedEthereumKey'; +import * as signTypedDataWithEthereumEncryptedKey from './generated/ethereum/signTypedDataWithEncryptedEthereumKey'; import * as generateEncryptedSolanaPrivateKey from './generated/solana/generateEncryptedSolanaPrivateKey'; import * as signMessageWithSolanaEncryptedKey from './generated/solana/signMessageWithEncryptedSolanaKey'; import * as signTransactionWithSolanaEncryptedKey from './generated/solana/signTransactionWithEncryptedSolanaKey'; @@ -22,6 +23,10 @@ const litActionRepository: LitActionCodeRepository = { evm: signMessageWithEthereumEncryptedKey.code, solana: signMessageWithSolanaEncryptedKey.code, }, + signTypedData: { + evm: signTypedDataWithEthereumEncryptedKey.code, + solana: '', + }, generateEncryptedKey: { evm: generateEncryptedEthereumPrivateKey.code, solana: generateEncryptedSolanaPrivateKey.code, @@ -47,6 +52,7 @@ export { generateEncryptedEthereumPrivateKey, signMessageWithEthereumEncryptedKey, signTransactionWithEthereumEncryptedKey, + signTypedDataWithEthereumEncryptedKey, generateEncryptedSolanaPrivateKey, signMessageWithSolanaEncryptedKey, signTransactionWithSolanaEncryptedKey, diff --git a/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTypedData.ts b/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTypedData.ts new file mode 100644 index 0000000000..55c395bd1c --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/internal/ethereum/signTypedData.ts @@ -0,0 +1,86 @@ +interface TypedDataField { + name: string; + type: string; +} +interface TypedDataMessage { + domain: { + name?: string; + version?: string; + chainId?: number | string | bigint; + verifyingContract?: string; + salt?: string; + }; + types: Record; + value: Record; +} +interface SignTypedDataParams { + privateKey: string; + messageToSign: TypedDataMessage; +} + +interface VerifyMessageSignatureParams { + messageToSign: TypedDataMessage; + signature: string; +} + +async function signTypedData({ + privateKey, + messageToSign, +}: SignTypedDataParams): Promise<{ signature: string; walletAddress: string }> { + try { + const wallet = new ethers.Wallet(privateKey); + const signature = await wallet._signTypedData( + messageToSign.domain, + messageToSign.types, + messageToSign.value + ); + + return { signature, walletAddress: wallet.address }; + } catch (err: unknown) { + if (err instanceof Error) { + throw new Error(`When signing message - ${err.message}`); + } else { + throw new Error(`An unexpected error occurred: ${err}`); + } + } +} + +function verifyMessageSignature({ + messageToSign, + signature, +}: VerifyMessageSignatureParams): string { + try { + return ethers.utils.verifyTypedData( + messageToSign.domain, + messageToSign.types, + messageToSign.value, + signature + ); + } catch (err: unknown) { + throw new Error( + `When validating signed Ethereum message is valid: ${ + (err as Error).message + }` + ); + } +} + +export async function signTypedDataEthereumKey({ + privateKey, + messageToSign, +}: SignTypedDataParams): Promise { + const { signature, walletAddress } = await signTypedData({ + privateKey, + messageToSign, + }); + + const recoveredAddress = verifyMessageSignature({ messageToSign, signature }); + + if (recoveredAddress !== walletAddress) { + throw new Error( + "Recovered address from verifyMessage doesn't match the wallet address" + ); + } + + return signature; +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/ethereum/signTypedDataWithEncryptedEthereumKey.ts b/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/ethereum/signTypedDataWithEncryptedEthereumKey.ts new file mode 100644 index 0000000000..f4e9f149ad --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/raw-action-functions/ethereum/signTypedDataWithEncryptedEthereumKey.ts @@ -0,0 +1,47 @@ +import { getDecryptedKeyToSingleNode } from '../../internal/common/getDecryptedKeyToSingleNode'; +import { signTypedDataEthereumKey } from '../../internal/ethereum/signTypedData'; + +interface TypedDataField { + name: string; + type: string; +} +interface TypedDataMessage { + domain: { + name?: string; + version?: string; + chainId?: number | string | bigint; + verifyingContract?: string; + salt?: string; + }; + types: Record; + value: Record; +} +export interface SignTypedDataWithEncryptedEthereumKeyParams { + accessControlConditions: string; + ciphertext: string; + dataToEncryptHash: string; + messageToSign: TypedDataMessage; +} + +/** + * Signs a message with the Ethers wallet which is also decrypted inside the Lit Action. + * @param {string} pkpAddress - The Eth address of the PKP which is associated with the Wrapped Key + * @returns {Promise} - Returns a message signed by the Ethers Wrapped key. Or returns errors if any. + */ +export async function signTypedDataWithEncryptedEthereumKey({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + messageToSign, +}: SignTypedDataWithEncryptedEthereumKeyParams): Promise { + const privateKey = await getDecryptedKeyToSingleNode({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + }); + + return signTypedDataEthereumKey({ + privateKey, + messageToSign, + }); +} diff --git a/packages/wrapped-keys-lit-actions/src/lib/self-executing-actions/ethereum/signTypedDataWithEncryptedEthereumKey.ts b/packages/wrapped-keys-lit-actions/src/lib/self-executing-actions/ethereum/signTypedDataWithEncryptedEthereumKey.ts new file mode 100644 index 0000000000..a6a3249073 --- /dev/null +++ b/packages/wrapped-keys-lit-actions/src/lib/self-executing-actions/ethereum/signTypedDataWithEncryptedEthereumKey.ts @@ -0,0 +1,23 @@ +import { litActionHandler } from '../../litActionHandler'; +import { + SignTypedDataWithEncryptedEthereumKeyParams, + signTypedDataWithEncryptedEthereumKey, +} from '../../raw-action-functions/ethereum/signTypedDataWithEncryptedEthereumKey'; + +import type { SignMessageWithEncryptedEthereumKeyParams } from '../../raw-action-functions/ethereum/signMessageWithEncryptedEthereumKey'; + +// Using local declarations to avoid _every file_ thinking these are always in scope +declare const accessControlConditions: SignMessageWithEncryptedEthereumKeyParams['accessControlConditions']; +declare const ciphertext: SignMessageWithEncryptedEthereumKeyParams['ciphertext']; +declare const dataToEncryptHash: SignMessageWithEncryptedEthereumKeyParams['dataToEncryptHash']; +declare const messageToSign: SignTypedDataWithEncryptedEthereumKeyParams['messageToSign']; + +(async () => + litActionHandler(async () => + signTypedDataWithEncryptedEthereumKey({ + accessControlConditions, + ciphertext, + dataToEncryptHash, + messageToSign, + }) + ))(); diff --git a/packages/wrapped-keys/src/index.ts b/packages/wrapped-keys/src/index.ts index 88312d4671..dd5db52c59 100644 --- a/packages/wrapped-keys/src/index.ts +++ b/packages/wrapped-keys/src/index.ts @@ -5,6 +5,7 @@ import { generatePrivateKey, importPrivateKey, signTransactionWithEncryptedKey, + signTypedDataWithEncryptedKey, storeEncryptedKey, listEncryptedKeyMetadata, batchGeneratePrivateKeys, @@ -72,6 +73,7 @@ export const api = { importPrivateKey, signMessageWithEncryptedKey, signTransactionWithEncryptedKey, + signTypedDataWithEncryptedKey, storeEncryptedKey, storeEncryptedKeyBatch, batchGeneratePrivateKeys, diff --git a/packages/wrapped-keys/src/lib/api/index.ts b/packages/wrapped-keys/src/lib/api/index.ts index 2eebfbfc57..291c81574a 100644 --- a/packages/wrapped-keys/src/lib/api/index.ts +++ b/packages/wrapped-keys/src/lib/api/index.ts @@ -6,6 +6,7 @@ import { importPrivateKey } from './import-private-key'; import { listEncryptedKeyMetadata } from './list-encrypted-key-metadata'; import { signMessageWithEncryptedKey } from './sign-message-with-encrypted-key'; import { signTransactionWithEncryptedKey } from './sign-transaction-with-encrypted-key'; +import { signTypedDataWithEncryptedKey } from './sign-typed-data-with-encrypted-key'; import { storeEncryptedKey } from './store-encrypted-key'; import { storeEncryptedKeyBatch } from './store-encrypted-key-batch'; @@ -16,6 +17,7 @@ export { signTransactionWithEncryptedKey, exportPrivateKey, signMessageWithEncryptedKey, + signTypedDataWithEncryptedKey, storeEncryptedKey, storeEncryptedKeyBatch, getEncryptedKey, diff --git a/packages/wrapped-keys/src/lib/api/sign-typed-data-with-encrypted-key.ts b/packages/wrapped-keys/src/lib/api/sign-typed-data-with-encrypted-key.ts new file mode 100644 index 0000000000..ce670f5dcd --- /dev/null +++ b/packages/wrapped-keys/src/lib/api/sign-typed-data-with-encrypted-key.ts @@ -0,0 +1,52 @@ +import { + getFirstSessionSig, + getPkpAccessControlCondition, + getPkpAddressFromSessionSig, +} from './utils'; +import { signMessageWithLitAction } from '../lit-actions-client'; +import { getLitActionCodeOrCid } from '../lit-actions-client/utils'; +import { fetchPrivateKey } from '../service-client'; +import { SignMessageWithEncryptedKeyParams } from '../types'; + +/** + * Signs a typed data inside the Lit Action using the previously persisted wrapped key associated with the current LIT PK. + * This method fetches the encrypted key from the wrapped keys service, then executes a Lit Action that decrypts the key inside the LIT action and uses + * the decrypted key to sign the provided typed data + * + * @param { SignMessageWithEncryptedKeyParams } params Parameters to use for signing the message + * + * @returns { Promise } - The signed typed data + */ +export async function signTypedDataWithEncryptedKey( + params: SignMessageWithEncryptedKeyParams +): Promise { + const { litNodeClient, network, pkpSessionSigs, id } = params; + + const sessionSig = getFirstSessionSig(pkpSessionSigs); + const pkpAddress = getPkpAddressFromSessionSig(sessionSig); + + const storedKeyMetadata = await fetchPrivateKey({ + pkpAddress, + id, + sessionSig, + litNetwork: litNodeClient.config.litNetwork, + }); + + const allowPkpAddressToDecrypt = getPkpAccessControlCondition( + storedKeyMetadata.pkpAddress + ); + + const { litActionCode, litActionIpfsCid } = getLitActionCodeOrCid( + network, + 'signTypedData' + ); + + return signMessageWithLitAction({ + ...params, + litActionIpfsCid: litActionCode ? undefined : litActionIpfsCid, + litActionCode: litActionCode ? litActionCode : undefined, + accessControlConditions: [allowPkpAddressToDecrypt], + pkpSessionSigs, + storedKeyMetadata, + }); +} diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.spec.ts b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.spec.ts index f7c8bbb845..b3b99b9e15 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.spec.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.spec.ts @@ -16,6 +16,10 @@ describe('wrapped keys lit action code repository', () => { evm: '', solana: '', }, + signTypedData: { + evm: '', + solana: '', + }, generateEncryptedKey: { evm: '', solana: '', @@ -88,6 +92,10 @@ describe('wrapped keys lit action code repository', () => { evm: 'test', solana: 'test', }, + signTypedData: { + evm: 'test', + solana: 'test', + }, generateEncryptedKey: { evm: 'test', solana: 'test', @@ -106,6 +114,10 @@ describe('wrapped keys lit action code repository', () => { evm: 'test', solana: 'test', }, + signTypedData: { + evm: 'test', + solana: 'test', + }, generateEncryptedKey: { evm: 'test', solana: 'test', @@ -127,6 +139,10 @@ describe('wrapped keys lit action code repository', () => { evm: 'test', solana: 'test', }, + signTypedData: { + evm: 'test', + solana: 'test', + }, generateEncryptedKey: { evm: 'test', solana: 'test', @@ -154,6 +170,10 @@ describe('wrapped keys lit action code repository', () => { evm: 'eth', solana: 'test', }, + signTypedData: { + evm: 'eth', + solana: 'test', + }, generateEncryptedKey: { evm: 'test', solana: 'test', diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts index 5f3121704b..9ba5a97402 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/code-repository.ts @@ -23,6 +23,10 @@ const litActionCodeRepository: LitActionCodeRepository = Object.freeze({ evm: '', solana: '', }), + signTypedData: Object.seal({ + evm: '', + solana: '', + }), generateEncryptedKey: Object.seal({ evm: '', solana: '', diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts b/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts index 999191cd8a..e007fbc208 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/constants.ts @@ -21,6 +21,10 @@ const LIT_ACTION_CID_REPOSITORY: LitCidRepository = deepFreeze({ evm: GENERATED_LIT_ACTION_CID_REPOSITORY.signMessage.evm, solana: GENERATED_LIT_ACTION_CID_REPOSITORY.signMessage.solana, }, + signTypedData: { + evm: GENERATED_LIT_ACTION_CID_REPOSITORY.signTypedData.evm, + solana: GENERATED_LIT_ACTION_CID_REPOSITORY.signTypedData.solana, + }, generateEncryptedKey: { evm: GENERATED_LIT_ACTION_CID_REPOSITORY.generateEncryptedKey.evm, solana: GENERATED_LIT_ACTION_CID_REPOSITORY.generateEncryptedKey.solana, diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/lit-action-cid-repository.json b/packages/wrapped-keys/src/lib/lit-actions-client/lit-action-cid-repository.json index f1ae0aa6a6..a7730a85b8 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/lit-action-cid-repository.json +++ b/packages/wrapped-keys/src/lib/lit-actions-client/lit-action-cid-repository.json @@ -7,6 +7,10 @@ "evm": "Qmdm4mGn6A8RmqeiDgRPXFt2yYEKCUMUVimSb5iPJfs31e", "solana": "QmS4Y6f2zHriNzioxBbysbuXQbjX7ga468CCyYcuY2AeeH" }, + "signTypedData": { + "evm": "Qme9u1juozWAZFv9c2dxnZS5t3ErUNcDfZNKqSuXkreH5t", + "solana": "" + }, "generateEncryptedKey": { "evm": "QmfW6g5PJ8SVS56XwDVC5W4gcUnobEempNkR28bj2g99tk", "solana": "QmWYcBCZqFmJJzsRfzVRHbPD5Hvuou45sbXPQcQzLbUgKd" diff --git a/packages/wrapped-keys/src/lib/lit-actions-client/types.ts b/packages/wrapped-keys/src/lib/lit-actions-client/types.ts index bda28ecd57..5687ed1b56 100644 --- a/packages/wrapped-keys/src/lib/lit-actions-client/types.ts +++ b/packages/wrapped-keys/src/lib/lit-actions-client/types.ts @@ -3,6 +3,7 @@ import { Network } from '../types'; export type LitActionType = | 'signTransaction' | 'signMessage' + | 'signTypedData' | 'generateEncryptedKey' | 'exportPrivateKey'; diff --git a/packages/wrapped-keys/src/lib/types.ts b/packages/wrapped-keys/src/lib/types.ts index b76f2cff02..ba6e449ae4 100644 --- a/packages/wrapped-keys/src/lib/types.ts +++ b/packages/wrapped-keys/src/lib/types.ts @@ -232,8 +232,27 @@ export interface ImportPrivateKeyResult { id: string; } +interface TypedDataField { + name: string; + type: string; +} + +interface TypedDataDomain { + name?: string; + version?: string; + chainId?: number | string | bigint; + verifyingContract?: string; + salt?: string; +} + +interface signTypedDataMessageParams { + domain: TypedDataDomain; + types: Record; + value: Record; +} + interface SignMessageParams { - messageToSign: string | Uint8Array; + messageToSign: string | Uint8Array | signTypedDataMessageParams; id: string; } diff --git a/packages/wrapped-keys/update-ipfs-cids.js b/packages/wrapped-keys/update-ipfs-cids.js index 3f5b8d5583..b838f00f8a 100644 --- a/packages/wrapped-keys/update-ipfs-cids.js +++ b/packages/wrapped-keys/update-ipfs-cids.js @@ -18,6 +18,9 @@ const { const { code: signTransactionWithEncryptedEthereumKey, } = require('../wrapped-keys-lit-actions/src/generated/ethereum/signTransactionWithEncryptedEthereumKey'); +const { + code: signTypedDataWithEncryptedEthereumKey, +} = require('../wrapped-keys-lit-actions/src/generated/ethereum/signTypedDataWithEncryptedEthereumKey'); const { code: generateEncryptedSolanaPrivateKey, } = require('../wrapped-keys-lit-actions/src/generated/solana/generateEncryptedSolanaPrivateKey'); @@ -39,6 +42,10 @@ async function updateConstants() { evm: await Hash.of(signMessageWithEncryptedEthereumKey), solana: await Hash.of(signMessageWithEncryptedSolanaKey), }, + signTypedData: { + evm: await Hash.of(signTypedDataWithEncryptedEthereumKey), + solana: '', + }, generateEncryptedKey: { evm: await Hash.of(generateEncryptedEthereumPrivateKey), solana: await Hash.of(generateEncryptedSolanaPrivateKey),