From f6e83827681b3ff08e8e43bb02eb59fe23591a4f Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Fri, 20 Dec 2024 13:02:13 +0100 Subject: [PATCH] test(e2e): test signData with drepID --- packages/e2e/package.json | 1 + .../PersonalWallet/cip30WalletApi.test.ts | 119 ++++++++++++++++++ .../e2e/test/web-extension/extension/const.ts | 2 - .../e2e/test/web-extension/extension/ui.html | 2 - .../e2e/test/web-extension/extension/ui.ts | 21 ---- .../test/web-extension/specs/wallet.spec.ts | 9 +- .../hardware-ledger/src/LedgerKeyAgent.ts | 45 ++++--- .../hardware/ledger/LedgerKeyAgent.test.ts | 21 ++++ yarn.lock | 1 + 9 files changed, 172 insertions(+), 49 deletions(-) create mode 100644 packages/e2e/test/wallet_epoch_0/PersonalWallet/cip30WalletApi.test.ts diff --git a/packages/e2e/package.json b/packages/e2e/package.json index a9ad039dcb9..f66d86a82ff 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -90,6 +90,7 @@ "@cardano-sdk/util-rxjs": "workspace:~", "@cardano-sdk/wallet": "workspace:~", "@dcspark/cardano-multiplatform-lib-nodejs": "^3.1.1", + "@emurgo/cardano-message-signing-nodejs": "^1.0.1", "@shiroyasha9/axios-fetch-adapter": "1.0.3", "axios": "^1.7.4", "bunyan": "^1.8.15", diff --git a/packages/e2e/test/wallet_epoch_0/PersonalWallet/cip30WalletApi.test.ts b/packages/e2e/test/wallet_epoch_0/PersonalWallet/cip30WalletApi.test.ts new file mode 100644 index 00000000000..2ff251147e0 --- /dev/null +++ b/packages/e2e/test/wallet_epoch_0/PersonalWallet/cip30WalletApi.test.ts @@ -0,0 +1,119 @@ +import * as Crypto from '@cardano-sdk/crypto'; +import { BaseWallet, cip30 } from '@cardano-sdk/wallet'; +import { Bip32Account, KeyRole, cip8 } from '@cardano-sdk/key-management'; +import { COSEKey, COSESign1 } from '@emurgo/cardano-message-signing-nodejs'; +import { Cardano, util } from '@cardano-sdk/core'; +import { Cip30DataSignature, SenderContext } from '@cardano-sdk/dapp-connector'; +import { HexBlob } from '@cardano-sdk/util'; +import { NEVER, firstValueFrom, of } from 'rxjs'; +import { buildDRepAddressFromDRepKey } from '../../../../wallet/test/util'; +import { getEnv, getWallet, walletReady, walletVariables } from '../../../src'; +import { logger } from '@cardano-sdk/util-dev'; + +const env = getEnv(walletVariables); + +const decodeSignature = (dataSignature: Cip30DataSignature) => { + const coseKey = COSEKey.from_bytes(Buffer.from(dataSignature.key, 'hex')); + const coseSign1 = COSESign1.from_bytes(Buffer.from(dataSignature.signature, 'hex')); + + const publicKeyHeader = coseKey.header(cip8.CoseLabel.x)!; + const publicKeyBytes = publicKeyHeader.as_bytes()!; + const publicKeyHex = util.bytesToHex(publicKeyBytes); + const signedData = coseSign1.signed_data(); + return { coseKey, coseSign1, publicKeyHex, signedData }; +}; + +describe('PersonalWallet/cip30WalletApi', () => { + let wallet: BaseWallet; + let drepKeyHashHex: Crypto.Ed25519KeyHashHex; + let drepPubKey: Crypto.Ed25519PublicKeyHex; + let walletApi: ReturnType; + let bip32Account: Bip32Account; + + beforeEach(async () => { + ({ wallet, bip32Account } = await getWallet({ env, logger, name: 'wallet' })); + await walletReady(wallet, 10n); + + drepPubKey = (await wallet.governance.getPubDRepKey())!; + drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(drepPubKey!).hash()).hex(); + + walletApi = cip30.createWalletApi( + of(wallet), + { + signData: () => Promise.resolve({ cancel$: NEVER }) + } as unknown as cip30.CallbackConfirmation, + { logger: console } + ); + }); + + it('can signData with hex DRepID', async () => { + const signature = await walletApi.signData( + { sender: '' } as unknown as SenderContext, + drepKeyHashHex, + HexBlob('abc123') + ); + + expect(decodeSignature(signature).publicKeyHex).toEqual(drepPubKey); + }); + + it('can signData with bech32 type 6 addr DRepID', async () => { + const drepAddr = (await buildDRepAddressFromDRepKey(drepPubKey))?.toAddress()?.toBech32(); + const signature = await walletApi.signData( + { sender: '' } as unknown as SenderContext, + drepAddr!, + HexBlob('abc123') + ); + + expect(decodeSignature(signature).publicKeyHex).toEqual(drepPubKey); + }); + + it('can signData with bech32 base address', async () => { + const [{ address, index }] = await firstValueFrom(wallet.addresses$); + const paymentKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.External })).hex(); + + const signature = await walletApi.signData({ sender: '' } as unknown as SenderContext, address, HexBlob('abc123')); + + expect(decodeSignature(signature).publicKeyHex).toEqual(paymentKeyHex); + }); + + it('can signData with hex-encoded base address', async () => { + const [{ address, index }] = await firstValueFrom(wallet.addresses$); + const addressHex = Cardano.Address.fromBech32(address).toBytes(); + const paymentKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.External })).hex(); + + const signature = await walletApi.signData( + { sender: '' } as unknown as SenderContext, + addressHex, + HexBlob('abc123') + ); + + expect(decodeSignature(signature).publicKeyHex).toEqual(paymentKeyHex); + }); + + it('can signData with bech32 base address', async () => { + const [{ rewardAccount, index }] = await firstValueFrom(wallet.addresses$); + const stakeKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.Stake })).hex(); + + const signature = await walletApi.signData( + { sender: '' } as unknown as SenderContext, + rewardAccount, + HexBlob('abc123') + ); + + expect(decodeSignature(signature).publicKeyHex).toEqual(stakeKeyHex); + }); + + it('can signData with hex-encoded reward account', async () => { + const [{ rewardAccount, index }] = await firstValueFrom(wallet.addresses$); + const rewardAccountHex = Cardano.Address.fromBech32(rewardAccount).toBytes(); + const stakeKeyHex = (await bip32Account.derivePublicKey({ index, role: KeyRole.Stake })).hex(); + + const signature = await walletApi.signData( + { sender: '' } as unknown as SenderContext, + rewardAccountHex, + HexBlob('abc123') + ); + + expect(decodeSignature(signature).publicKeyHex).toEqual(stakeKeyHex); + }); +}); diff --git a/packages/e2e/test/web-extension/extension/const.ts b/packages/e2e/test/web-extension/extension/const.ts index d23a0b5159a..2649634dbef 100644 --- a/packages/e2e/test/web-extension/extension/const.ts +++ b/packages/e2e/test/web-extension/extension/const.ts @@ -17,12 +17,10 @@ export const selectors = { btnDelegate: '#multiDelegation .delegate button', btnGrantAccess: '#requestAccessGrant', btnSignAndBuildTx: '#buildAndSignTx', - btnSignDataWithDRepId: '#signDataWithDRepId', deactivateWallet: '#deactivateWallet', destroyWallet: '#destroyWallet', divAdaPrice: '#adaPrice', divBgPortDisconnectStatus: '#remoteApiPortDisconnect .bgPortDisconnect', - divDataSignature: '#dataSignature', divSignature: '#signature', divUiPortDisconnectStatus: '#remoteApiPortDisconnect .uiPortDisconnect', liPercents: '#multiDelegation .distribution li .percent', diff --git a/packages/e2e/test/web-extension/extension/ui.html b/packages/e2e/test/web-extension/extension/ui.html index 77f2b2931cf..e616e68a1c9 100644 --- a/packages/e2e/test/web-extension/extension/ui.html +++ b/packages/e2e/test/web-extension/extension/ui.html @@ -45,8 +45,6 @@

Delegation distribution:

Signature: -
- -
Signature: -
diff --git a/packages/e2e/test/web-extension/extension/ui.ts b/packages/e2e/test/web-extension/extension/ui.ts index b1ab310b086..01fde282e6c 100644 --- a/packages/e2e/test/web-extension/extension/ui.ts +++ b/packages/e2e/test/web-extension/extension/ui.ts @@ -124,23 +124,6 @@ const sendDelegationTx = async (portfolio: { pool: Cardano.StakePool; weight: nu document.querySelector('#multiDelegation .delegateTxId')!.textContent = msg; }; -const signDataWithDRepID = async (): Promise => { - let msg: string; - const dRepId = 'drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz'; - try { - const signature = await wallet.signData({ - payload: HexBlob('abc123'), - signWith: Cardano.DRepID(dRepId) - }); - msg = JSON.stringify(signature); - } catch (error) { - msg = `ERROR signing data with DRepID: ${JSON.stringify(error)}`; - } - - // Set text with signature or error - document.querySelector(selectors.divDataSignature)!.textContent = msg; -}; - const setAddresses = ({ address, stakeAddress }: { address: string; stakeAddress: string }): void => { document.querySelector(selectors.spanAddress)!.textContent = address; document.querySelector(selectors.spanStakeAddress)!.textContent = stakeAddress; @@ -378,10 +361,6 @@ document.querySelector(selectors.btnSignAndBuildTx)!.addEventListener('click', a setSignature(signedTx.witness.signatures.values().next().value); }); -document - .querySelector(selectors.btnSignDataWithDRepId)! - .addEventListener('click', async () => await signDataWithDRepID()); - // Code below tests that a disconnected port in background script will result in the consumed API method call promise to reject // UI consumes API -> BG exposes fake API that closes port const disconnectPortTestObj = consumeRemoteApi( diff --git a/packages/e2e/test/web-extension/specs/wallet.spec.ts b/packages/e2e/test/web-extension/specs/wallet.spec.ts index 55b27ff646b..cc861a7d19f 100644 --- a/packages/e2e/test/web-extension/specs/wallet.spec.ts +++ b/packages/e2e/test/web-extension/specs/wallet.spec.ts @@ -23,9 +23,7 @@ describe('wallet', () => { liPools, liPercents, divBgPortDisconnectStatus, - divUiPortDisconnectStatus, - btnSignDataWithDRepId, - divDataSignature + divUiPortDisconnectStatus } = selectors; // The address is filled in by the tests, which are order dependent @@ -137,11 +135,6 @@ describe('wallet', () => { await buildAndSign(); }); - it('can sign data with a DRepID', async () => { - await (await $(btnSignDataWithDRepId)).click(); - await expect($(divDataSignature)).toHaveTextContaining('signature'); - }); - it('can destroy second wallet before switching back to the first wallet', async () => { // Destroy also clears associated store. Store will be rebuilt during future activation of same wallet await $(destroyWallet).click(); diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index 552ceebd26d..fd3f47acafc 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -209,26 +209,17 @@ type OpenTransportForDeviceParams = { device: LedgerDevice; }; +const isPaymentAddress = ( + signWith: Cardano.PaymentAddress | Cardano.RewardAccount +): signWith is Cardano.PaymentAddress => signWith.startsWith('addr'); + const getDerivationPath = ( signWith: Cardano.PaymentAddress | Cardano.RewardAccount, knownAddresses: GroupedAddress[], accountIndex: number, - purpose: number + purpose: number, + dRepKeyHash: Crypto.Ed25519KeyHashHex ): { signingPath: BIP32Path; addressParams: DeviceOwnedAddress } => { - if (Cardano.DRepID.isValid(signWith)) { - const path = util.accountKeyDerivationPathToBip32Path(accountIndex, util.DREP_KEY_DERIVATION_PATH, purpose); - - return { - addressParams: { - params: { - spendingPath: path - }, - type: AddressType.ENTERPRISE_KEY - }, - signingPath: path - }; - } - const isRewardAccount = signWith.startsWith('stake'); // Reward account @@ -255,6 +246,25 @@ const getDerivationPath = ( }; } + if (isPaymentAddress(signWith)) { + const drepAddr = Cardano.Address.fromString(signWith); + if ( + drepAddr?.getType() === Cardano.AddressType.EnterpriseKey && + drepAddr?.getProps().paymentPart?.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash) + ) { + const path = util.accountKeyDerivationPathToBip32Path(accountIndex, util.DREP_KEY_DERIVATION_PATH, purpose); + return { + addressParams: { + params: { + spendingPath: path + }, + type: AddressType.ENTERPRISE_KEY + }, + signingPath: path + }; + } + } + const knownAddress = knownAddresses.find(({ address }) => address === signWith); if (!knownAddress) { @@ -746,11 +756,14 @@ export class LedgerKeyAgent extends KeyAgentBase { async signCip8Data(request: cip8.Cip8SignDataContext): Promise { try { + const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH); + const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); const { signingPath, addressParams } = getDerivationPath( request.signWith, request.knownAddresses, this.accountIndex, - this.purpose + this.purpose, + dRepKeyHashHex ); const messageData: MessageData = { diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts index 1609da963f4..a15208d663b 100644 --- a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts +++ b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts @@ -17,6 +17,7 @@ import { Hash32ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; import { LedgerKeyAgent, LedgerTransportType } from '@cardano-sdk/hardware-ledger'; +import { buildDRepAddressFromDRepKey } from '../../util'; import { firstValueFrom } from 'rxjs'; import { getDevices } from '@ledgerhq/hw-transport-node-hid-noevents'; import { dummyLogger as logger } from 'ts-log'; @@ -747,6 +748,26 @@ describe('LedgerKeyAgent', () => { ) ).toBe(true); }); + + // TODO: Enable once we have confirmation from Ledger team that it is supported + it.skip('can sign with drep key', async () => { + const drepPubKey = await wallet.governance.getPubDRepKey(); + const signWith = (await buildDRepAddressFromDRepKey(drepPubKey!))?.toAddress()?.toBech32(); + const { coseSign1, publicKeyHex, signedData } = await signAndDecode(signWith!, wallet); + const signedDataBytes = HexBlob.fromBytes(signedData.to_bytes()); + const signatureBytes = HexBlob.fromBytes(coseSign1.signature()) as unknown as Crypto.Ed25519SignatureHex; + const cryptoProvider = new Crypto.SodiumBip32Ed25519(); + + expect(publicKeyHex).toEqual(drepPubKey); + + expect( + await cryptoProvider.verify( + signatureBytes, + signedDataBytes, + publicKeyHex as unknown as Crypto.Ed25519PublicKeyHex + ) + ).toBe(true); + }); }); }); }); diff --git a/yarn.lock b/yarn.lock index 9d9ffc73978..642a8e322a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3626,6 +3626,7 @@ __metadata: "@dcspark/cardano-multiplatform-lib-browser": ^3.1.1 "@dcspark/cardano-multiplatform-lib-nodejs": ^3.1.1 "@emurgo/cardano-message-signing-asmjs": ^1.0.1 + "@emurgo/cardano-message-signing-nodejs": ^1.0.1 "@shiroyasha9/axios-fetch-adapter": 1.0.3 "@types/bunyan": ^1.8.8 "@types/chalk": ^2.2.0