From a13b96cd7534c2857e3acd8c2c86cfbbf7ee5754 Mon Sep 17 00:00:00 2001 From: Nick Cruz Date: Mon, 13 Feb 2023 17:29:21 -0500 Subject: [PATCH] initial commit --- package.json | 11 ++---- src/App.tsx | 50 +++++++++++++++++++---- src/utils/index.ts | 1 + src/utils/serializeLedgerMessage.ts | 61 +++++++++++++++++++++++++++++ src/utils/signMessage.ts | 6 ++- src/utils/verifyMessage.ts | 29 ++++++++++++++ 6 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 src/utils/serializeLedgerMessage.ts create mode 100644 src/utils/verifyMessage.ts diff --git a/package.json b/package.json index 1f1dc0a..2226d8d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ ], "main": "src/index.tsx", "dependencies": { - "@solana/web3.js": "1.63.1" + "@solana/web3.js": "1.63.1", + "bs58": "^4.0.1", + "tweetnacl": "^1.0.3" }, "devDependencies": { "@types/react": "^17.0.21", @@ -36,10 +38,5 @@ "test": "react-scripts test --env=jsdom", "tsc": "tsc" }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ] + "browserslist": [">0.2%", "not dead", "not ie <= 11", "not op_mini all"] } diff --git a/src/App.tsx b/src/App.tsx index 440dabc..0a6d3e9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,8 @@ import { signAndSendTransactionV0WithLookupTable, signMessage, signTransaction, + verifyMessage, + verifyOffChainLedgerMessage, } from './utils'; import { TLog } from './types'; @@ -54,13 +56,13 @@ const message = 'To avoid digital dognappers, sign below to authenticate with Cr export type ConnectedMethods = | { - name: string; - onClick: () => Promise; - } + name: string; + onClick: () => Promise; + } | { - name: string; - onClick: () => Promise; - }; + name: string; + onClick: () => Promise; + }; interface Props { publicKey: PublicKey | null; @@ -325,10 +327,39 @@ const useProps = (): Props => { try { const signedMessage = await signMessage(provider, message); + const verified = verifyMessage(message, signedMessage, provider.publicKey); + if (!verified) { + throw new Error(`Unable to verify signature ${signedMessage} with public key ${provider.publicKey.toString()}`); + } + createLog({ + status: 'success', + method: 'signMessage', + message: `✅ Message signed and verified: ${JSON.stringify(signedMessage)}`, + }); + return signedMessage; + } catch (error) { + createLog({ + status: 'error', + method: 'signMessage', + message: error.message, + }); + } + }, [createLog]); + + /** SignMessage (Ledger) */ + const handleSignMessageLedger = useCallback(async () => { + if (!provider) return; + + try { + const signedMessage = await signMessage(provider, message); + const verified = verifyOffChainLedgerMessage(message, signedMessage, provider.publicKey); + if (!verified) { + throw new Error(`Unable to verify signature ${signedMessage} with public key ${provider.publicKey.toString()}`); + } createLog({ status: 'success', method: 'signMessage', - message: `Message signed: ${JSON.stringify(signedMessage)}`, + message: `✅ Message signed and verified: ${JSON.stringify(signedMessage)}`, }); return signedMessage; } catch (error) { @@ -396,6 +427,10 @@ const useProps = (): Props => { name: 'Sign Message', onClick: handleSignMessage, }, + { + name: 'Sign Message (Ledger)', + onClick: handleSignMessageLedger, + }, { name: 'Disconnect', onClick: handleDisconnect, @@ -408,6 +443,7 @@ const useProps = (): Props => { handleSignTransaction, handleSignAllTransactions, handleSignMessage, + handleSignMessageLedger, handleDisconnect, ]); diff --git a/src/utils/index.ts b/src/utils/index.ts index ea30eb8..458dadf 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -10,3 +10,4 @@ export { default as signAndSendTransaction } from './signAndSendTransaction'; export { default as signAndSendTransactionV0WithLookupTable } from './signAndSendTransactionV0WithLookupTable'; export { default as signMessage } from './signMessage'; export { default as signTransaction } from './signTransaction'; +export { verifyMessage, verifyOffChainLedgerMessage } from './verifyMessage'; diff --git a/src/utils/serializeLedgerMessage.ts b/src/utils/serializeLedgerMessage.ts new file mode 100644 index 0000000..4fe73e1 --- /dev/null +++ b/src/utils/serializeLedgerMessage.ts @@ -0,0 +1,61 @@ +/** + * Inspired from: + * https://github.com/LedgerHQ/app-solana/blob/bc5e53581de651b33d2ec437dfa468048c27c4b5/examples/example-sign.js + */ + +// Currently only on version 0 for Ledger off chain message signing as of Solana app version 1.3.1 +const OFF_CHAIN_MESSAGE_VERSION = 0; + +// Max off-chain message length supported by Ledger +const OFFCM_MAX_LEDGER_LEN = 1212; +// Max length of version 0 off-chain message +const OFFCM_MAX_V0_LEN = 65515; + +const isPrintableASCII = (message: Buffer): boolean => message.every((el) => el >= 0x20 && el <= 0x7e); + +const parseMessageFormat = (message: Buffer): number | undefined => { + if (message.length <= OFFCM_MAX_LEDGER_LEN) { + if (isPrintableASCII(message)) { + return 0; + } else { + return 1; + } + } else if (message.length <= OFFCM_MAX_V0_LEN) { + return 2; + } + return undefined; +}; + +type SerializeLedgerOffChainMessageResponse = + | { + status: 'success'; + data: Buffer; + } + | { + status: 'error'; + errorType: 'message-too-long'; + errorMessage: string; + }; + +export const serializeLedgerOffChainMessage = (message: Buffer): SerializeLedgerOffChainMessageResponse => { + const messageFormat = parseMessageFormat(message); + + if (messageFormat === undefined) { + return { + status: 'error', + errorType: 'message-too-long', + errorMessage: `off-chain message too long. Max length: ${OFFCM_MAX_V0_LEN}, but got ${message.length}`, + }; + } + + const buffer = Buffer.alloc(4); + let offset = buffer.writeUInt8(OFF_CHAIN_MESSAGE_VERSION); + offset = buffer.writeUInt8(messageFormat, offset); + buffer.writeUInt16LE(message.length, offset); + const data = Buffer.concat([Buffer.from([255]), Buffer.from('solana offchain'), buffer, message]); + + return { + status: 'success', + data, + }; +}; diff --git a/src/utils/signMessage.ts b/src/utils/signMessage.ts index 8c6bca5..677b7fd 100644 --- a/src/utils/signMessage.ts +++ b/src/utils/signMessage.ts @@ -1,4 +1,5 @@ import { PhantomProvider } from '../types'; +import bs58 from 'bs58'; /** * Signs a message @@ -10,7 +11,10 @@ const signMessage = async (provider: PhantomProvider, message: string): Promise< try { const encodedMessage = new TextEncoder().encode(message); const signedMessage = await provider.signMessage(encodedMessage); - return signedMessage; + if ('signature' in signedMessage) { + return bs58.encode(signedMessage.signature); + } + throw new Error(`invalid signature object: ${JSON.stringify(signedMessage)}`); } catch (error) { console.warn(error); throw new Error(error.message); diff --git a/src/utils/verifyMessage.ts b/src/utils/verifyMessage.ts new file mode 100644 index 0000000..3bcf5c6 --- /dev/null +++ b/src/utils/verifyMessage.ts @@ -0,0 +1,29 @@ +import bs58 from 'bs58'; +import { PublicKey } from '@solana/web3.js'; +import nacl from 'tweetnacl'; +import { serializeLedgerOffChainMessage } from './serializeLedgerMessage'; + +/** + * Verifies off-chain message signature from Phantom. + * + * tweetnacl can be used to do signature verification. + */ +export const verifyMessage = (message: string, signature: string, publicKey: PublicKey): boolean => { + const messageBuffer = Buffer.from(message, 'utf-8'); + return nacl.sign.detached.verify(messageBuffer, bs58.decode(signature), Buffer.from(publicKey.toBytes())); +}; + +/** + * Verifies off-chain message signature from Ledger. + * + * Ledger prepends some extra data to the start of the raw message bytes, so we have to re-serialize + * the original message in the same way that it's sent to Ledger. + */ +export const verifyOffChainLedgerMessage = (message: string, signature: string, publicKey: PublicKey): boolean => { + const messageBuffer = Buffer.from(message, 'utf-8'); + const serialized = serializeLedgerOffChainMessage(messageBuffer); + if (serialized.status === 'error') { + throw new Error(`unable to serialize off chain ledger message: ${message}`); + } + return nacl.sign.detached.verify(serialized.data, bs58.decode(signature), Buffer.from(publicKey.toBytes())); +};