From 1363674cd60b16a496bb22d779cc6894ffe47ecb Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Thu, 19 Dec 2024 11:26:46 +0100 Subject: [PATCH 1/5] fix(key-management): signData with drep bech32 addr --- .../key-management/src/cip8/cip30signData.ts | 24 ++++++++++++++++-- .../test/cip8/cip30signData.test.ts | 25 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/key-management/src/cip8/cip30signData.ts b/packages/key-management/src/cip8/cip30signData.ts index 68d16704d2a..13a7318057a 100644 --- a/packages/key-management/src/cip8/cip30signData.ts +++ b/packages/key-management/src/cip8/cip30signData.ts @@ -49,9 +49,14 @@ export const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.Rewar return Buffer.from(address.toBytes(), 'hex'); }; +const isPaymentAddress = ( + signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID +): signWith is Cardano.PaymentAddress => signWith.startsWith('addr'); + const getDerivationPath = async ( signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID, - knownAddresses: GroupedAddress[] + knownAddresses: GroupedAddress[], + dRepKeyHash: Crypto.Ed25519KeyHashHex ) => { if (Cardano.DRepID.isValid(signWith)) { return DREP_KEY_DERIVATION_PATH; @@ -68,6 +73,16 @@ const getDerivationPath = async ( return knownRewardAddress.stakeKeyDerivationPath || STAKE_KEY_DERIVATION_PATH; } + if (isPaymentAddress(signWith)) { + const drepAddr = Cardano.Address.fromString(signWith); + if ( + drepAddr?.getType() === Cardano.AddressType.EnterpriseKey && + drepAddr?.getProps().paymentPart?.hash === Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(dRepKeyHash) + ) { + return DREP_KEY_DERIVATION_PATH; + } + } + const knownAddress = knownAddresses.find(({ address }) => address === signWith); if (!knownAddress) { @@ -120,8 +135,13 @@ export const cip30signData = async ( if (Cardano.DRepID.isValid(signWith) && !Cardano.DRepID.canSign(signWith)) { throw new Cip30DataSignError(Cip30DataSignErrorCode.AddressNotPK, 'Invalid address'); } + + const dRepKeyHash = ( + await Crypto.Ed25519PublicKey.fromHex(await keyAgent.derivePublicKey(DREP_KEY_DERIVATION_PATH)).hash() + ).hex(); + const addressBytes = getAddressBytes(signWith); - const derivationPath = await getDerivationPath(signWith, knownAddresses); + const derivationPath = await getDerivationPath(signWith, knownAddresses, dRepKeyHash); const builder = COSESign1Builder.new( Headers.new(ProtectedHeaderMap.new(createSigStructureHeaders(addressBytes)), HeaderMap.new()), diff --git a/packages/key-management/test/cip8/cip30signData.test.ts b/packages/key-management/test/cip8/cip30signData.test.ts index 5c44606000e..1042a89840a 100644 --- a/packages/key-management/test/cip8/cip30signData.test.ts +++ b/packages/key-management/test/cip8/cip30signData.test.ts @@ -3,6 +3,7 @@ import { AddressType, AsyncKeyAgent, GroupedAddress, KeyAgent, KeyRole, cip8 } f import { COSEKey, COSESign1, SigStructure } from '@emurgo/cardano-message-signing-nodejs'; import { Cardano, util } from '@cardano-sdk/core'; import { CoseLabel } from '../../src/cip8/util'; +import { DREP_KEY_DERIVATION_PATH } from '../../src/util'; import { HexBlob } from '@cardano-sdk/util'; import { createCoseKey, getAddressBytes } from '../../src/cip8'; import { testAsyncKeyAgent, testKeyAgent } from '../mocks'; @@ -13,6 +14,8 @@ describe('cip30signData', () => { let keyAgent: KeyAgent; let asyncKeyAgent: AsyncKeyAgent; let address: GroupedAddress; + let drepKeyHex: Crypto.Ed25519PublicKeyHex; + let drepKeyHash: Crypto.Ed25519KeyHashHex; const cryptoProvider = new Crypto.SodiumBip32Ed25519(); beforeAll(async () => { @@ -20,6 +23,8 @@ describe('cip30signData', () => { keyAgent = await keyAgentReady; asyncKeyAgent = await testAsyncKeyAgent(undefined, keyAgentReady); address = await asyncKeyAgent.deriveAddress(addressDerivationPath, 0); + drepKeyHex = await asyncKeyAgent.derivePublicKey(DREP_KEY_DERIVATION_PATH); + drepKeyHash = (await Crypto.Ed25519PublicKey.fromHex(drepKeyHex).hash()).hex(); }); const signAndDecode = async ( @@ -82,6 +87,26 @@ describe('cip30signData', () => { ); }); + it('supports signing with drep key hash as bech32 enterprise payment address', async () => { + const drepAddr = new Cardano.Address({ + paymentPart: { + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(drepKeyHash), + type: Cardano.CredentialType.KeyHash + }, + type: Cardano.AddressType.EnterpriseKey + }); + + const signWith = drepAddr.toBech32(); + if (!signWith) { + expect(signWith).toBeDefined(); + return; + } + + const { signedData, publicKeyHex } = await signAndDecode(signWith, [address]); + testAddressHeader(signedData, signWith); + expect(publicKeyHex).toEqual(drepKeyHex); + }); + it('signature can be verified', async () => { const signWith = address.address; const { coseSign1, publicKeyHex, signedData } = await signAndDecode(signWith, [address]); From 7958087b079def3cab68c31ed7945da91b5493cf Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Thu, 19 Dec 2024 15:59:24 +0100 Subject: [PATCH 2/5] chore: remove DrepID.canSign It was checking if the drep bech32 address is a valid type 6 address, but that is not the case, and the drepId used in the test was incorrect (not a CIP129, but had the length of CIP129) --- packages/core/src/Cardano/Address/DRepID.ts | 10 +--------- packages/core/src/Cardano/Address/PaymentAddress.ts | 1 - packages/core/test/Cardano/Address/DRepID.test.ts | 13 ++----------- packages/key-management/src/cip8/cip30signData.ts | 4 ---- 4 files changed, 3 insertions(+), 25 deletions(-) diff --git a/packages/core/src/Cardano/Address/DRepID.ts b/packages/core/src/Cardano/Address/DRepID.ts index d964ca7def5..190e7e670f2 100644 --- a/packages/core/src/Cardano/Address/DRepID.ts +++ b/packages/core/src/Cardano/Address/DRepID.ts @@ -1,5 +1,5 @@ import * as BaseEncoding from '@scure/base'; -import { Address, AddressType, Credential, CredentialType } from './Address'; +import { Credential, CredentialType } from './Address'; import { Hash28ByteBase16 } from '@cardano-sdk/crypto'; import { OpaqueString, typedBech32 } from '@cardano-sdk/util'; @@ -29,14 +29,6 @@ DRepID.isValid = (value: string): boolean => { } }; -DRepID.canSign = (value: string): boolean => { - try { - return DRepID.isValid(value) && Address.fromBech32(value).getType() === AddressType.EnterpriseKey; - } catch { - return false; - } -}; - DRepID.cip105FromCredential = (credential: Credential): DRepID => { let prefix = 'drep'; if (credential.type === CredentialType.ScriptHash) { diff --git a/packages/core/src/Cardano/Address/PaymentAddress.ts b/packages/core/src/Cardano/Address/PaymentAddress.ts index 883f022c6a2..132be21e3cf 100644 --- a/packages/core/src/Cardano/Address/PaymentAddress.ts +++ b/packages/core/src/Cardano/Address/PaymentAddress.ts @@ -17,7 +17,6 @@ export type PaymentAddress = OpaqueString<'PaymentAddress'>; /** * @param {string} address mainnet or testnet address - * @throws InvalidStringError */ export const isRewardAccount = (address: string) => { try { diff --git a/packages/core/test/Cardano/Address/DRepID.test.ts b/packages/core/test/Cardano/Address/DRepID.test.ts index 5731a5ae90f..82c64970b3a 100644 --- a/packages/core/test/Cardano/Address/DRepID.test.ts +++ b/packages/core/test/Cardano/Address/DRepID.test.ts @@ -51,7 +51,7 @@ describe('Cardano/Address/DRepID', () => { }); it('DRepID() accepts a valid bech32 string with drep as prefix', () => { - expect(() => DRepID('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).not.toThrow(); + expect(() => DRepID(CIP105_PUB_KEY_HASH_ID)).not.toThrow(); }); it('DRepID() throws an error if the bech32 string has the wrong prefix', () => { @@ -62,19 +62,10 @@ describe('Cardano/Address/DRepID', () => { describe('isValid', () => { it('is true if string is a valid DRepID', () => { - expect(DRepID.isValid('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).toBe(true); + expect(DRepID.isValid(CIP129_PUB_KEY_HASH_ID)).toBe(true); }); it('is false if string is not a valid DRepID', () => { expect(DRepID.isValid('addr_test1vpudzrw5uq46qwl6h5szlc66fydr0l2rlsw4nvaaxfld40g3ys07c')).toBe(false); }); }); - - describe('canSign', () => { - it('is true if DRepID is a valid type 6 address', () => { - expect(DRepID.canSign('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).toBe(true); - }); - it('is false if DRepID is not a type 6 address', () => { - expect(DRepID.canSign('drep1wpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9qcluy2z')).toBe(false); - }); - }); }); diff --git a/packages/key-management/src/cip8/cip30signData.ts b/packages/key-management/src/cip8/cip30signData.ts index 13a7318057a..8bedda15571 100644 --- a/packages/key-management/src/cip8/cip30signData.ts +++ b/packages/key-management/src/cip8/cip30signData.ts @@ -132,10 +132,6 @@ export const cip30signData = async ( keyAgent: KeyAgent, { knownAddresses, signWith, payload }: Cip8SignDataContext ): Promise => { - if (Cardano.DRepID.isValid(signWith) && !Cardano.DRepID.canSign(signWith)) { - throw new Cip30DataSignError(Cip30DataSignErrorCode.AddressNotPK, 'Invalid address'); - } - const dRepKeyHash = ( await Crypto.Ed25519PublicKey.fromHex(await keyAgent.derivePublicKey(DREP_KEY_DERIVATION_PATH)).hash() ).hex(); From e39842b8c0cdc2848870ebdd0b01c5a5b0eca2c2 Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Thu, 19 Dec 2024 16:18:53 +0100 Subject: [PATCH 3/5] feat(core): add util to convert drepId to enterprise address --- packages/core/src/Cardano/Address/DRepID.ts | 11 +++++- .../core/test/Cardano/Address/DRepID.test.ts | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/core/src/Cardano/Address/DRepID.ts b/packages/core/src/Cardano/Address/DRepID.ts index 190e7e670f2..64eb3c44d6c 100644 --- a/packages/core/src/Cardano/Address/DRepID.ts +++ b/packages/core/src/Cardano/Address/DRepID.ts @@ -1,5 +1,6 @@ import * as BaseEncoding from '@scure/base'; -import { Credential, CredentialType } from './Address'; +import { Address, AddressType, Credential, CredentialType } from './Address'; +import { EnterpriseAddress } from './EnterpriseAddress'; import { Hash28ByteBase16 } from '@cardano-sdk/crypto'; import { OpaqueString, typedBech32 } from '@cardano-sdk/util'; @@ -98,3 +99,11 @@ DRepID.toCip129DRepID = (drepId: DRepID): DRepID => { const credential = DRepID.toCredential(drepId); return DRepID.cip129FromCredential(credential); }; + +DRepID.toAddress = (drepId: DRepID): EnterpriseAddress | undefined => { + const credential = DRepID.toCredential(drepId); + return new Address({ + paymentPart: credential, + type: credential.type === CredentialType.KeyHash ? AddressType.EnterpriseKey : AddressType.EnterpriseScript + }).asEnterprise(); +}; diff --git a/packages/core/test/Cardano/Address/DRepID.test.ts b/packages/core/test/Cardano/Address/DRepID.test.ts index 82c64970b3a..d41316a9218 100644 --- a/packages/core/test/Cardano/Address/DRepID.test.ts +++ b/packages/core/test/Cardano/Address/DRepID.test.ts @@ -1,3 +1,4 @@ +import { Cardano } from '../../../src'; import { Credential, CredentialType, DRepID } from '../../../src/Cardano'; import { InvalidStringError } from '@cardano-sdk/util'; @@ -68,4 +69,38 @@ describe('Cardano/Address/DRepID', () => { expect(DRepID.isValid('addr_test1vpudzrw5uq46qwl6h5szlc66fydr0l2rlsw4nvaaxfld40g3ys07c')).toBe(false); }); }); + + describe('toAddress', () => { + it('can convert a CIP105 DRepID to a type 6 Cardano.Address', () => { + const drepId = DRepID(CIP105_PUB_KEY_HASH_ID); + const drepAddress = DRepID.toAddress(drepId); + expect(drepAddress).toBeDefined(); + expect(drepAddress?.toAddress().getType()).toEqual(Cardano.AddressType.EnterpriseKey); + expect(drepAddress?.toAddress().getProps().paymentPart).toEqual(pubKeyHashCredential); + }); + + it('can convert a CIP129 DRepID to a type 6 Cardano.Address', () => { + const drepId = DRepID(CIP129_PUB_KEY_HASH_ID); + const drepAddress = DRepID.toAddress(drepId); + expect(drepAddress).toBeDefined(); + expect(drepAddress?.toAddress().getType()).toEqual(Cardano.AddressType.EnterpriseKey); + expect(drepAddress?.toAddress().getProps().paymentPart).toEqual(pubKeyHashCredential); + }); + + it('can convert a CIP105 script hash DRepID to a type 7 Cardano.Address', () => { + const drepId = DRepID(CIP105_SCRIPT_HASH_ID); + const drepAddress = DRepID.toAddress(drepId); + expect(drepAddress).toBeDefined(); + expect(drepAddress?.toAddress().getType()).toEqual(Cardano.AddressType.EnterpriseScript); + expect(drepAddress?.toAddress().getProps().paymentPart).toEqual(scriptHashCredential); + }); + + it('can convert a CIP129 script hash DRepID to a type 7 Cardano.Address', () => { + const drepId = DRepID(CIP129_SCRIPT_HASH_ID); + const drepAddress = DRepID.toAddress(drepId); + expect(drepAddress).toBeDefined(); + expect(drepAddress?.toAddress().getType()).toEqual(Cardano.AddressType.EnterpriseScript); + expect(drepAddress?.toAddress().getProps().paymentPart).toEqual(scriptHashCredential); + }); + }); }); From 44c37163e834efa76876a99c4ed0ca4c7c67dfbf Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Thu, 19 Dec 2024 20:56:36 +0100 Subject: [PATCH 4/5] feat: implement sign with drep key signData now accepts drepId as key hash hex, or type6 address. Support for DRepID bech32 was removed to align with cip-95. BREAKING CHANGE: SignData type no longer accepts bech32 DRepID --- .../src/WalletApi/Cip30Wallet.ts | 9 +- .../dapp-connector/src/WalletApi/types.ts | 3 +- .../hardware-ledger/src/LedgerKeyAgent.ts | 2 +- .../key-management/src/cip8/cip30signData.ts | 12 +-- packages/key-management/src/cip8/types.ts | 2 +- packages/wallet/src/cip30.ts | 35 +++++- .../test/PersonalWallet/methods.test.ts | 22 ++-- .../test/integration/cip30mapping.test.ts | 101 +++++++++++++++--- packages/wallet/test/util.ts | 29 +++-- 9 files changed, 157 insertions(+), 58 deletions(-) diff --git a/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts b/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts index 71d5282e1ac..0f44f186148 100644 --- a/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts +++ b/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts @@ -28,7 +28,7 @@ export const CipMethodsMapping: Record = { 'signData', 'submitTx' ], - 95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey'] + 95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey', 'signData'] }; export const WalletApiMethodNames: WalletMethod[] = Object.values(CipMethodsMapping).flat(); @@ -182,7 +182,8 @@ export class Cip30Wallet { getUnusedAddresses: () => walletApi.getUnusedAddresses(), getUsedAddresses: (paginate?: Paginate) => walletApi.getUsedAddresses(paginate), getUtxos: (amount?: Cbor, paginate?: Paginate) => walletApi.getUtxos(amount, paginate), - signData: (addr: Cardano.PaymentAddress | Bytes, payload: Bytes) => walletApi.signData(addr, payload), + signData: (addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes) => + walletApi.signData(addr, payload), signTx: (tx: Cbor, partialSign?: Boolean) => walletApi.signTx(tx, partialSign), submitTx: (tx: Cbor) => walletApi.submitTx(tx) }; @@ -191,7 +192,9 @@ export class Cip30Wallet { cip95: { getPubDRepKey: () => walletApi.getPubDRepKey(), getRegisteredPubStakeKeys: () => walletApi.getRegisteredPubStakeKeys(), - getUnregisteredPubStakeKeys: () => walletApi.getUnregisteredPubStakeKeys() + getUnregisteredPubStakeKeys: () => walletApi.getUnregisteredPubStakeKeys(), + signData: (addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes) => + walletApi.signData(addr, payload) } }; diff --git a/packages/dapp-connector/src/WalletApi/types.ts b/packages/dapp-connector/src/WalletApi/types.ts index b75a246712e..c5144aed7c5 100644 --- a/packages/dapp-connector/src/WalletApi/types.ts +++ b/packages/dapp-connector/src/WalletApi/types.ts @@ -153,7 +153,7 @@ export type SignTx = (tx: Cbor, partialSign?: Boolean) => Promise; * @throws DataSignError */ export type SignData = ( - addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes, + addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes ) => Promise; @@ -203,6 +203,7 @@ export interface Cip95WalletApi { getRegisteredPubStakeKeys: () => Promise; getUnregisteredPubStakeKeys: () => Promise; getPubDRepKey: () => Promise; + signData: SignData; } export type WalletApi = Cip30WalletApi & Cip95WalletApi; diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index 1174e721d45..552ceebd26d 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -210,7 +210,7 @@ type OpenTransportForDeviceParams = { }; const getDerivationPath = ( - signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID, + signWith: Cardano.PaymentAddress | Cardano.RewardAccount, knownAddresses: GroupedAddress[], accountIndex: number, purpose: number diff --git a/packages/key-management/src/cip8/cip30signData.ts b/packages/key-management/src/cip8/cip30signData.ts index 8bedda15571..359664eeb08 100644 --- a/packages/key-management/src/cip8/cip30signData.ts +++ b/packages/key-management/src/cip8/cip30signData.ts @@ -22,7 +22,7 @@ import { DREP_KEY_DERIVATION_PATH, STAKE_KEY_DERIVATION_PATH } from '../util'; export interface Cip30SignDataRequest { knownAddresses: GroupedAddress[]; - signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID; + signWith: Cardano.PaymentAddress | Cardano.RewardAccount; payload: HexBlob; sender?: MessageSender; } @@ -39,7 +39,7 @@ export class Cip30DataSignError extends ComposableError { +export const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccount) => { const address = Cardano.Address.fromString(signWith); if (!address) { @@ -50,18 +50,14 @@ export const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.Rewar }; const isPaymentAddress = ( - signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID + signWith: Cardano.PaymentAddress | Cardano.RewardAccount ): signWith is Cardano.PaymentAddress => signWith.startsWith('addr'); const getDerivationPath = async ( - signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID, + signWith: Cardano.PaymentAddress | Cardano.RewardAccount, knownAddresses: GroupedAddress[], dRepKeyHash: Crypto.Ed25519KeyHashHex ) => { - if (Cardano.DRepID.isValid(signWith)) { - return DREP_KEY_DERIVATION_PATH; - } - const isRewardAccount = signWith.startsWith('stake'); if (isRewardAccount) { diff --git a/packages/key-management/src/cip8/types.ts b/packages/key-management/src/cip8/types.ts index 5d4cae4e660..3cb5c2e1ecf 100644 --- a/packages/key-management/src/cip8/types.ts +++ b/packages/key-management/src/cip8/types.ts @@ -8,7 +8,7 @@ export type CoseKeyCborHex = HexBlob; export interface Cip8SignDataContext { knownAddresses: GroupedAddress[]; - signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID; + signWith: Cardano.PaymentAddress | Cardano.RewardAccount; payload: HexBlob; } diff --git a/packages/wallet/src/cip30.ts b/packages/wallet/src/cip30.ts index 016f63bd6e6..380f2407f1d 100644 --- a/packages/wallet/src/cip30.ts +++ b/packages/wallet/src/cip30.ts @@ -18,6 +18,7 @@ import { WithSenderContext } from '@cardano-sdk/dapp-connector'; import { Cardano, Serialization, coalesceValueQuantities } from '@cardano-sdk/core'; +import { Ed25519KeyHashHex, Hash28ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util'; import { InputSelectionError, InputSelectionFailure } from '@cardano-sdk/input-selection'; import { Logger } from 'ts-log'; @@ -42,7 +43,7 @@ export type SignDataCallbackParams = { type: Cip30ConfirmationCallbackType.SignData; sender: MessageSender; data: { - addr: Cardano.PaymentAddress | Cardano.DRepID; + addr: Cardano.PaymentAddress | Cardano.RewardAccount; payload: HexBlob; }; }; @@ -258,6 +259,32 @@ const getSortedUtxos = async (observableUtxos: Observable): Prom return utxos.sort(compareUtxos); }; +/** + * Detect type of hex encoded addr and convert to PaymentAddress or RewardAddress. + * + * @param addr when hex encoded, it can be a PaymentAddress, RewardAddress or DRepKeyHash + * @returns PaymentAddress | RewardAddress DRepKeyHash is converted to a type 6 address + */ +const addrToSignWith = ( + addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes +): Cardano.PaymentAddress | Cardano.RewardAccount => { + try { + return Cardano.isRewardAccount(addr) ? Cardano.RewardAccount(addr) : Cardano.PaymentAddress(addr); + } catch { + // Try to parse as drep key hash + const drepKeyHash = Ed25519KeyHashHex(addr); + const drepId = Cardano.DRepID.cip129FromCredential({ + hash: Hash28ByteBase16.fromEd25519KeyHashHex(drepKeyHash), + type: Cardano.CredentialType.KeyHash + }); + const drepAddr = Cardano.DRepID.toAddress(drepId)?.toAddress(); + if (!drepAddr) { + throw new DataSignError(DataSignErrorCode.AddressNotPK, 'Invalid address'); + } + return drepAddr.toBech32(); + } +}; + const baseCip30WalletApi = ( wallet$: Observable, confirmationCallback: CallbackConfirmation, @@ -430,12 +457,12 @@ const baseCip30WalletApi = ( }, signData: async ( { sender }: SenderContext, - addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes, + addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes ): Promise => { logger.debug('signData'); + const signWith = addrToSignWith(addr); const hexBlobPayload = HexBlob(payload); - const signWith = Cardano.DRepID.isValid(addr) ? Cardano.DRepID(addr) : Cardano.PaymentAddress(addr); const confirmationResult = await confirmationCallback .signData({ @@ -579,7 +606,7 @@ const getPubStakeKeys = async ( const extendedCip95WalletApi = ( wallet$: Observable, { logger }: Cip30WalletDependencies -): Cip95WalletApi => ({ +): Omit => ({ getPubDRepKey: async () => { logger.debug('getting public DRep key'); try { diff --git a/packages/wallet/test/PersonalWallet/methods.test.ts b/packages/wallet/test/PersonalWallet/methods.test.ts index 11b902797fa..eb47ab19c46 100644 --- a/packages/wallet/test/PersonalWallet/methods.test.ts +++ b/packages/wallet/test/PersonalWallet/methods.test.ts @@ -21,7 +21,7 @@ import { import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps } from '@cardano-sdk/tx-construction'; import { babbageTx } from '../../../core/test/Serialization/testData'; -import { buildDRepIDFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util'; +import { buildDRepAddressFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util'; import { getPassphrase, stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; import { dummyLogger as logger } from 'ts-log'; @@ -494,9 +494,17 @@ describe('BaseWallet methods', () => { }); it('signs with bech32 DRepID', async () => { + const drepPubKey = await wallet.governance.getPubDRepKey(); + const drepAddr = (await buildDRepAddressFromDRepKey(drepPubKey!))?.toAddress()?.toBech32(); + + if (!drepAddr) { + expect(drepAddr).toBeDefined(); + return; + } + const response = await wallet.signData({ payload: HexBlob('abc123'), - signWith: Cardano.DRepID('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz') + signWith: drepAddr }); expect(response).toHaveProperty('signature'); }); @@ -513,16 +521,6 @@ describe('BaseWallet methods', () => { signWith: address }); }); - - test('rejects if bech32 DRepID is not a type 6 address', async () => { - const dRepKey = await wallet.governance.getPubDRepKey(); - for (const type in Cardano.AddressType) { - if (!Number.isNaN(Number(type)) && Number(type) !== Cardano.AddressType.EnterpriseKey) { - const drepid = buildDRepIDFromDRepKey(dRepKey!, 0, type as unknown as Cardano.AddressType); - await expect(wallet.signData({ payload: HexBlob('abc123'), signWith: drepid })).rejects.toThrow(); - } - } - }); }); it('getPubDRepKey', async () => { diff --git a/packages/wallet/test/integration/cip30mapping.test.ts b/packages/wallet/test/integration/cip30mapping.test.ts index a67c833788e..ae69aee3836 100644 --- a/packages/wallet/test/integration/cip30mapping.test.ts +++ b/packages/wallet/test/integration/cip30mapping.test.ts @@ -33,7 +33,7 @@ import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construct import { NEVER, firstValueFrom, of } from 'rxjs'; import { Providers, createWallet } from './util'; import { address_0_0, address_1_0, rewardAccount_0, rewardAccount_1 } from '../services/ChangeAddress/testData'; -import { buildDRepIDFromDRepKey, signTx, waitForWalletStateSettle } from '../util'; +import { buildDRepAddressFromDRepKey, signTx, waitForWalletStateSettle } from '../util'; import { dummyLogger as logger } from 'ts-log'; import { stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; import uniq from 'lodash/uniq.js'; @@ -686,30 +686,105 @@ describe('cip30', () => { }); describe('api.signData', () => { - test('sign with address', async () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('sign with bech32 address', async () => { const [{ address }] = await firstValueFrom(wallet.addresses$); const cip30dataSignature = await api.signData(context, address, HexBlob('abc123')); expect(typeof cip30dataSignature.key).toBe('string'); expect(typeof cip30dataSignature.signature).toBe('string'); }); - test('sign with bech32 DRepID', async () => { + test('sign with hex-encoded address', async () => { + const signDataSpy = jest.spyOn(wallet, 'signData'); + const [{ address }] = await firstValueFrom(wallet.addresses$); + const addressHex = Cardano.Address.fromString(address)?.toBytes(); + if (!addressHex) { + expect(addressHex).toBeDefined(); + return; + } + const cip30dataSignature = await api.signData(context, addressHex, HexBlob('abc123')); + expect(typeof cip30dataSignature.key).toBe('string'); + expect(typeof cip30dataSignature.signature).toBe('string'); + expect(signDataSpy.mock.calls[0][0].signWith).toEqual(address); + }); + + test('sign with bech32 reward account', async () => { + const signDataSpy = jest.spyOn(wallet, 'signData'); + const [{ rewardAccount }] = await firstValueFrom(wallet.addresses$); + + const cip30dataSignature = await api.signData(context, rewardAccount, HexBlob('abc123')); + expect(typeof cip30dataSignature.key).toBe('string'); + expect(typeof cip30dataSignature.signature).toBe('string'); + expect(signDataSpy.mock.calls[0][0].signWith).toEqual(rewardAccount); + }); + + test('sign with hex-encoded reward account', async () => { + const signDataSpy = jest.spyOn(wallet, 'signData'); + const [{ rewardAccount }] = await firstValueFrom(wallet.addresses$); + const rewardAccountHex = Cardano.Address.fromString(rewardAccount)?.toBytes(); + + const cip30dataSignature = await api.signData(context, rewardAccountHex!, HexBlob('abc123')); + expect(typeof cip30dataSignature.key).toBe('string'); + expect(typeof cip30dataSignature.signature).toBe('string'); + expect(signDataSpy.mock.calls[0][0].signWith).toEqual(rewardAccount); + }); + + test('sign with hex-encoded DRepID key hash hex', async () => { + const signDataSpy = jest.spyOn(wallet, 'signData'); const dRepKey = await api.getPubDRepKey(context); - const drepid = buildDRepIDFromDRepKey(dRepKey); + const drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex(); + + await api.signData(context, drepKeyHashHex, HexBlob('abc123')); + expect(signDataSpy).toHaveBeenCalledTimes(1); + // Wallet signData is called with the DRepID as bech32 address because it was transformed by the cip30Api. + // The address credential should be the drepKeyHash + const signAddr = Cardano.Address.fromString(signDataSpy.mock.calls[0][0].signWith); + expect(signAddr?.getProps().paymentPart?.hash).toEqual(drepKeyHashHex); + }); - const cip95dataSignature = await api.signData(context, drepid, HexBlob('abc123')); - expect(typeof cip95dataSignature.key).toBe('string'); - expect(typeof cip95dataSignature.signature).toBe('string'); + test('sign with hex-encoded type 6 DRepID address', async () => { + const signDataSpy = jest.spyOn(wallet, 'signData'); + const dRepKey = await api.getPubDRepKey(context); + const drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex(); + const drepAddress = await buildDRepAddressFromDRepKey(dRepKey); + // CIP95 DRepID as type 6 hex-encoded Address + const drepAddressBytes = drepAddress?.toAddress()?.toBytes(); + + if (!drepAddressBytes) { + expect(drepAddressBytes).toBeDefined(); + return; + } + + await api.signData(context, drepAddressBytes, HexBlob('abc123')); + expect(signDataSpy).toHaveBeenCalledTimes(1); + // Wallet signData is called with the DRepID as bech32 address because it was transformed by the cip30Api. + // The address credential should be the drepKeyHash + const signAddr = Cardano.Address.fromString(signDataSpy.mock.calls[0][0].signWith); + expect(signAddr?.getProps().paymentPart?.hash).toEqual(drepKeyHashHex); }); - test('rejects if bech32 DRepID is not a type 6 address', async () => { + test('sign with bech32 type 6 DRepID address', async () => { + const signDataSpy = jest.spyOn(wallet, 'signData'); const dRepKey = await api.getPubDRepKey(context); - for (const type in Cardano.AddressType) { - if (!Number.isNaN(Number(type)) && Number(type) !== Cardano.AddressType.EnterpriseKey) { - const drepid = buildDRepIDFromDRepKey(dRepKey, 0, type as unknown as Cardano.AddressType); - await expect(api.signData(context, drepid, HexBlob('abc123'))).rejects.toThrow(); - } + const drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex(); + const drepAddress = await buildDRepAddressFromDRepKey(dRepKey); + // CIP95 DRepID as type 6 hex-encoded Address + const drepAddressBech32 = drepAddress?.toAddress()?.toBech32(); + + if (!drepAddressBech32) { + expect(drepAddressBech32).toBeDefined(); + return; } + + await api.signData(context, drepAddressBech32, HexBlob('abc123')); + expect(signDataSpy).toHaveBeenCalledTimes(1); + // Wallet signData is called with the DRepID as bech32 address because it was transformed by the cip30Api. + // The address credential should be the drepKeyHash + const signAddr = Cardano.Address.fromString(signDataSpy.mock.calls[0][0].signWith); + expect(signAddr?.getProps().paymentPart?.hash).toEqual(drepKeyHashHex); }); it('passes through sender from dapp connector context', async () => { diff --git a/packages/wallet/test/util.ts b/packages/wallet/test/util.ts index 328bd08591a..e4b20c78fc8 100644 --- a/packages/wallet/test/util.ts +++ b/packages/wallet/test/util.ts @@ -1,7 +1,6 @@ import * as Crypto from '@cardano-sdk/crypto'; import { Cardano, Serialization } from '@cardano-sdk/core'; import { GroupedAddress, InMemoryKeyAgent, WitnessedTx, util } from '@cardano-sdk/key-management'; -import { HexBlob } from '@cardano-sdk/util'; import { Observable, catchError, filter, firstValueFrom, throwError, timeout } from 'rxjs'; import { ObservableWallet, OutgoingTx, WalletUtil } from '../src'; import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto'; @@ -49,23 +48,23 @@ export const toSignedTx = (tx: Cardano.Tx): WitnessedTx => ({ export const dummyCbor = Serialization.TxCBOR('123'); -/** Construct a type 6 address for a DRepKey using an appropriate Network Tag and a hash of a public DRep Key. */ -export const buildDRepIDFromDRepKey = ( +/** + * Construct a type 6 or 7 address for a DRepKey using a hash of a public DRep Key. + * + * @param dRepKey The public DRep key to hash and use in the address + * @param type The type of credential to use in the address. + * @returns A a type 6 address for keyHash credential type, or a type 7 address for script credential type. + */ +export const buildDRepAddressFromDRepKey = async ( dRepKey: Crypto.Ed25519PublicKeyHex, - networkId: Cardano.NetworkId = Cardano.NetworkId.Testnet, - addressType: Cardano.AddressType = Cardano.AddressType.EnterpriseKey + type: Cardano.CredentialType = Cardano.CredentialType.KeyHash ) => { - const dRepKeyBytes = Buffer.from(dRepKey, 'hex'); - const dRepIdHex = Crypto.blake2b(28).update(dRepKeyBytes).digest('hex'); - const paymentAddress = Cardano.EnterpriseAddress.packParts({ - networkId, - paymentPart: { - hash: Crypto.Hash28ByteBase16(dRepIdHex), - type: Cardano.CredentialType.KeyHash - }, - type: addressType + const drepKeyHash = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex(); + const drepId = Cardano.DRepID.cip129FromCredential({ + hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(drepKeyHash), + type }); - return HexBlob.toTypedBech32('drep', HexBlob.fromBytes(paymentAddress)); + return Cardano.DRepID.toAddress(drepId); }; export const createAsyncKeyAgent = async () => From 99a5930a9243df29a0eae1daeae4ed29c29a98ca Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Fri, 20 Dec 2024 13:02:13 +0100 Subject: [PATCH 5/5] 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 | 82 +++++++----- .../hardware/ledger/LedgerKeyAgent.test.ts | 20 +++ yarn.lock | 1 + 9 files changed, 194 insertions(+), 63 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..8c87d461df2 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 -): { 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 - }; - } - + purpose: number, + dRepKeyHash: Crypto.Ed25519KeyHashHex +): { signingPath: BIP32Path; addressParams?: DeviceOwnedAddress; addressFieldType: MessageAddressFieldType } => { const isRewardAccount = signWith.startsWith('stake'); // Reward account @@ -245,6 +236,7 @@ const getDerivationPath = ( ); return { + addressFieldType: MessageAddressFieldType.ADDRESS, addressParams: { params: { stakingPath: path @@ -255,6 +247,20 @@ 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 { + addressFieldType: MessageAddressFieldType.KEY_HASH, + signingPath: path + }; + } + } + const knownAddress = knownAddresses.find(({ address }) => address === signWith); if (!knownAddress) { @@ -282,6 +288,7 @@ const getDerivationPath = ( ); return { + addressFieldType: MessageAddressFieldType.ADDRESS, addressParams: { params: { spendingPath, @@ -304,6 +311,7 @@ const getDerivationPath = ( // Enterprise Address return { + addressFieldType: MessageAddressFieldType.ADDRESS, addressParams: { params: { spendingPath @@ -746,25 +754,39 @@ export class LedgerKeyAgent extends KeyAgentBase { async signCip8Data(request: cip8.Cip8SignDataContext): Promise { try { - const { signingPath, addressParams } = getDerivationPath( + const dRepPublicKey = await this.derivePublicKey(util.DREP_KEY_DERIVATION_PATH); + const dRepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); + + const { signingPath, addressParams, addressFieldType } = getDerivationPath( request.signWith, request.knownAddresses, this.accountIndex, - this.purpose + this.purpose, + dRepKeyHashHex ); - const messageData: MessageData = { - address: addressParams, - addressFieldType: MessageAddressFieldType.ADDRESS, - hashPayload: false, - messageHex: request.payload, - network: { - networkId: this.chainId.networkId, - protocolMagic: this.chainId.networkMagic - }, - preferHexDisplay: false, - signingPath - }; + const messageData: MessageData = + addressFieldType === MessageAddressFieldType.ADDRESS && addressParams + ? { + address: addressParams, + addressFieldType: MessageAddressFieldType.ADDRESS, + hashPayload: false, + messageHex: request.payload, + + network: { + networkId: this.chainId.networkId, + protocolMagic: this.chainId.networkMagic + }, + preferHexDisplay: false, + signingPath + } + : { + addressFieldType: MessageAddressFieldType.KEY_HASH, + hashPayload: false, + messageHex: request.payload, + preferHexDisplay: false, + signingPath + }; const deviceConnection = await LedgerKeyAgent.checkDeviceConnection( this.#communicationType, diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts index 1609da963f4..850e4fc9ec2 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,25 @@ describe('LedgerKeyAgent', () => { ) ).toBe(true); }); + + it('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