Skip to content

Commit

Permalink
feat(connect): Cardano message signing
Browse files Browse the repository at this point in the history
  • Loading branch information
jaskp committed Jan 29, 2024
1 parent bb37005 commit a873ff2
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 3 deletions.
44 changes: 44 additions & 0 deletions packages/connect/e2e/__fixtures__/cardanoSignMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { NETWORK_IDS, PROTOCOL_MAGICS, ALGORITHM_IDS } from '../../src/constants/cardano';

const legacyResults = {
beforeMessageSigning: {
rules: ['<2.6.5', '1'],
success: false,
},
};

export default {
method: 'cardanoSignMessage',
setup: {
mnemonic: 'mnemonic_all',
},
tests: [
{
description: 'Sign short payload hash',
params: {
signingPath: "m/1852'/1815'/0'/0/0",
payload: '54657374',
hashPayload: true,
protocolMagic: PROTOCOL_MAGICS.mainnet,
networkId: NETWORK_IDS.mainnet,
keyPath: "m/1852'/1815'/0'/0/0",
},
result: {
payload: '54657374',
signature:
'cde9451e081f325ed9991b5c20f22c7220526f97e646abee71b8fe232e475b8b06a98df28fdec911e81a050d47c0fcbe3b629d38fc12730fb74ab0a5f56f7f05',
headers: {
protected: {
1: ALGORITHM_IDS.EdDSA,
address: '80f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa',
},
unprotected: {
hashed: true,
version: 1,
},
},
},
legacyResults: [legacyResults.beforeMessageSigning],
},
],
};
2 changes: 2 additions & 0 deletions packages/connect/e2e/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import cardanoGetAddressDerivations from './cardanoGetAddressDerivations';
import cardanoGetNativeScriptHash from './cardanoGetNativeScriptHash';
import cardanoGetPublicKey from './cardanoGetPublicKey';
import cardanoSignTransaction from './cardanoSignTransaction';
import cardanoSignMessage from './cardanoSignMessage';
import composeTransaction from './composeTransaction';
import eosGetPublicKey from './eosGetPublicKey';
import eosSignTransaction from './eosSignTransaction';
Expand Down Expand Up @@ -105,6 +106,7 @@ let fixtures = [
cardanoGetNativeScriptHash,
cardanoGetPublicKey,
cardanoSignTransaction,
cardanoSignMessage,
composeTransaction,
eosGetPublicKey,
eosSignTransaction,
Expand Down
124 changes: 124 additions & 0 deletions packages/connect/src/api/cardano/api/cardanoSignMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { AbstractMethod } from '../../../core/AbstractMethod';
import { PROTO, CARDANO, ERRORS } from '../../../constants';
import { getFirmwareRange } from '../../common/paramsValidator';
import { getMiscNetwork } from '../../../data/coinInfo';
import { Path } from '../cardanoInputs';
import { validatePath } from '../../../utils/pathUtils';
import { hexStringByteLength, sendChunkedHexString } from '../cardanoUtils';
import {
CardanoSignMessage as CardanoSignMessageSchema,
CardanoMessageHeaders,
CardanoSignedMessage,
} from '../../../types/api/cardano';
import { addressParametersToProto } from '../cardanoAddressParameters';
import { Assert } from '@trezor/schema-utils';
import { hasHexPrefix, isHexString } from '../../../utils/formatUtils';

export type CardanoSignMessageParams = {
signingPath: Path;
payload: string;
hashPayload: boolean;
networkId: number;
protocolMagic: number;
addressParameters?: PROTO.CardanoAddressParametersType;
keyPath?: Path;
derivationType: PROTO.CardanoDerivationType;
preferHexDisplay?: boolean;
};

export default class CardanoSignMessage extends AbstractMethod<
'cardanoSignMessage',
CardanoSignMessageParams
> {
static readonly VERSION = 1;

init(): void {
this.requiredPermissions = ['read', 'write'];
this.firmwareRange = getFirmwareRange(
this.name,
getMiscNetwork('Cardano'),
this.firmwareRange,
);

const { payload } = this;

Assert(CardanoSignMessageSchema, payload);

// TODO: Should we rather allow textual payloads and convert them to hex?
// Also, it would be nicer to validate this in the schema by extending typebox
// with Type.String({ format: 'hex' }), but I guess that's out of scope.
if (!isHexString(payload.payload) || hasHexPrefix(payload.payload)) {
throw ERRORS.TypedError(
'Method_InvalidParameter',
'Message payload must be a hexadecimal string without a "0x" prefix.',
);
}

this.params = {
signingPath: validatePath(payload.signingPath, 5),
payload: payload.payload,
hashPayload: payload.hashPayload,
networkId: payload.networkId,
protocolMagic: payload.protocolMagic,
addressParameters:
payload.addressParameters && addressParametersToProto(payload.addressParameters),
keyPath: payload.keyPath != null ? validatePath(payload.keyPath, 5) : undefined,
derivationType: payload.derivationType ?? PROTO.CardanoDerivationType.ICARUS_TREZOR,
preferHexDisplay: payload.preferHexDisplay,
};
}

async run(): Promise<CardanoSignedMessage> {
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());

const payloadSize = hexStringByteLength(this.params.payload);
const MAX_CHUNK_SIZE = 1024 * 2; // 1024 hex-encoded bytes

await typedCall('CardanoSignMessageInit', 'CardanoMessageItemAck', {
signing_path: this.params.signingPath,
payload_size: payloadSize,
hash_payload: this.params.hashPayload,
network_id: this.params.networkId,
protocol_magic: this.params.protocolMagic,
address_parameters: this.params.addressParameters,
key_path: this.params.keyPath ?? [],
derivation_type: this.params.derivationType,
prefer_hex_display: this.params.preferHexDisplay,
});

await sendChunkedHexString(
typedCall,
this.params.payload,
MAX_CHUNK_SIZE,
'CardanoMessagePayloadChunk',
'CardanoMessageItemAck',
);

const {
message: { signature, address },
} = await typedCall('CardanoMessageItemHostAck', 'CardanoSignMessageFinished');

return {
signature,
payload: this.params.payload,
headers: this._createHeaders(address),
};
}

_createHeaders(address: string): CardanoMessageHeaders {
return {
protected: {
1: CARDANO.ALGORITHM_IDS.EdDSA,
address,
},
unprotected: {
hashed: this.params.hashPayload,
version: CardanoSignMessage.VERSION,
},
};
}

get info() {
return 'Sign Cardano message';
}
}
1 change: 1 addition & 0 deletions packages/connect/src/api/cardano/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as cardanoGetNativeScriptHash } from './cardanoGetNativeScriptH
export { default as cardanoGetPublicKey } from './cardanoGetPublicKey';
export { default as cardanoSignTransaction } from './cardanoSignTransaction';
export { default as cardanoComposeTransaction } from './cardanoComposeTransaction';
export { default as cardanoSignMessage } from './cardanoSignMessage';
3 changes: 2 additions & 1 deletion packages/connect/src/api/cardano/cardanoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ export const sendChunkedHexString = async (
data: string,
chunkSize: number,
messageType: string,
responseType = 'CardanoTxItemAck',
) => {
let processedSize = 0;
while (processedSize < data.length) {
const chunk = data.slice(processedSize, processedSize + chunkSize);
await typedCall(messageType, 'CardanoTxItemAck', {
await typedCall(messageType, responseType, {
data: chunk,
});
processedSize += chunkSize;
Expand Down
4 changes: 4 additions & 0 deletions packages/connect/src/constants/cardano.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export enum NETWORK_IDS {
mainnet = 1,
testnet = 0,
}

export enum ALGORITHM_IDS {
EdDSA = -8,
}
5 changes: 5 additions & 0 deletions packages/connect/src/data/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ export const config = {
min: { T1B1: '0', T2T1: '2.4.3' },
comment: ['Cardano GetNativeScriptHash call added in 2.4.3'],
},
{
methods: ['cardanoSignMessage'],
min: { T1B1: '0', T2T1: '2.6.5' },
comment: ['Cardano SignMessage call added in 2.6.5'],
},
{
methods: ['tezosSignTransaction'],
min: { T1B1: '0', T2T1: '2.1.8' },
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ export const factory = ({
cardanoComposeTransaction: params =>
call({ ...params, method: 'cardanoComposeTransaction' }),

cardanoSignMessage: params => call({ ...params, method: 'cardanoSignMessage' }),

cipherKeyValue: params => call({ ...params, method: 'cipherKeyValue' }),

composeTransaction: params => call({ ...params, method: 'composeTransaction' }),
Expand Down
37 changes: 37 additions & 0 deletions packages/connect/src/types/api/__tests__/cardano.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,40 @@ export const cardanoSignTransaction = async (api: TrezorConnect) => {
}
}
};

export const cardanoSignMessage = async (api: TrezorConnect) => {
const sign = await api.cardanoSignMessage({
signingPath: 'm/44',
payload: 'Test..',
hashPayload: true,
networkId: 0,
protocolMagic: 0,
addressParameters: {
addressType: CardanoAddressType.BASE,
path: 'm/44',
stakingPath: 'm/44',
stakingKeyHash: 'aaff00..',
stakingScriptHash: 'aaff00..',
paymentScriptHash: 'aaff00..',
certificatePointer: {
blockIndex: 0,
txIndex: 0,
certificateIndex: 0,
},
},
keyPath: 'm/44',
derivationType: PROTO.CardanoDerivationType.ICARUS_TREZOR,
preferHexDisplay: true,
});

if (sign.success) {
const { payload } = sign;
payload.payload.toLowerCase();
payload.signature.toLowerCase();
const { headers } = payload;
headers.protected[1].toFixed();
headers.protected.address.toLowerCase();
[true, false].includes(headers.unprotected.hashed);
headers.unprotected.version.toFixed();
}
};
40 changes: 39 additions & 1 deletion packages/connect/src/types/api/cardano/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Type, Static } from '@trezor/schema-utils';
import { PROTO } from '../../../constants';
import { PROTO, CARDANO } from '../../../constants';
import { GetPublicKey, PublicKey, DerivationPath } from '../../params';

// cardanoGetAddress
Expand Down Expand Up @@ -288,3 +288,41 @@ export const CardanoSignedTxData = Type.Object({
witnesses: Type.Array(CardanoSignedTxWitness),
auxiliaryDataSupplement: Type.Optional(CardanoAuxiliaryDataSupplement),
});

export type CardanoSignMessage = Static<typeof CardanoSignMessage>;
export const CardanoSignMessage = Type.Object({
signingPath: DerivationPath,
payload: Type.String(),
hashPayload: Type.Boolean(),
networkId: Type.Number(),
protocolMagic: Type.Number(),
keyPath: Type.Optional(DerivationPath),
addressParameters: Type.Optional(CardanoAddressParameters),
derivationType: Type.Optional(PROTO.EnumCardanoDerivationType),
preferHexDisplay: Type.Optional(Type.Boolean()),
});

export type CardanoMessageProtectedHeaders = Static<typeof CardanoMessageProtectedHeaders>;
export const CardanoMessageProtectedHeaders = Type.Object({
1: Type.Literal(CARDANO.ALGORITHM_IDS.EdDSA),
address: Type.String(),
});

export type CardanoMessageUnprotectedHeaders = Static<typeof CardanoMessageUnprotectedHeaders>;
export const CardanoMessageUnprotectedHeaders = Type.Object({
hashed: Type.Boolean(),
version: Type.Number(),
});

export type CardanoMessageHeaders = Static<typeof CardanoMessageHeaders>;
export const CardanoMessageHeaders = Type.Object({
protected: CardanoMessageProtectedHeaders,
unprotected: CardanoMessageUnprotectedHeaders,
});

export type CardanoSignedMessage = Static<typeof CardanoSignedMessage>;
export const CardanoSignedMessage = Type.Object({
headers: CardanoMessageHeaders,
payload: Type.String(),
signature: Type.String(),
});
6 changes: 6 additions & 0 deletions packages/connect/src/types/api/cardanoSignMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Params, Response } from '../params';
import type { CardanoSignMessage, CardanoSignedMessage } from './cardano';

export declare function cardanoSignMessage(
params: Params<CardanoSignMessage>,
): Response<CardanoSignedMessage>;
4 changes: 4 additions & 0 deletions packages/connect/src/types/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { cardanoGetAddress } from './cardanoGetAddress';
import { cardanoGetNativeScriptHash } from './cardanoGetNativeScriptHash';
import { cardanoGetPublicKey } from './cardanoGetPublicKey';
import { cardanoSignTransaction } from './cardanoSignTransaction';
import { cardanoSignMessage } from './cardanoSignMessage';
import { cardanoComposeTransaction } from './cardanoComposeTransaction';
import { changePin } from './changePin';
import { changeWipeCode } from './changeWipeCode';
Expand Down Expand Up @@ -166,6 +167,9 @@ export interface TrezorConnect {

cardanoComposeTransaction: typeof cardanoComposeTransaction;

// todo: link docs
cardanoSignMessage: typeof cardanoSignMessage;

// https://github.com/trezor/trezor-suite/blob/develop/docs/packages/connect/methods/changePin.md
changePin: typeof changePin;

Expand Down
2 changes: 1 addition & 1 deletion packages/connect/src/utils/formatUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const stripHexPrefix = (str: string) => (hasHexPrefix(str) ? str.slice(2)
export const addHexPrefix = (str: string) => (str && !hasHexPrefix(str) ? `0x${str}` : str);

// from (isHexString) https://github.com/ethjs/ethjs-util/blob/master/src/index.js
const isHexString = (value: string, length?: number) => {
export const isHexString = (value: string, length?: number) => {
if (typeof value !== 'string' || !value.match(/^(0x|0X)?[0-9A-Fa-f]*$/)) {
return false;
}
Expand Down
Loading

0 comments on commit a873ff2

Please sign in to comment.