diff --git a/packages/sdk/wallets/wallet-ledger/package.json b/packages/sdk/wallets/wallet-ledger/package.json index eec80ef06..98674c059 100644 --- a/packages/sdk/wallets/wallet-ledger/package.json +++ b/packages/sdk/wallets/wallet-ledger/package.json @@ -42,6 +42,7 @@ "@celo/contractkit": "^8.0.0", "@ledgerhq/hw-transport-node-hid": "^6.28.5", "@noble/hashes": "^1.3.3", + "@noble/curves": "^1.4.0", "patch-package": "^8.0.0", "ts-node": "^10.9.2", "web3": "1.10.4" diff --git a/packages/sdk/wallets/wallet-ledger/src/data.ts b/packages/sdk/wallets/wallet-ledger/src/data.ts index aa53a1aa1..cd9bb6b1d 100644 --- a/packages/sdk/wallets/wallet-ledger/src/data.ts +++ b/packages/sdk/wallets/wallet-ledger/src/data.ts @@ -1 +1,16 @@ export default 'AAAAaARDRUxPRx7ON1DaI3+TuOM5xTaYm4l4pDgAAAASAACk7DBFAiEA5rECRg94+fCoIvoG9/5qWh62zl2C6Y+aFuuZrFe4CtcCIEJbRrkL3gqwT/Jj+7L3neazgpVCCTZZ3HX9JXXg5vleAAAAaARjVVNEdl3oFoRYYedaJfyhIrtomLixKCoAAAASAACk7DBFAiEApwQFHNBKXp+V2jq8BMD2y/5AwC9bhPQ2H4hT/vMl/B4CIFalOVtBFGREUKMU/F5vDlJLeQrTn6GQeDertpB2FpMvAAAAaARjRVVS2HY8uidqNzjm3oW0s79f3tbWynMAAAASAACk7DBFAiEAh2UeP1+SI2Ed5SiAjpJF6MkMrVa94gUwjJztyBlzhWMCIHfaOrEsxdxAGx+P+hxuSNO4zcw6KRLfJkkuic1V/CrHAAAAagZiIENFTE/dyb5X9VP+dXUtYWBrlMvX4CZO+AAAABIAAPNwMEUCIQCi62KsBfuNcfX0MriiRZ7a5DKERhtIz7sZ1SqBT7ruhgIgVrfmavyWzxzDW4AQeHn++A4qPjB1pQKoHvNXo8Hf1SMAAABpBmIgY1VTRGJJKmRKWI/ZBCcL7QatUrmr/qGuAAAAEgAA83AwRAIgGDYx4oB/gkYUqLeXqvEZXx9nOxVHzTe2ajyd2wnehxgCICQBe/rBPcXiaQJj3pdoXxroct/hV6r3G2G7y79EOEAPAAAAaQZiIGNFVVL57OMBJHrSziGJSUGDCiRw9Od0ygAAABIAAPNwMEQCIEdcFWP+HxEUoF1sCGVd34QGS0hL5cVUdrWdqVm3bYTgAiBCMA+Rg3Ubc3xla/35wzZesPlbeSMEPcr4uqL+8PeydwAAAGoGYSBDRUxP8ZSv31CwPmm9fQV8GqnhDJlU5MkAAAASAACu8zBFAiEAk/o0FBus2/QCrunFGEyoneQIRaMRC+y5L6Dvar8MU/kCIByJt2ziRhDG3AAbyXBIuJfZQujSHFcSJL3xF0xIlcPdAAAAaQZhIGNVU0SHQGn6HrFtRNYi8uDKJe6hcjabwQAAABIAAK7zMEQCIClrH2xgE3WMbD+hgQ7t5SiAcVG5WiUZ655voqCszKEoAiA/cO8UVgNY891MNJ5yeDk8w47WO0E1DQecrK71LR8g8gAAAGoGYSBjRVVSEMiSpuxDpT5F0LkWtLfTg7G3jA8AAAASAACu8zBFAiEAgpktbB1ZxyAwMJwKTSbZ30n8zgRuW0twbXoZxlsUAswCIHek4l4CIbjVMG2HVr0Ml9/8kA4F9dr69JBMaoSUkdKl' + +// How did we get this? By following these steps: +// 1 - get pubkey from https://github.com/blooo-io/app-celo-spender/pull/7/files#diff-e0cf5b28d9b6b600f0af2bc78e8fd30ec675fd731a5da86f0c4283ffc0e40176L75-L83 +// 2 - now you've got to trust me +// 3 - run `ASN1_PREFIX=3056301006072a8648ce3d020106052b8104000a034200` +// 4 - remove spaces, colons, 0xs from the pubkey of step 1, I've done that for you :) +// 5 - and store it in key `KEY=04b06cf5d8f7ed71d8bd9b9dc37944a1c6d240f69bb0be3621dddbb6ac0eccd1508bcc2ea46227e43b941e2c6f1b1cd0ae68e54b185e2cabef3455580604bd45b8` +// 5 - finally run `echo $ASN1_PREFIX$KEY | xxd -r -p - | openssl ec -inform der -pubin -pubout` +// 6 - enjoy +export const legacyLedgerPublicKeyHex = [ + `-----BEGIN PUBLIC KEY-----`, + `MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEsGz12Pftcdi9m53DeUShxtJA9puwvjYh`, + `3du2rA7M0VCLzC6kYifkO5QeLG8bHNCuaOVLGF4sq+80VVgGBL1FuA==`, + `-----END PUBLIC KEY-----`, +].join('\n') diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts index b3fc1de79..b946bff1e 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts @@ -12,7 +12,6 @@ import { AddressValidation, LedgerWallet } from './ledger-wallet' import { legacyTokenInfoByAddressAndChainId, tokenInfoByAddressAndChainId } from './tokens' const debug = debugFactory('kit:wallet:ledger') -const CELO_APP_ACCEPTS_CONTRACT_DATA_FROM_VERSION = '1.0.2' /** * Signs the EVM transaction with a Ledger device @@ -171,7 +170,7 @@ export class LedgerSigner implements Signer { const version = new SemVer(this.appConfiguration.version) if (meetsVersionRequirements(version, { minimum: LedgerWallet.MIN_VERSION_TOKEN_DATA })) { const getTokenInfo = meetsVersionRequirements(version, { - minimum: LedgerWallet.MIN_VERSION_EIP159, + minimum: LedgerWallet.MIN_VERSION_EIP1559, }) ? tokenInfoByAddressAndChainId : legacyTokenInfoByAddressAndChainId diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts index 928f793a4..1ef15b59d 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts @@ -20,7 +20,12 @@ import { import * as ethUtil from '@ethereumjs/util' import Ledger from '@ledgerhq/hw-app-eth' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import { VerifyPublicKeyInput, createVerify } from 'crypto' +import { readFileSync } from 'fs' +import { dirname, join } from 'path' import Web3 from 'web3' +import { legacyLedgerPublicKeyHex } from './data' +import { meetsVersionRequirements } from './ledger-utils' import { AddressValidation, LedgerWallet } from './ledger-wallet' // Update this variable when testing using a physical device @@ -116,7 +121,7 @@ interface ILedger { } const mockLedgerImplementation = (mockForceValidation: () => void, version: string): ILedger => { - return { + const _ledger = { getAddress: async (derivationPath: string, forceValidation?: boolean) => { if (forceValidation) { mockForceValidation() @@ -197,15 +202,55 @@ const mockLedgerImplementation = (mockForceValidation: () => void, version: stri starkv2Supported: 1, } }, - provideERC20TokenInformation: async (_token) => { - return true + provideERC20TokenInformation: async (tokenData: string) => { + let pubkey: VerifyPublicKeyInput + const version = (await _ledger.getAppConfiguration()).version + if ( + meetsVersionRequirements(version, { + minimum: LedgerWallet.MIN_VERSION_EIP1559, + }) + ) { + // verify with new pubkey + const pubDir = dirname(require.resolve('@celo/ledger-token-signer')) + pubkey = { key: readFileSync(join(pubDir, 'pubkey.pem')).toString() } + } else { + // verify with oldpubkey + pubkey = { key: legacyLedgerPublicKeyHex } + } + + const verify = createVerify('sha256') + const tokenDataBuf = Buffer.from(tokenData, 'hex') + const BASE_DATA_LENGTH = + 20 + // contract address, 20 bytes + 4 + // decimals, uint32, 4 bytes + 4 // chainId, uint32, 4 bytes + // first byte of data is the ticker length, so we add that to base data length + const dataLen = BASE_DATA_LENGTH + tokenDataBuf.readInt8(0) + // start at 1 since the first byte was just informative + const data = tokenDataBuf.slice(1, dataLen + 1) + verify.update(data) + verify.end() + // read from end of data til the end + const signature = tokenDataBuf.slice(dataLen + 1) + const verified = verify.verify(pubkey, signature) + + if (!verified) { + throw new Error('couldnt verify data sent to MockLedger') + } + return verified }, } + return _ledger } -function mockLedger(wallet: LedgerWallet, mockForceValidation: () => void, version = '1.2.0') { +function mockLedger( + wallet: LedgerWallet, + mockForceValidation: () => void, + version = LedgerWallet.MIN_VERSION_EIP1559 +) { jest .spyOn(wallet, 'generateNewLedger') + .mockClear() .mockImplementation((_transport: any): ILedger => { return mockLedgerImplementation(mockForceValidation, version) }) @@ -394,7 +439,7 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, '1.0.0') + mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) await wallet.init() expect( @@ -418,7 +463,7 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, '1.0.0') + mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) await wallet.init() const warnSpy = jest.spyOn(console, 'warn') // setup complete @@ -522,6 +567,7 @@ describe('LedgerWallet class', () => { describe('[celo-legacy]', () => { beforeEach(async () => { + const kit = newKit('https://alfajores-forno.celo-testnet.org') celoTransaction = { from: knownAddress, to: otherAddress, @@ -530,7 +576,7 @@ describe('LedgerWallet class', () => { nonce: 0, gas: 99, gasPrice: 99, - feeCurrency: '0xD8763CBa276a3738E6DE85b4b3bF5FDed6D6cA73', + feeCurrency: (await kit.contracts.getStableToken(StableToken.cUSD)).address, } }) describe('with old ledger app', () => { @@ -546,7 +592,7 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, '1.0.0') + mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) await wallet.init() }) @@ -576,7 +622,7 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, '1.0.0') + mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) await wallet.init() const warnSpy = jest.spyOn(console, 'warn') // setup complete @@ -584,15 +630,15 @@ describe('LedgerWallet class', () => { await expect(wallet.signTransaction(celoTransaction)).resolves .toMatchInlineSnapshot(` { - "raw": "0xf87f80636394d8763cba276a3738e6de85b4b3bf5fded6d6ca73808094588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a76400008083015e09a09d8307331534bc7839f055dcaab64d991057da094b021466c734f26b08c3e1f4a02332ec85ff2889e1a05590a97fe0c429fadb2c4260af4dea2b6b69a26c4d60e0", + "raw": "0xf87f80636394874069fa1eb16d44d622f2e0ca25eea172369bc1808094588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a76400008083015e09a024c4b1d027c50d2e847d371cd902d3e22c9fa10fcbd59e9c5a854282afed34daa0686180d75830ea223c3ed1ca12613d029bc3613b4b5b51a724b33067491c2398", "tx": { - "feeCurrency": "0xd8763cba276a3738e6de85b4b3bf5fded6d6ca73", + "feeCurrency": "0x874069fa1eb16d44d622f2e0ca25eea172369bc1", "gas": "0x63", - "hash": "0xc0467a86cae1f1a526899e450764c747777dc146f6c021edbe9021c13e1e189b", + "hash": "0x302b12585b4a17317e9affcbe0abd0cd8cd39ef402108625085485b1c1d92d71", "input": "0x", "nonce": "0", - "r": "0x9d8307331534bc7839f055dcaab64d991057da094b021466c734f26b08c3e1f4", - "s": "0x2332ec85ff2889e1a05590a97fe0c429fadb2c4260af4dea2b6b69a26c4d60e0", + "r": "0x24c4b1d027c50d2e847d371cd902d3e22c9fa10fcbd59e9c5a854282afed34da", + "s": "0x686180d75830ea223c3ed1ca12613d029bc3613b4b5b51a724b33067491c2398", "to": "0x588e4b68193001e4d10928660ab4165b813717c0", "v": "0x015e09", "value": "0x0de0b6b3a7640000", diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts index 465111fdb..1fa49baf9 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts @@ -55,7 +55,7 @@ const debug = debugFactory('kit:wallet:ledger') export class LedgerWallet extends RemoteWallet implements ReadOnlyWallet { static MIN_VERSION_SUPPORTED = '1.0.0' static MIN_VERSION_TOKEN_DATA = '1.0.2' - static MIN_VERSION_EIP159 = '1.2.0' + static MIN_VERSION_EIP1559 = '1.2.0' ledger: Ledger | undefined /** @@ -104,10 +104,10 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly const version = new SemVer(deviceApp.version) // if the app is of minimum version it doesnt matter if chain is cel2 or not - if (meetsVersionRequirements(version, { minimum: LedgerWallet.MIN_VERSION_EIP159 })) { + if (meetsVersionRequirements(version, { minimum: LedgerWallet.MIN_VERSION_EIP1559 })) { if (txParams.gasPrice && txParams.feeCurrency && txParams.feeCurrency !== '0x') { throw new Error( - `celo ledger app above ${LedgerWallet.MIN_VERSION_EIP159} cannot serialize legacy celo transactions. Replace "gasPrice" with "maxFeePerGas".` + `celo ledger app above ${LedgerWallet.MIN_VERSION_EIP1559} cannot serialize legacy celo transactions. Replace "gasPrice" with "maxFeePerGas".` ) } if (txParams.gasPrice) { @@ -129,12 +129,12 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly // but if not celo as layer 2 and as layer 1 are different } else if (this.isCel2) { throw new Error( - `celo ledger app version must be at least ${LedgerWallet.MIN_VERSION_EIP159} to sign transactions supported on celo after the L2 upgrade` + `celo ledger app version must be at least ${LedgerWallet.MIN_VERSION_EIP1559} to sign transactions supported on celo after the L2 upgrade` ) } else { // the l1 legacy case console.warn( - `Upgrade your celo ledger app to at least ${LedgerWallet.MIN_VERSION_EIP159} before cel2 transition` + `Upgrade your celo ledger app to at least ${LedgerWallet.MIN_VERSION_EIP1559} before cel2 transition` ) if (!txParams.gasPrice) { // this version of app only supports legacy so must have gasPrice diff --git a/packages/sdk/wallets/wallet-ledger/src/tokens.ts b/packages/sdk/wallets/wallet-ledger/src/tokens.ts index b16209404..02f1f3eae 100644 --- a/packages/sdk/wallets/wallet-ledger/src/tokens.ts +++ b/packages/sdk/wallets/wallet-ledger/src/tokens.ts @@ -41,50 +41,42 @@ function generateContractKey(contract: Address, chainId: number): string { return [normalizeAddressWith0x(contract), chainId].join('-') } -// this internal get() will lazy load and cache the data from the erc20 data blob -const get: (data: string) => API = (() => { - let cache: API - return (data) => { - if (cache) { - return cache +const get = (data: string): API => { + const buf = Buffer.from(data, 'base64') + const byContract: { [id: string]: TokenInfo } = {} + const entries: TokenInfo[] = [] + let i = 0 + while (i < buf.length) { + const length = buf.readUInt32BE(i) + i += 4 + const item = buf.slice(i, i + length) + let j = 0 + const tickerLength = item.readUInt8(j) + j += 1 + const ticker = item.slice(j, j + tickerLength).toString('ascii') + j += tickerLength + const contractAddress: string = normalizeAddressWith0x(item.slice(j, j + 20).toString('hex')) + j += 20 + const decimals = item.readUInt32BE(j) + j += 4 + const chainId = item.readUInt32BE(j) + j += 4 + const signature = item.slice(j) + const entry: TokenInfo = { + ticker, + contractAddress, + decimals, + chainId, + signature, + data: item, } - const buf = Buffer.from(data, 'base64') - const byContract: { [id: string]: TokenInfo } = {} - const entries: TokenInfo[] = [] - let i = 0 - while (i < buf.length) { - const length = buf.readUInt32BE(i) - i += 4 - const item = buf.slice(i, i + length) - let j = 0 - const tickerLength = item.readUInt8(j) - j += 1 - const ticker = item.slice(j, j + tickerLength).toString('ascii') - j += tickerLength - const contractAddress: string = normalizeAddressWith0x(item.slice(j, j + 20).toString('hex')) - j += 20 - const decimals = item.readUInt32BE(j) - j += 4 - const chainId = item.readUInt32BE(j) - j += 4 - const signature = item.slice(j) - const entry: TokenInfo = { - ticker, - contractAddress, - decimals, - chainId, - signature, - data: item, - } - entries.push(entry) - byContract[generateContractKey(contractAddress, chainId)] = entry - i += length - } - const api = { - list: () => entries, - byContractKey: (id: string) => byContract[id], - } - cache = api - return api + entries.push(entry) + byContract[generateContractKey(contractAddress, chainId)] = entry + i += length } -})() + const api = { + list: () => entries, + byContractKey: (id: string) => byContract[id], + } + return api +}