Skip to content
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

Add verification for signMessage, support Ledger #33

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"]
}
50 changes: 43 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
signAndSendTransactionV0WithLookupTable,
signMessage,
signTransaction,
verifyMessage,
verifyOffChainLedgerMessage,
} from './utils';

import { TLog } from './types';
Expand Down Expand Up @@ -54,13 +56,13 @@ const message = 'To avoid digital dognappers, sign below to authenticate with Cr

export type ConnectedMethods =
| {
name: string;
onClick: () => Promise<string>;
}
name: string;
onClick: () => Promise<string>;
}
| {
name: string;
onClick: () => Promise<void>;
};
name: string;
onClick: () => Promise<void>;
};

interface Props {
publicKey: PublicKey | null;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -396,6 +427,10 @@ const useProps = (): Props => {
name: 'Sign Message',
onClick: handleSignMessage,
},
{
name: 'Sign Message (Ledger)',
onClick: handleSignMessageLedger,
},
{
name: 'Disconnect',
onClick: handleDisconnect,
Expand All @@ -408,6 +443,7 @@ const useProps = (): Props => {
handleSignTransaction,
handleSignAllTransactions,
handleSignMessage,
handleSignMessageLedger,
handleDisconnect,
]);

Expand Down
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
61 changes: 61 additions & 0 deletions src/utils/serializeLedgerMessage.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
6 changes: 5 additions & 1 deletion src/utils/signMessage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PhantomProvider } from '../types';
import bs58 from 'bs58';

/**
* Signs a message
Expand All @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions src/utils/verifyMessage.ts
Original file line number Diff line number Diff line change
@@ -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()));
};