From 82d0019b1239b128edc58b270feac23e64d1a59a Mon Sep 17 00:00:00 2001 From: KarishmaBothara Date: Mon, 22 Mar 2021 22:21:08 +1300 Subject: [PATCH] Added code to support custom signed extension/user extension (#670) * Added code to support custom signed extension/user extension * fix lint issues * added test * as per review comment * fix lint * as per review suggestion * fix lint * as per review comments * fix lint --- .../src/background/RequestExtrinsicSign.ts | 3 - .../src/background/handlers/Extension.test.ts | 255 +++++++++++++++++- .../src/background/handlers/Extension.ts | 20 ++ packages/extension-chains/src/index.ts | 7 +- packages/extension-inject/src/types.ts | 2 + 5 files changed, 280 insertions(+), 7 deletions(-) diff --git a/packages/extension-base/src/background/RequestExtrinsicSign.ts b/packages/extension-base/src/background/RequestExtrinsicSign.ts index 89bc8174..8b644242 100644 --- a/packages/extension-base/src/background/RequestExtrinsicSign.ts +++ b/packages/extension-base/src/background/RequestExtrinsicSign.ts @@ -15,9 +15,6 @@ export default class RequestExtrinsicSign implements RequestSign { } sign (registry: TypeRegistry, pair: KeyringPair): { signature: string } { - // inject the current signed extensions for encoding - registry.setSignedExtensions(this.payload.signedExtensions); - return registry .createType('ExtrinsicPayload', this.payload, { version: this.payload.version }) .sign(pair); diff --git a/packages/extension-base/src/background/handlers/Extension.test.ts b/packages/extension-base/src/background/handlers/Extension.test.ts index 38d86326..e6dbf1da 100644 --- a/packages/extension-base/src/background/handlers/Extension.test.ts +++ b/packages/extension-base/src/background/handlers/Extension.test.ts @@ -3,15 +3,25 @@ import '../../../../../__mocks__/chrome'; +import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types'; +import type { SignerPayloadJSON } from '@polkadot/types/types'; + +import { ResponseSigning } from '@polkadot/extension-base/background/types'; +import { MetadataDef } from '@polkadot/extension-inject/types'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { TypeRegistry } from '@polkadot/types'; import keyring from '@polkadot/ui-keyring'; import { cryptoWaitReady } from '@polkadot/util-crypto'; import { AccountsStore } from '../../stores'; import Extension from './Extension'; -import State from './State'; +import State, { AuthUrls } from './State'; +import Tabs from './Tabs'; describe('Extension', () => { let extension: Extension; + let state: State; + let tabs: Tabs; const suri = 'seed sock milk update focus rotate barely fade car face mechanic mercy'; const password = 'passw0rd'; @@ -19,8 +29,20 @@ describe('Extension', () => { await cryptoWaitReady(); keyring.loadAll({ store: new AccountsStore() }); + const authUrls: AuthUrls = {}; - return new Extension(new State()); + authUrls['localhost:3000'] = { + count: 0, + id: '11', + isAllowed: true, + origin: 'example.com', + url: 'http://localhost:3000' + }; + localStorage.setItem('authUrls', JSON.stringify(authUrls)); + state = new State(); + tabs = new Tabs(state); + + return new Extension(state); } const createAccount = async (): Promise => { @@ -142,4 +164,233 @@ describe('Extension', () => { }).toThrowError('Unable to decode using the supplied passphrase'); }); }); + + describe('custom user extension', () => { + let address: string, payload: SignerPayloadJSON, pair: KeyringPair; + + beforeEach(async () => { + address = await createAccount(); + pair = keyring.getPair(address); + pair.decodePkcs8(password); + payload = { + address, + blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80', + blockNumber: '0x00000393', + era: '0x3601', + genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62', + method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100', + nonce: '0x0000000000000000', + signedExtensions: ['CheckSpecVersion', 'CheckTxVersion', 'CheckGenesis', 'CheckMortality', 'CheckNonce', 'CheckWeight', 'ChargeTransactionPayment'], + specVersion: '0x00000026', + tip: null, + transactionVersion: '0x00000005', + version: 4 + } as unknown as SignerPayloadJSON; + }); + + test('signs with default signed extensions', async () => { + const registry = new TypeRegistry(); + + registry.setSignedExtensions(payload.signedExtensions); + + const signatureExpected = registry + .createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair); + + tabs.handle('1615191860871.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port) + .then((result) => { + expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature); + }).catch((err) => console.log(err)); + + await expect(extension.handle('1615192072290.7', 'pri(signing.approve.password)', { + id: state.allSignRequests[0].id, + password, + savePass: false + }, {} as chrome.runtime.Port)).resolves.toEqual(true); + }); + + test('signs with user extensions, known types', async () => { + const types = {} as unknown as Record; + + const userExtensions = { + MyUserExtension: { + extrinsic: { + assetId: 'AssetId' + }, + payload: {} + } + } as unknown as ExtDef; + + const meta: MetadataDef = { + chain: 'Development', + color: '#191a2e', + genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62', + icon: '', + specVersion: 38, + ss58Format: 0, + tokenDecimals: 12, + tokenSymbol: '', + types, + userExtensions + }; + + state.saveMetadata(meta); + + const payload = { + address, + blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80', + blockNumber: '0x00000393', + era: '0x3601', + genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62', + method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100', + nonce: '0x0000000000000000', + signedExtensions: ['MyUserExtension'], + specVersion: '0x00000026', + tip: null, + transactionVersion: '0x00000005', + version: 4 + } as unknown as SignerPayloadJSON; + + const registry = new TypeRegistry(); + + registry.setSignedExtensions(payload.signedExtensions, userExtensions); + registry.register(types); + + const signatureExpected = registry + .createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair); + + tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port) + .then((result) => { + expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature); + }).catch((err) => console.log(err)); + + await expect(extension.handle('1615192062290.7', 'pri(signing.approve.password)', { + id: state.allSignRequests[0].id, + password, + savePass: false + }, {} as chrome.runtime.Port)).resolves.toEqual(true); + }); + + test('override default signed extension', async () => { + const types = { + FeeExchangeV1: { + assetId: 'Compact', + maxPayment: 'Compact' + }, + PaymentOptions: { + feeExchange: 'FeeExchangeV1', + tip: 'Compact' + } + } as unknown as Record; + + const userExtensions = { + ChargeTransactionPayment: { + extrinsic: { + transactionPayment: 'PaymentOptions' + }, + payload: {} + } + } as unknown as ExtDef; + + const meta: MetadataDef = { + chain: 'Development', + color: '#191a2e', + genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62', + icon: '', + specVersion: 38, + ss58Format: 0, + tokenDecimals: 12, + tokenSymbol: '', + types, + userExtensions + }; + + state.saveMetadata(meta); + + const registry = new TypeRegistry(); + + registry.setSignedExtensions(payload.signedExtensions, userExtensions); + registry.register(types); + + const signatureExpected = registry + .createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair); + + tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port) + .then((result) => { + expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature); + }).catch((err) => console.log(err)); + + await expect(extension.handle('1615192062290.7', 'pri(signing.approve.password)', { + id: state.allSignRequests[0].id, + password, + savePass: false + }, {} as chrome.runtime.Port)).resolves.toEqual(true); + }); + + test('signs with user extensions, additional types', async () => { + const types = { + myCustomType: { + feeExchange: 'Compact', + tip: 'Compact' + } + } as unknown as Record; + + const userExtensions = { + MyUserExtension: { + extrinsic: { + myCustomType: 'myCustomType' + }, + payload: {} + } + } as unknown as ExtDef; + + const meta: MetadataDef = { + chain: 'Development', + color: '#191a2e', + genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62', + icon: '', + specVersion: 38, + ss58Format: 0, + tokenDecimals: 12, + tokenSymbol: '', + types, + userExtensions + }; + + state.saveMetadata(meta); + + const payload = { + address, + blockHash: '0xe1b1dda72998846487e4d858909d4f9a6bbd6e338e4588e5d809de16b1317b80', + blockNumber: '0x00000393', + era: '0x3601', + genesisHash: '0x242a54b35e1aad38f37b884eddeb71f6f9931b02fac27bf52dfb62ef754e5e62', + method: '0x040105fa8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4882380100', + nonce: '0x0000000000000000', + signedExtensions: ['MyUserExtension', 'CheckTxVersion', 'CheckGenesis', 'CheckMortality', 'CheckNonce', 'CheckWeight', 'ChargeTransactionPayment'], + specVersion: '0x00000026', + tip: null, + transactionVersion: '0x00000005', + version: 4 + } as unknown as SignerPayloadJSON; + + const registry = new TypeRegistry(); + + registry.setSignedExtensions(payload.signedExtensions, userExtensions); + registry.register(types); + + const signatureExpected = registry + .createType('ExtrinsicPayload', payload, { version: payload.version }).sign(pair); + + tabs.handle('1615191860771.5', 'pub(extrinsic.sign)', payload, 'http://localhost:3000', {} as chrome.runtime.Port) + .then((result) => { + expect((result as ResponseSigning)?.signature).toEqual(signatureExpected.signature); + }).catch((err) => console.log(err)); + + await expect(extension.handle('1615192062290.7', 'pri(signing.approve.password)', { + id: state.allSignRequests[0].id, + password, + savePass: false + }, {} as chrome.runtime.Port)).resolves.toEqual(true); + }); + }); }); diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index f929af80..ec30c184 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -3,6 +3,7 @@ import type { MetadataDef } from '@polkadot/extension-inject/types'; import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '@polkadot/keyring/types'; +import type { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types'; import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; import type { AccountJson, AllowedPath, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountBatchExport, RequestAccountChangePassword, RequestAccountCreateExternal, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked, ResponseType, SigningRequest } from '../types'; @@ -33,6 +34,10 @@ function transformAccounts (accounts: SubjectInfo): AccountJson[] { })); } +function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is SignerPayloadJSON { + return (value as SignerPayloadJSON).genesisHash !== undefined; +} + export default class Extension { readonly #cachedUnlocks: CachedUnlocks; @@ -349,6 +354,21 @@ export default class Extension { pair.decodePkcs8(password); } + const { payload } = request; + + if (isJsonPayload(payload)) { + // Get the metadata for the genesisHash + const currentMetadata = this.#state.knownMetadata.find((meta: MetadataDef) => + meta.genesisHash === payload.genesisHash); + + // set the registry before calling the sign function + registry.setSignedExtensions(payload.signedExtensions, currentMetadata?.userExtensions); + + if (currentMetadata) { + registry.register(currentMetadata?.types); + } + } + const result = request.sign(registry, pair); if (savePass) { diff --git a/packages/extension-chains/src/index.ts b/packages/extension-chains/src/index.ts index 454d6d00..2838b1ce 100644 --- a/packages/extension-chains/src/index.ts +++ b/packages/extension-chains/src/index.ts @@ -24,7 +24,7 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha return cached; } - const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types } = definition; + const { chain, genesisHash, icon, metaCalls, specVersion, ss58Format, tokenDecimals, tokenSymbol, types, userExtensions } = definition; const registry = new TypeRegistry(); if (!isPartial) { @@ -43,7 +43,10 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha if (metaCalls && !isPartial) { hasMetadata = true; - registry.setMetadata(new Metadata(registry, base64Decode(metaCalls))); + const metadata = new Metadata(registry, base64Decode(metaCalls)); + const signedExtensions = metadata.asLatest.extrinsic.signedExtensions.toJSON() as string[]; + + registry.setMetadata(metadata, signedExtensions, userExtensions); } const result = { diff --git a/packages/extension-inject/src/types.ts b/packages/extension-inject/src/types.ts index d463d824..c290a5b6 100644 --- a/packages/extension-inject/src/types.ts +++ b/packages/extension-inject/src/types.ts @@ -3,6 +3,7 @@ import type { Signer as InjectedSigner } from '@polkadot/api/types'; import type { ProviderInterface } from '@polkadot/rpc-provider/types'; +import type { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types'; import type { KeypairType } from '@polkadot/util-crypto/types'; // eslint-disable-next-line no-undef @@ -62,6 +63,7 @@ export interface MetadataDef extends MetadataDefBase { tokenSymbol: string; types: Record | string>; metaCalls?: string; + userExtensions?: ExtDef; } export interface InjectedMetadataKnown {