Skip to content

Commit

Permalink
Merge pull request #1551 from input-output-hk/fix/lw-11831-cip95-sign…
Browse files Browse the repository at this point in the history
…-data

Fix/lw 11831 cip95 sign data
  • Loading branch information
mchappell authored Jan 2, 2025
2 parents 90c0b02 + 99a5930 commit 6b9a5fb
Show file tree
Hide file tree
Showing 21 changed files with 438 additions and 141 deletions.
17 changes: 9 additions & 8 deletions packages/core/src/Cardano/Address/DRepID.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as BaseEncoding from '@scure/base';
import { Address, AddressType, Credential, CredentialType } from './Address';
import { EnterpriseAddress } from './EnterpriseAddress';
import { Hash28ByteBase16 } from '@cardano-sdk/crypto';
import { OpaqueString, typedBech32 } from '@cardano-sdk/util';

Expand Down Expand Up @@ -29,14 +30,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) {
Expand Down Expand Up @@ -106,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();
};
1 change: 0 additions & 1 deletion packages/core/src/Cardano/Address/PaymentAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export type PaymentAddress = OpaqueString<'PaymentAddress'>;

/**
* @param {string} address mainnet or testnet address
* @throws InvalidStringError
*/
export const isRewardAccount = (address: string) => {
try {
Expand Down
40 changes: 33 additions & 7 deletions packages/core/test/Cardano/Address/DRepID.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Cardano } from '../../../src';
import { Credential, CredentialType, DRepID } from '../../../src/Cardano';
import { InvalidStringError } from '@cardano-sdk/util';

Expand Down Expand Up @@ -51,7 +52,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', () => {
Expand All @@ -62,19 +63,44 @@ 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);
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('is false if DRepID is not a type 6 address', () => {
expect(DRepID.canSign('drep1wpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9qcluy2z')).toBe(false);

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);
});
});
});
9 changes: 6 additions & 3 deletions packages/dapp-connector/src/WalletApi/Cip30Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const CipMethodsMapping: Record<number, WalletMethod[]> = {
'signData',
'submitTx'
],
95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey']
95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey', 'signData']
};
export const WalletApiMethodNames: WalletMethod[] = Object.values(CipMethodsMapping).flat();

Expand Down Expand Up @@ -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)
};
Expand All @@ -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)
}
};

Expand Down
3 changes: 2 additions & 1 deletion packages/dapp-connector/src/WalletApi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export type SignTx = (tx: Cbor, partialSign?: Boolean) => Promise<Cbor>;
* @throws DataSignError
*/
export type SignData = (
addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes,
addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes,
payload: Bytes
) => Promise<Cip30DataSignature>;

Expand Down Expand Up @@ -203,6 +203,7 @@ export interface Cip95WalletApi {
getRegisteredPubStakeKeys: () => Promise<Ed25519PublicKeyHex[]>;
getUnregisteredPubStakeKeys: () => Promise<Ed25519PublicKeyHex[]>;
getPubDRepKey: () => Promise<Ed25519PublicKeyHex>;
signData: SignData;
}

export type WalletApi = Cip30WalletApi & Cip95WalletApi;
Expand Down
1 change: 1 addition & 0 deletions packages/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
119 changes: 119 additions & 0 deletions packages/e2e/test/wallet_epoch_0/PersonalWallet/cip30WalletApi.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof cip30.createWalletApi>;
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);
});
});
2 changes: 0 additions & 2 deletions packages/e2e/test/web-extension/extension/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 0 additions & 2 deletions packages/e2e/test/web-extension/extension/ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ <h3>Delegation distribution:</h3>
<button id="buildAndSignTx">Build & Sign TX</button>
<div>Signature: <span id="signature">-</span></div>

<button id="signDataWithDRepId">Sign Data with DRepID</button>
<div>Signature: <span id="dataSignature">-</span></div>
<script src="ui.js"></script>
</body>

Expand Down
21 changes: 0 additions & 21 deletions packages/e2e/test/web-extension/extension/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,23 +124,6 @@ const sendDelegationTx = async (portfolio: { pool: Cardano.StakePool; weight: nu
document.querySelector('#multiDelegation .delegateTxId')!.textContent = msg;
};

const signDataWithDRepID = async (): Promise<void> => {
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;
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 1 addition & 8 deletions packages/e2e/test/web-extension/specs/wallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 6b9a5fb

Please sign in to comment.