diff --git a/packages/connect/e2e/__fixtures__/cardanoSignMessage.ts b/packages/connect/e2e/__fixtures__/cardanoSignMessage.ts new file mode 100644 index 000000000000..e870f3ee2bed --- /dev/null +++ b/packages/connect/e2e/__fixtures__/cardanoSignMessage.ts @@ -0,0 +1,42 @@ +import { 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 ASCII payload hash', + params: { + signingPath: "m/1852'/1815'/0'/0/0", + payload: '54657374', + hashPayload: true, + displayAscii: true, + }, + result: { + payload: '54657374', + signature: + 'cde9451e081f325ed9991b5c20f22c7220526f97e646abee71b8fe232e475b8b06a98df28fdec911e81a050d47c0fcbe3b629d38fc12730fb74ab0a5f56f7f05', + headers: { + protected: { + 1: ALGORITHM_IDS.EdDSA, + address: '80f9e2c88e6c817008f3a812ed889b4a4da8e0bd103f86e7335422aa', + }, + unprotected: { + hashed: true, + version: 1, + }, + }, + }, + legacyResults: [legacyResults.beforeMessageSigning], + }, + ], +}; diff --git a/packages/connect/e2e/__fixtures__/index.ts b/packages/connect/e2e/__fixtures__/index.ts index 58421102f2b6..79a663d35c0e 100644 --- a/packages/connect/e2e/__fixtures__/index.ts +++ b/packages/connect/e2e/__fixtures__/index.ts @@ -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'; @@ -105,6 +106,7 @@ let fixtures = [ cardanoGetNativeScriptHash, cardanoGetPublicKey, cardanoSignTransaction, + cardanoSignMessage, composeTransaction, eosGetPublicKey, eosSignTransaction, diff --git a/packages/connect/src/api/cardano/api/cardanoSignMessage.ts b/packages/connect/src/api/cardano/api/cardanoSignMessage.ts new file mode 100644 index 000000000000..ab0c2096956b --- /dev/null +++ b/packages/connect/src/api/cardano/api/cardanoSignMessage.ts @@ -0,0 +1,118 @@ +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; + displayAscii: boolean; + networkId?: number; + protocolMagic?: number; + addressParameters?: PROTO.CardanoAddressParametersType; + derivationType: PROTO.CardanoDerivationType; +}; + +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); + + 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, + displayAscii: payload.displayAscii, + networkId: payload.networkId, + protocolMagic: payload.protocolMagic, + addressParameters: + payload.addressParameters && addressParametersToProto(payload.addressParameters), + derivationType: payload.derivationType ?? PROTO.CardanoDerivationType.ICARUS_TREZOR, + }; + } + + async run(): Promise { + 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, + display_ascii: this.params.displayAscii, + derivation_type: this.params.derivationType, + }); + + 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'; + } +} diff --git a/packages/connect/src/api/cardano/api/index.ts b/packages/connect/src/api/cardano/api/index.ts index 0ec3de894cee..a83e79021ba2 100644 --- a/packages/connect/src/api/cardano/api/index.ts +++ b/packages/connect/src/api/cardano/api/index.ts @@ -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'; diff --git a/packages/connect/src/api/cardano/cardanoUtils.ts b/packages/connect/src/api/cardano/cardanoUtils.ts index c6f1b29440ad..0c5fddd2ff35 100644 --- a/packages/connect/src/api/cardano/cardanoUtils.ts +++ b/packages/connect/src/api/cardano/cardanoUtils.ts @@ -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; diff --git a/packages/connect/src/constants/cardano.ts b/packages/connect/src/constants/cardano.ts index db8f34f9558b..d6f16c65fcd5 100644 --- a/packages/connect/src/constants/cardano.ts +++ b/packages/connect/src/constants/cardano.ts @@ -10,3 +10,7 @@ export enum NETWORK_IDS { mainnet = 1, testnet = 0, } + +export enum ALGORITHM_IDS { + EdDSA = -8, +} diff --git a/packages/connect/src/data/config.ts b/packages/connect/src/data/config.ts index 918069cb2d60..42f04a7852eb 100644 --- a/packages/connect/src/data/config.ts +++ b/packages/connect/src/data/config.ts @@ -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' }, diff --git a/packages/connect/src/factory.ts b/packages/connect/src/factory.ts index 771c794ce954..d1557fb2a159 100644 --- a/packages/connect/src/factory.ts +++ b/packages/connect/src/factory.ts @@ -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' }), diff --git a/packages/connect/src/types/api/__tests__/cardano.ts b/packages/connect/src/types/api/__tests__/cardano.ts index caf38add058c..79075822bcbe 100644 --- a/packages/connect/src/types/api/__tests__/cardano.ts +++ b/packages/connect/src/types/api/__tests__/cardano.ts @@ -421,3 +421,39 @@ export const cardanoSignTransaction = async (api: TrezorConnect) => { } } }; + +export const cardanoSignMessage = async (api: TrezorConnect) => { + const sign = await api.cardanoSignMessage({ + signingPath: 'm/44', + payload: 'Test..', + hashPayload: true, + displayAscii: 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, + }, + }, + derivationType: PROTO.CardanoDerivationType.ICARUS_TREZOR, + }); + + 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(); + } +}; diff --git a/packages/connect/src/types/api/cardano/index.ts b/packages/connect/src/types/api/cardano/index.ts index 0af73a327abb..3e265cadf7d3 100644 --- a/packages/connect/src/types/api/cardano/index.ts +++ b/packages/connect/src/types/api/cardano/index.ts @@ -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 @@ -288,3 +288,40 @@ export const CardanoSignedTxData = Type.Object({ witnesses: Type.Array(CardanoSignedTxWitness), auxiliaryDataSupplement: Type.Optional(CardanoAuxiliaryDataSupplement), }); + +export type CardanoSignMessage = Static; +export const CardanoSignMessage = Type.Object({ + signingPath: DerivationPath, + payload: Type.String(), + hashPayload: Type.Boolean(), + displayAscii: Type.Boolean(), + networkId: Type.Optional(Type.Number()), + protocolMagic: Type.Optional(Type.Number()), + addressParameters: Type.Optional(CardanoAddressParameters), + derivationType: Type.Optional(PROTO.EnumCardanoDerivationType), +}); + +export type CardanoMessageProtectedHeaders = Static; +export const CardanoMessageProtectedHeaders = Type.Object({ + 1: Type.Literal(CARDANO.ALGORITHM_IDS.EdDSA), + address: Type.String(), +}); + +export type CardanoMessageUnprotectedHeaders = Static; +export const CardanoMessageUnprotectedHeaders = Type.Object({ + hashed: Type.Boolean(), + version: Type.Number(), +}); + +export type CardanoMessageHeaders = Static; +export const CardanoMessageHeaders = Type.Object({ + protected: CardanoMessageProtectedHeaders, + unprotected: CardanoMessageUnprotectedHeaders, +}); + +export type CardanoSignedMessage = Static; +export const CardanoSignedMessage = Type.Object({ + headers: CardanoMessageHeaders, + payload: Type.String(), + signature: Type.String(), +}); diff --git a/packages/connect/src/types/api/cardanoSignMessage.ts b/packages/connect/src/types/api/cardanoSignMessage.ts new file mode 100644 index 000000000000..cd5e7d07b18e --- /dev/null +++ b/packages/connect/src/types/api/cardanoSignMessage.ts @@ -0,0 +1,6 @@ +import type { Params, Response } from '../params'; +import type { CardanoSignMessage, CardanoSignedMessage } from './cardano'; + +export declare function cardanoSignMessage( + params: Params, +): Response; diff --git a/packages/connect/src/types/api/index.ts b/packages/connect/src/types/api/index.ts index b11596b22ea5..26421bba997a 100644 --- a/packages/connect/src/types/api/index.ts +++ b/packages/connect/src/types/api/index.ts @@ -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'; @@ -166,6 +167,8 @@ export interface TrezorConnect { cardanoComposeTransaction: typeof cardanoComposeTransaction; + cardanoSignMessage: typeof cardanoSignMessage; + // https://github.com/trezor/trezor-suite/blob/develop/docs/packages/connect/methods/changePin.md changePin: typeof changePin; diff --git a/packages/connect/src/utils/formatUtils.ts b/packages/connect/src/utils/formatUtils.ts index 78d925f7e010..492b7a9cca31 100644 --- a/packages/connect/src/utils/formatUtils.ts +++ b/packages/connect/src/utils/formatUtils.ts @@ -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; } diff --git a/packages/protobuf/messages.json b/packages/protobuf/messages.json index 2da876edbc27..9a9f4095bc78 100644 --- a/packages/protobuf/messages.json +++ b/packages/protobuf/messages.json @@ -2474,6 +2474,79 @@ "CardanoSignTxFinished": { "fields": {} }, + "CardanoSignMessageInit": { + "fields": { + "protocol_magic": { + "type": "uint32", + "id": 1 + }, + "network_id": { + "type": "uint32", + "id": 2 + }, + "signing_path": { + "rule": "repeated", + "type": "uint32", + "id": 3, + "options": { + "packed": false + } + }, + "payload_size": { + "rule": "required", + "type": "uint32", + "id": 4 + }, + "hash_payload": { + "rule": "required", + "type": "bool", + "id": 5 + }, + "display_ascii": { + "rule": "required", + "type": "bool", + "id": 6 + }, + "address_parameters": { + "type": "CardanoAddressParametersType", + "id": 7 + }, + "derivation_type": { + "rule": "required", + "type": "CardanoDerivationType", + "id": 8 + } + } + }, + "CardanoMessageItemAck": { + "fields": {} + }, + "CardanoMessagePayloadChunk": { + "fields": { + "data": { + "rule": "required", + "type": "bytes", + "id": 1 + } + } + }, + "CardanoMessageItemHostAck": { + "fields": {} + }, + "CardanoSignMessageFinished": { + "fields": { + "signature": { + "rule": "required", + "type": "bytes", + "id": 1 + }, + "address": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, "Success": { "fields": { "message": { @@ -7154,6 +7227,19 @@ } } }, + "StellarClaimClaimableBalanceOp": { + "fields": { + "source_account": { + "type": "string", + "id": 1 + }, + "balance_id": { + "rule": "required", + "type": "bytes", + "id": 2 + } + } + }, "StellarSignedTx": { "fields": { "public_key": { @@ -8258,6 +8344,9 @@ "MessageType_StellarPathPaymentStrictSendOp": { "(wire_in)": true }, + "MessageType_StellarClaimClaimableBalanceOp": { + "(wire_in)": true + }, "MessageType_StellarSignedTx": { "(wire_out)": true }, @@ -8348,6 +8437,21 @@ "MessageType_CardanoTxReferenceInput": { "(wire_in)": true }, + "MessageType_CardanoSignMessageInit": { + "(wire_in)": true + }, + "MessageType_CardanoMessagePayloadChunk": { + "(wire_in)": true + }, + "MessageType_CardanoMessageItemAck": { + "(wire_out)": true + }, + "MessageType_CardanoMessageItemHostAck": { + "(wire_in)": true + }, + "MessageType_CardanoSignMessageFinished": { + "(wire_out)": true + }, "MessageType_RippleGetAddress": { "(wire_in)": true }, @@ -8691,6 +8795,7 @@ "MessageType_StellarBumpSequenceOp": 221, "MessageType_StellarManageBuyOfferOp": 222, "MessageType_StellarPathPaymentStrictSendOp": 223, + "MessageType_StellarClaimClaimableBalanceOp": 225, "MessageType_StellarSignedTx": 230, "MessageType_CardanoGetPublicKey": 305, "MessageType_CardanoPublicKey": 306, @@ -8721,6 +8826,11 @@ "MessageType_CardanoTxInlineDatumChunk": 335, "MessageType_CardanoTxReferenceScriptChunk": 336, "MessageType_CardanoTxReferenceInput": 337, + "MessageType_CardanoSignMessageInit": 338, + "MessageType_CardanoMessagePayloadChunk": 339, + "MessageType_CardanoMessageItemAck": 340, + "MessageType_CardanoMessageItemHostAck": 341, + "MessageType_CardanoSignMessageFinished": 342, "MessageType_RippleGetAddress": 400, "MessageType_RippleAddress": 401, "MessageType_RippleSignTx": 402, diff --git a/packages/protobuf/src/messages-schema.ts b/packages/protobuf/src/messages-schema.ts index 5ee30e4d4878..a4e97823d6a3 100644 --- a/packages/protobuf/src/messages-schema.ts +++ b/packages/protobuf/src/messages-schema.ts @@ -1030,6 +1030,35 @@ export const CardanoTxBodyHash = Type.Object({ export type CardanoSignTxFinished = Static; export const CardanoSignTxFinished = Type.Object({}); +export type CardanoSignMessageInit = Static; +export const CardanoSignMessageInit = Type.Object({ + protocol_magic: Type.Optional(Type.Number()), + network_id: Type.Optional(Type.Number()), + signing_path: Type.Array(Type.Number()), + payload_size: Type.Number(), + hash_payload: Type.Boolean(), + display_ascii: Type.Boolean(), + address_parameters: Type.Optional(CardanoAddressParametersType), + derivation_type: EnumCardanoDerivationType, +}); + +export type CardanoMessageItemAck = Static; +export const CardanoMessageItemAck = Type.Object({}); + +export type CardanoMessagePayloadChunk = Static; +export const CardanoMessagePayloadChunk = Type.Object({ + data: Type.String(), +}); + +export type CardanoMessageItemHostAck = Static; +export const CardanoMessageItemHostAck = Type.Object({}); + +export type CardanoSignMessageFinished = Static; +export const CardanoSignMessageFinished = Type.Object({ + signature: Type.String(), + address: Type.String(), +}); + export type Success = Static; export const Success = Type.Object({ message: Type.String(), @@ -2685,6 +2714,11 @@ export const MessageType = Type.Object({ CardanoTxHostAck, CardanoTxBodyHash, CardanoSignTxFinished, + CardanoSignMessageInit, + CardanoMessageItemAck, + CardanoMessagePayloadChunk, + CardanoMessageItemHostAck, + CardanoSignMessageFinished, Success, Failure, ButtonRequest, diff --git a/packages/protobuf/src/messages.ts b/packages/protobuf/src/messages.ts index e03dfefeb39f..9a6836d5e649 100644 --- a/packages/protobuf/src/messages.ts +++ b/packages/protobuf/src/messages.ts @@ -943,6 +943,35 @@ export type CardanoTxBodyHash = { // CardanoSignTxFinished export type CardanoSignTxFinished = {}; +// CardanoSignMessageInit +export type CardanoSignMessageInit = { + protocol_magic?: number; + network_id?: number; + signing_path: number[]; + payload_size: number; + hash_payload: boolean; + display_ascii: boolean; + address_parameters?: CardanoAddressParametersType; + derivation_type: CardanoDerivationType; +}; + +// CardanoMessageItemAck +export type CardanoMessageItemAck = {}; + +// CardanoMessagePayloadChunk +export type CardanoMessagePayloadChunk = { + data: string; +}; + +// CardanoMessageItemHostAck +export type CardanoMessageItemHostAck = {}; + +// CardanoSignMessageFinished +export type CardanoSignMessageFinished = { + signature: string; + address: string; +}; + // Success export type Success = { message: string; @@ -2225,6 +2254,12 @@ export type StellarBumpSequenceOp = { bump_to: UintType; }; +// StellarClaimClaimableBalanceOp +export type StellarClaimClaimableBalanceOp = { + source_account?: string; + balance_id: string; +}; + // StellarSignedTx export type StellarSignedTx = { public_key: string; @@ -2462,6 +2497,11 @@ export type MessageType = { CardanoTxHostAck: CardanoTxHostAck; CardanoTxBodyHash: CardanoTxBodyHash; CardanoSignTxFinished: CardanoSignTxFinished; + CardanoSignMessageInit: CardanoSignMessageInit; + CardanoMessageItemAck: CardanoMessageItemAck; + CardanoMessagePayloadChunk: CardanoMessagePayloadChunk; + CardanoMessageItemHostAck: CardanoMessageItemHostAck; + CardanoSignMessageFinished: CardanoSignMessageFinished; Success: Success; Failure: Failure; ButtonRequest: ButtonRequest; @@ -2619,6 +2659,7 @@ export type MessageType = { StellarAccountMergeOp: StellarAccountMergeOp; StellarManageDataOp: StellarManageDataOp; StellarBumpSequenceOp: StellarBumpSequenceOp; + StellarClaimClaimableBalanceOp: StellarClaimClaimableBalanceOp; StellarSignedTx: StellarSignedTx; TezosGetAddress: TezosGetAddress; TezosAddress: TezosAddress;