From 5c66eb6a95a5a10b650404ee77bfbf1313b3437a Mon Sep 17 00:00:00 2001 From: Jan Mazak Date: Mon, 5 Feb 2024 11:48:39 +0100 Subject: [PATCH] feature: message signing (CIP-8) --- CHANGELOG.md | 10 + package.json | 2 +- src/Ada.ts | 30 +++ src/errors/invalidDataReason.ts | 4 + src/interactions/common/ins.ts | 1 + src/interactions/getVersion.ts | 4 + src/interactions/serialization/messageData.ts | 44 ++++ src/interactions/signMessage.ts | 134 +++++++++++ src/parsing/messageData.ts | 77 ++++++ src/types/internal.ts | 18 ++ src/types/public.ts | 60 +++++ test/integration/__fixtures__/signMessage.ts | 224 ++++++++++++++++++ test/integration/getVersion.test.ts | 3 +- test/integration/signMessage.test.ts | 28 +++ 14 files changed, 637 insertions(+), 2 deletions(-) create mode 100644 src/interactions/serialization/messageData.ts create mode 100644 src/interactions/signMessage.ts create mode 100644 src/parsing/messageData.ts create mode 100644 test/integration/__fixtures__/signMessage.ts create mode 100644 test/integration/signMessage.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ac11d8..08ca6c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + +## [7.1.0](TBD) - [TBD] + +Message signing (CIP-8) + +### Added + +- support for message signing (CIP-8, CIP-30) + + ## [7.0.1](TBD) - [TBD] ### Changed diff --git a/package.json b/package.json index d92b82eb..ea1d4b78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cardano-foundation/ledgerjs-hw-app-cardano", - "version": "7.0.1", + "version": "7.1.0", "files": [ "dist" ], diff --git a/src/Ada.ts b/src/Ada.ts index aae568a4..026533f8 100644 --- a/src/Ada.ts +++ b/src/Ada.ts @@ -30,6 +30,8 @@ import {runTests} from './interactions/runTests' import {showAddress} from './interactions/showAddress' import {signCVote} from './interactions/signCVote' import {signOperationalCertificate} from './interactions/signOperationalCertificate' +import {signMessage} from './interactions/signMessage' +import {parseMessageData} from './parsing/messageData' import {signTransaction} from './interactions/signTx' import {parseAddress} from './parsing/address' import {parseCVote} from './parsing/cVote' @@ -42,6 +44,7 @@ import {parseSignTransactionRequest} from './parsing/transaction' import type { ParsedAddressParams, ParsedCVote, + ParsedMessageData, ParsedNativeScript, ParsedOperationalCertificate, ParsedSigningRequest, @@ -54,6 +57,8 @@ import type { DeviceCompatibility, DeviceOwnedAddress, ExtendedPublicKey, + MessageData, + SignedMessageData, NativeScript, NativeScriptHash, NativeScriptHashDisplayFormat, @@ -367,6 +372,18 @@ export class Ada { return yield* signOperationalCertificate(version, request) } + async signMessage(request: SignMessageRequest): Promise { + const parsedMsgData = parseMessageData(request) + + return interact(this._signMessage(parsedMsgData), this._send) + } + + /** @ignore */ + *_signMessage(request: ParsedMessageData): Interaction { + const version = yield* getVersion() + return yield* signMessage(version, request) + } + async signCIP36Vote( request: SignCIP36VoteRequest, ): Promise { @@ -500,6 +517,19 @@ export type SignOperationalCertificateRequest = OperationalCertificate */ export type SignOperationalCertificateResponse = OperationalCertificateSignature +/** + * Sign CIP-8 message ([[Ada.signMessage]]) request data + * @category Main + * @see [[SignMessageResponse]] + */ +export type SignMessageRequest = MessageData +/** + * Sign CIP-8 message ([[Ada.signMessage]]) response data + * @category Main + * @see [[SignMessageRequest]] + */ +export type SignMessageResponse = SignedMessageData + /** * Sign CIP36 vote ([[Ada.signCIP36Vote]]) request data * @category Main diff --git a/src/errors/invalidDataReason.ts b/src/errors/invalidDataReason.ts index 9b7b17d8..8eaa05f8 100644 --- a/src/errors/invalidDataReason.ts +++ b/src/errors/invalidDataReason.ts @@ -227,6 +227,10 @@ export enum InvalidDataReason { OPERATIONAL_CERTIFICATE_INVALID_ISSUE_COUNTER = 'invalid operational certificate issue counter', OPERATIONAL_CERTIFICATE_INVALID_COLD_KEY_PATH = 'invalid operational certificate cold key path', + MESSAGE_DATA_INVALID_WITNESS_PATH = 'CIP-8 message signing: invalid witness path', + MESSAGE_DATA_INVALID_MESSAGE_HEX = 'CIP-8 message signing: invalid message hex string', + MESSAGE_DATA_LONG_NON_HASHED_MSG = 'CIP-8 message signing: non-hashed message too long', + CVOTE_INVALID_VOTECAST_DATA = 'invalid votecast data for CIP36 vote', CVOTE_INVALID_WITNESS = 'invalid witness for CIP36 vote', diff --git a/src/interactions/common/ins.ts b/src/interactions/common/ins.ts index 16a7e70f..4ffcc1e7 100644 --- a/src/interactions/common/ins.ts +++ b/src/interactions/common/ins.ts @@ -9,6 +9,7 @@ export const enum INS { SIGN_TX = 0x21, SIGN_OPERATIONAL_CERTIFICATE = 0x22, SIGN_CIP36_VOTE = 0x23, + SIGN_MESSAGE = 0x24, RUN_TESTS = 0xf0, } diff --git a/src/interactions/getVersion.ts b/src/interactions/getVersion.ts index 62ede5e1..d5adc313 100644 --- a/src/interactions/getVersion.ts +++ b/src/interactions/getVersion.ts @@ -88,6 +88,9 @@ export function getCompatibility(version: Version): DeviceCompatibility { const v7_0 = isLedgerAppVersionAtLeast(version, 7, 0) && isLedgerAppVersionAtMost(version, 7, Infinity) + const v7_1 = + isLedgerAppVersionAtLeast(version, 7, 1) && + isLedgerAppVersionAtMost(version, 7, Infinity) const isAppXS = version.flags.isAppXS @@ -110,6 +113,7 @@ export function getCompatibility(version: Version): DeviceCompatibility { supportsBabbage: v5_0, supportsCIP36Vote: v6_0, supportsConway: v7_0, + supportsMessageSigning: v7_1, } } diff --git a/src/interactions/serialization/messageData.ts b/src/interactions/serialization/messageData.ts new file mode 100644 index 00000000..f2554e19 --- /dev/null +++ b/src/interactions/serialization/messageData.ts @@ -0,0 +1,44 @@ +import type {Version} from '../../types/public' +import {MessageAddressFieldType} from '../../types/public' +import type {ParsedMessageData, Uint32_t, Uint8_t} from '../../types/internal' +import {path_to_buf, uint32_to_buf, uint8_to_buf} from '../../utils/serialize' +import {serializeAddressParams} from './addressParams' + +export function serializeMessageDataInit( + version: Version, + msgData: ParsedMessageData, +): Buffer { + const msgLengthBuffer = uint32_to_buf( + (msgData.messageHex.length / 2) as Uint32_t, + ) + + const hashPayloadBuffer = msgData.hashPayload + ? uint8_to_buf(1 as Uint8_t) + : uint8_to_buf(0 as Uint8_t) + + const isAsciiBuffer = msgData.isAscii + ? uint8_to_buf(1 as Uint8_t) + : uint8_to_buf(0 as Uint8_t) + + const addressFieldTypeEncoding = { + [MessageAddressFieldType.ADDRESS]: 0x01, + [MessageAddressFieldType.KEYHASH]: 0x02, + } as const + const addressFieldTypeBuffer = uint8_to_buf( + addressFieldTypeEncoding[msgData.addressFieldType] as Uint8_t, + ) + + const addressBuffer = + msgData.addressFieldType === MessageAddressFieldType.ADDRESS + ? serializeAddressParams(msgData.address, version) + : Buffer.concat([]) + + return Buffer.concat([ + msgLengthBuffer, + path_to_buf(msgData.signingPath), + hashPayloadBuffer, + isAsciiBuffer, + addressFieldTypeBuffer, + addressBuffer, + ]) +} diff --git a/src/interactions/signMessage.ts b/src/interactions/signMessage.ts new file mode 100644 index 00000000..b34808a7 --- /dev/null +++ b/src/interactions/signMessage.ts @@ -0,0 +1,134 @@ +import {buf_to_uint32, hex_to_buf, uint32_to_buf} from '../utils/serialize' +import {DeviceVersionUnsupported, InvalidDataReason} from '../errors' +import type {ParsedMessageData} from '../types/internal' +import { + ED25519_SIGNATURE_LENGTH, + PUBLIC_KEY_LENGTH, + Uint32_t, +} from '../types/internal' +import type {SignedMessageData, Version} from '../types/public' +import {getVersionString} from '../utils' +import {INS} from './common/ins' +import type {Interaction, SendParams} from './common/types' +import {getCompatibility} from './getVersion' +import {serializeMessageDataInit} from './serialization/messageData' +import {validate} from '../utils/parse' + +const send = (params: { + p1: number + p2: number + data: Buffer + expectedResponseLength?: number +}): SendParams => ({ins: INS.SIGN_MESSAGE, ...params}) + +export function* signMessage( + version: Version, + msgData: ParsedMessageData, +): Interaction { + if (!getCompatibility(version).supportsMessageSigning) { + throw new DeviceVersionUnsupported( + `CIP-8 message signing not supported by Ledger app version ${getVersionString( + version, + )}.`, + ) + } + + const enum P1 { + STAGE_INIT = 0x01, + STAGE_CHUNK = 0x02, + STAGE_CONFIRM = 0x03, + } + const enum P2 { + UNUSED = 0x00, + } + + // INIT + yield send({ + p1: P1.STAGE_INIT, + p2: P2.UNUSED, + data: serializeMessageDataInit(version, msgData), + expectedResponseLength: 0, + }) + + // CHUNK + const MAX_CIP8_MSG_FIRST_CHUNK_ASCII_SIZE = 198 + const MAX_CIP8_MSG_FIRST_CHUNK_HEX_SIZE = 99 + const MAX_CIP8_MSG_HIDDEN_CHUNK_SIZE = 250 + + const msgBytes = hex_to_buf(msgData.messageHex) + + const getChunkData = (start: number, end: number) => { + const chunk = msgBytes.slice(start, end) + return Buffer.concat([uint32_to_buf(chunk.length as Uint32_t), chunk]) + } + + const firstChunkSize = msgData.isAscii + ? MAX_CIP8_MSG_FIRST_CHUNK_ASCII_SIZE + : MAX_CIP8_MSG_FIRST_CHUNK_HEX_SIZE + + let start = 0 + let end = Math.min(msgBytes.length, firstChunkSize) + + yield send({ + p1: P1.STAGE_CHUNK, + p2: P2.UNUSED, + data: getChunkData(start, end), + expectedResponseLength: 0, + }) + start = end + + if (start < msgBytes.length) { + // non-hashed messages must be processed in a single APDU + validate( + msgData.hashPayload, + InvalidDataReason.MESSAGE_DATA_LONG_NON_HASHED_MSG, + ) + } + while (start < msgBytes.length) { + end = Math.min(msgBytes.length, start + MAX_CIP8_MSG_HIDDEN_CHUNK_SIZE) + + yield send({ + p1: P1.STAGE_CHUNK, + p2: P2.UNUSED, + data: getChunkData(start, end), + expectedResponseLength: 0, + }) + + start = end + } + + // CONFIRM + const MAX_ADDRESS_SIZE = 128 + + const confirmResponse = yield send({ + p1: P1.STAGE_CONFIRM, + p2: P2.UNUSED, + data: Buffer.concat([]), + expectedResponseLength: + ED25519_SIGNATURE_LENGTH + PUBLIC_KEY_LENGTH + 4 + MAX_ADDRESS_SIZE, + }) + + let s = 0 + const signatureHex = confirmResponse + .slice(s, s + ED25519_SIGNATURE_LENGTH) + .toString('hex') + s += ED25519_SIGNATURE_LENGTH + + const signingPublicKeyHex = confirmResponse + .slice(s, s + PUBLIC_KEY_LENGTH) + .toString('hex') + s += PUBLIC_KEY_LENGTH + + const addressFieldSizeBuf = confirmResponse.slice(s, s + 4) + s += 4 + const addressFieldSize = buf_to_uint32(addressFieldSizeBuf) + const addressFieldHex = confirmResponse + .slice(s, s + addressFieldSize) + .toString('hex') + + return { + signatureHex, + signingPublicKeyHex, + addressFieldHex, + } +} diff --git a/src/parsing/messageData.ts b/src/parsing/messageData.ts new file mode 100644 index 00000000..a451dab5 --- /dev/null +++ b/src/parsing/messageData.ts @@ -0,0 +1,77 @@ +import {unreachable} from '../utils/assert' +import {InvalidDataReason} from '../errors/invalidDataReason' +import type {ParsedMessageData} from '../types/internal' +import type {MessageData} from '../types/public' +import {MessageAddressFieldType} from '../types/public' +import {parseBIP32Path, parseHexString} from '../utils/parse' +import {parseAddress} from './address' + +// check if a non-null-terminated buffer contains printable ASCII between 32 and 126 (inclusive) +// copied from Ledger app +function isPrintableAscii(buffer: Buffer): boolean { + for (let i = 0; i < buffer.length; i++) { + if (buffer[i] > 126) return false + if (buffer[i] < 32) return false + } + + return true +} + +// check if the string can be unambiguously displayed to the user +// copied from Ledger app +function isAscii(msg: string): boolean { + const buffer = Buffer.from(msg, 'hex') + + // must not be empty + if (buffer.length === 0) return false + + // no non-printable characters except spaces + if (!isPrintableAscii(buffer)) return false + + const space = ' '.charCodeAt(0) + + // no leading spaces + if (buffer[0] === space) return false + + // no trailing spaces + if (buffer[buffer.length - 1] === space) return false + + // only single spaces + for (let i = 0; i + 1 < buffer.length; i++) { + if (buffer[i] === space && buffer[i + 1] === space) return false + } + + return true +} + +export function parseMessageData(data: MessageData): ParsedMessageData { + const preferHexDisplay = data.preferHexDisplay || false + const common = { + signingPath: parseBIP32Path( + data.signingPath, + InvalidDataReason.MESSAGE_DATA_INVALID_WITNESS_PATH, + ), + isAscii: isAscii(data.messageHex) && !preferHexDisplay, + hashPayload: data.hashPayload, + messageHex: parseHexString( + data.messageHex, + InvalidDataReason.MESSAGE_DATA_INVALID_MESSAGE_HEX, + ), + } + + switch (data.addressFieldType) { + case MessageAddressFieldType.ADDRESS: + return { + ...common, + addressFieldType: MessageAddressFieldType.ADDRESS, + address: parseAddress(data.network, data.address), + } + case MessageAddressFieldType.KEYHASH: + return { + ...common, + addressFieldType: MessageAddressFieldType.KEYHASH, + } + default: + unreachable(data) + } +} diff --git a/src/types/internal.ts b/src/types/internal.ts index ba9a6c50..0ff2393b 100644 --- a/src/types/internal.ts +++ b/src/types/internal.ts @@ -5,6 +5,7 @@ import { CIP36VoteDelegationType, CIP36VoteRegistrationFormat, DatumType, + MessageAddressFieldType, NativeScriptType, PoolKeyType, PoolOwnerType, @@ -57,6 +58,7 @@ export { } from './public' // Our types export const EXTENDED_PUBLIC_KEY_LENGTH = 64 +export const PUBLIC_KEY_LENGTH = 32 export const KEY_HASH_LENGTH = 28 export const SCRIPT_HASH_LENGTH = 28 export const TX_HASH_LENGTH = 32 @@ -603,6 +605,22 @@ export type ParsedOperationalCertificate = { coldKeyPath: ValidBIP32Path } +export type ParsedMessageData = + | { + messageHex: HexString + signingPath: ValidBIP32Path + hashPayload: boolean + isAscii: boolean + } & ( + | { + addressFieldType: MessageAddressFieldType.ADDRESS + address: ParsedAddressParams + } + | { + addressFieldType: MessageAddressFieldType.KEYHASH + } + ) + export type ParsedCVote = { voteCastDataHex: HexString witnessPath: ValidBIP32Path diff --git a/src/types/public.ts b/src/types/public.ts index 1ea51ff0..51e75ff0 100644 --- a/src/types/public.ts +++ b/src/types/public.ts @@ -294,6 +294,51 @@ export type OperationalCertificate = { coldKeyPath: BIP32Path } +/** + * CIP-8 message signing + * + * The content of the `address` field in the protected header in Sig_structure in CIP-8 + * is not defined. Some dApps use actual Cardano address, but in many cases it would be + * more useful to just use a key hash (e.g. for messages signed by DRep keys). + * This is implemented on Ledger. + */ +export enum MessageAddressFieldType { + ADDRESS = 'address', + KEYHASH = 'key_hash', +} + +/** + * CIP-8 message signing + * + * @category Basic types + */ +export type MessageData = { + // message as bytes in hex + // Note: If the message is too long to be displayed at once, it must be hashed. + // MAX_CIP8_MSG_FIRST_CHUNK_ASCII_SIZE 198 + // MAX_CIP8_MSG_FIRST_CHUNK_HEX_SIZE 99 + messageHex: string + + // the message will be signed with this key + signingPath: BIP32Path + + // whether to hash the message when creating payload + hashPayload: boolean + + // if true, message will be displayed in hex even if it is ascii + preferHexDisplay?: boolean +} & ( + | { + addressFieldType: MessageAddressFieldType.ADDRESS + address: DeviceOwnedAddress + network: Network + } + | { + addressFieldType: MessageAddressFieldType.KEYHASH + // the hash of key derived from witnessPath is used in the CIP-8 `address` field + } +) + /** CIP-36 vote * * @category Basic types @@ -1234,6 +1279,10 @@ export type DeviceCompatibility = { * Whether we support Conway era transaction elements */ supportsConway: boolean + /** + * Whether we support CIP-8 message signing + */ + supportsMessageSigning: boolean } /** @@ -1276,6 +1325,17 @@ export type OperationalCertificateSignature = { signatureHex: string } +/** + * CIP-8 message signature + * @category Basic types + * @see [[Ada.signMessage]] + */ +export type SignedMessageData = { + signatureHex: string + signingPublicKeyHex: string + addressFieldHex: string +} + /** * Result of signing a CIP-36 vote. * @category Basic types diff --git a/test/integration/__fixtures__/signMessage.ts b/test/integration/__fixtures__/signMessage.ts new file mode 100644 index 00000000..95fcf5d8 --- /dev/null +++ b/test/integration/__fixtures__/signMessage.ts @@ -0,0 +1,224 @@ +import {str_to_path} from '../../../src/utils/address' +import { + AddressType, + MessageAddressFieldType, + MessageData, + SignedMessageData, +} from '../../../src/Ada' +import {Networks} from '../../test_utils' + +export type TestCase = { + testName: string + signMessageData: MessageData + expected: SignedMessageData +} + +export const tests: TestCase[] = [ + { + testName: + 'msg01: Should correctly sign an empty message with keyhash as address field', + signMessageData: { + messageHex: '', + signingPath: str_to_path("1852'/1815'/0'/0/1"), + hashPayload: false, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '4ac0d7422617cb794c166b7137a4f097d08bb01b58091ca8c6e0b3816288a2869c8121daddab958cdc58899cc6e1e564e36d35753f9e032f23df00b249149e06', + signingPublicKeyHex: + 'b3d5f4158f0c391ee2a28a2e285f218f3e895ff6ff59cb9369c64b03b5bab5eb', + addressFieldHex: + '5a53103829a7382c2ab76111fb69f13e69d616824c62058e44f1a8b3', + }, + }, + { + testName: + 'msg02: Should correctly sign a short non-hashed ascii message with keyhash as address field', + signMessageData: { + messageHex: '68656c6c6f20776f726c64', // 'hello world' + signingPath: str_to_path("1852'/1815'/0'/0/1"), + hashPayload: false, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + 'd1fc9388b6cc0d7e80f4f72267ef53caae6d53420997128004b6e44cc1618b90496f1f4bdb63dcf9d1311cf2633cfbb0ec759a715825c6d509154739beecb607', + signingPublicKeyHex: + 'b3d5f4158f0c391ee2a28a2e285f218f3e895ff6ff59cb9369c64b03b5bab5eb', + addressFieldHex: + '5a53103829a7382c2ab76111fb69f13e69d616824c62058e44f1a8b3', + }, + }, + { + testName: + 'msg03: Should correctly sign a short hashed ascii message with keyhash as address field', + signMessageData: { + messageHex: '68656c6c6f20776f726c64', // 'hello world' + signingPath: str_to_path("1852'/1815'/0'/0/1"), + hashPayload: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '8a77cbd7000ca92ac902b76822abfc502074151b183857afa179c043dacd1b9230c0daa55558e7e2d32e6c2f5a9c4d41ae13da90ce4e70637a5f80b841286a05', + signingPublicKeyHex: + 'b3d5f4158f0c391ee2a28a2e285f218f3e895ff6ff59cb9369c64b03b5bab5eb', + addressFieldHex: + '5a53103829a7382c2ab76111fb69f13e69d616824c62058e44f1a8b3', + }, + }, + { + testName: + 'msg04: Should correctly sign a short non-hashed ascii message displayed as hex', + signMessageData: { + messageHex: '68656c6c6f20776f726c64', // 'hello world' + signingPath: str_to_path("1852'/1815'/0'/4/0"), + hashPayload: false, + preferHexDisplay: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '30ac6ab7f4ddc7779701324b163c52c68d4c0fd4af968122f1b43eea49b9586b366567395833ffb863ba1054863ab7191d09bdc5781f668db5c30b982fd37e07', + signingPublicKeyHex: + 'bc8c8a37d6ab41339bb073e72ce2e776cefed98d1a6d070ea5fada80dc7d6737', + addressFieldHex: + 'cf737588be6e9edeb737eb2e6d06e5cbd292bd8ee32e410c0bba1ba6', + }, + }, + { + testName: + 'msg05: Should correctly sign a short non-hashed hex message with keyhash as address field', + signMessageData: { + messageHex: 'ff656c6c6f20776f726c64', + signingPath: str_to_path("1853'/1815'/0'/0'"), + hashPayload: false, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '3dcc9abb30584a15fd9ce39f790662a80331243d9f2978eca8549fba99740a8980c4bba73e6fc1cc1eee466e303c91542a13b9ee330c1c708cd04f9b093da403', + signingPublicKeyHex: + '3d7e84dca8b4bc322401a2cc814af7c84d2992a22f99554fe340d7df7910768d', + addressFieldHex: + 'dbfee4665e58c8f8e9b9ff02b17f32e08a42c855476a5d867c2737b7', + }, + }, + { + testName: + 'msg06: Should correctly sign a short hashed hex message with keyhash as address field', + signMessageData: { + messageHex: 'ff656c6c6f20776f726c64', + signingPath: str_to_path("1853'/1815'/0'/0'"), + hashPayload: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '0cd0dea4600a2eda7ab145bf600ca252d4a5911959a56fe0294e48e71a249db6e95ded5228e76c97b0add2aa1a8dfc0aed65acd46fc71ac0e99d4b917b1b870d', + signingPublicKeyHex: + '3d7e84dca8b4bc322401a2cc814af7c84d2992a22f99554fe340d7df7910768d', + addressFieldHex: + 'dbfee4665e58c8f8e9b9ff02b17f32e08a42c855476a5d867c2737b7', + }, + }, + { + testName: + 'msg07: Should correctly sign a 198 bytes long non-hashed ascii message with keyhash as address field', + signMessageData: { + messageHex: '6869'.repeat(99), + signingPath: str_to_path("1852'/1815'/0'/3/0"), + hashPayload: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '6659bb68075cbb5d5b5ab0c6290f87931f8c0dddd4b6bea2ecbdb9b8519109a389f0408eeb917894c15db16019052f26da540fd29752d0f61285f78299770805', + signingPublicKeyHex: + '7cc18df2fbd3ee1b16b76843b18446679ab95dbcd07b7833b66a9407c0709e37', + addressFieldHex: + 'ba41c59ac6e1a0e4ac304af98db801097d0bf8d2a5b28a54752426a1', + }, + }, + { + testName: + 'msg08: Should correctly sign a 99 bytes long non-hashed hex message with keyhash as address field', + signMessageData: { + messageHex: 'de'.repeat(99), + signingPath: str_to_path("1852'/1815'/0'/3/0"), + hashPayload: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '4fadaf3541df071455d13d99da061b7b5056f19f88051c99ff59e7902ff15389eca1614c6e0faf9c29131c086b8fbb16d87e7ec7d19936c898fcbfdfb5d93602', + signingPublicKeyHex: + '7cc18df2fbd3ee1b16b76843b18446679ab95dbcd07b7833b66a9407c0709e37', + addressFieldHex: + 'ba41c59ac6e1a0e4ac304af98db801097d0bf8d2a5b28a54752426a1', + }, + }, + { + testName: + 'msg09: Should correctly sign a 1000 bytes long hashed ascii message with keyhash as address field', + signMessageData: { + messageHex: '6869'.repeat(500), + signingPath: str_to_path("1852'/1815'/0'/3/0"), + hashPayload: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '87be8e7be2407ecb8324adb40d63cb4e7126378d0fa87f13e09226da896e11115b15275368ede14cdb42ea13b076dadc7f0eccf49d745312e2366cfb5105b906', + signingPublicKeyHex: + '7cc18df2fbd3ee1b16b76843b18446679ab95dbcd07b7833b66a9407c0709e37', + addressFieldHex: + 'ba41c59ac6e1a0e4ac304af98db801097d0bf8d2a5b28a54752426a1', + }, + }, + { + testName: + 'msg10: Should correctly sign a 349 bytes long hashed hex message with keyhash as address field', + signMessageData: { + messageHex: 'fa'.repeat(349), + signingPath: str_to_path("1852'/1815'/0'/3/0"), + hashPayload: true, + addressFieldType: MessageAddressFieldType.KEYHASH, + }, + expected: { + signatureHex: + '6fcc42c954ecaa143c8fab436a5cc1d0beb4f46c29c7e554d3593d5c4343b27e83a66b3df011c3197e88032a2e879730c67db71ed0f2d9cd3e9a0978990d3a02', + signingPublicKeyHex: + '7cc18df2fbd3ee1b16b76843b18446679ab95dbcd07b7833b66a9407c0709e37', + addressFieldHex: + 'ba41c59ac6e1a0e4ac304af98db801097d0bf8d2a5b28a54752426a1', + }, + }, + { + testName: + 'msg11: Should correctly sign a short non-hashed hex message with address in address field', + signMessageData: { + messageHex: 'deadbeef', + signingPath: str_to_path("1852'/1815'/0'/5/0"), + hashPayload: false, + addressFieldType: MessageAddressFieldType.ADDRESS, + address: { + type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY, + params: { + spendingPath: str_to_path("1852'/1815'/0'/0/1"), + stakingPath: str_to_path("1852'/1815'/0'/2/0"), + }, + }, + network: Networks.Mainnet, + }, + expected: { + signatureHex: + '92586e24a1a43b538720ea3915be0f6536f0894e4ea88713c01f948673865b6d2189a0306bbefc124954e578f8aa1d0f131b1d3e7af7827d1b4488d6fa0f6b07', + signingPublicKeyHex: + '650eb87ddfffe7babd505f2d66c2db28b1c05ac54f9121589107acd6eb20cc2c', + addressFieldHex: + '015a53103829a7382c2ab76111fb69f13e69d616824c62058e44f1a8b31d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c', + }, + }, +] diff --git a/test/integration/getVersion.test.ts b/test/integration/getVersion.test.ts index 84443881..74c7e14d 100644 --- a/test/integration/getVersion.test.ts +++ b/test/integration/getVersion.test.ts @@ -21,7 +21,7 @@ describe('getVersion', () => { const {version, compatibility} = await ada.getVersion() expect(version.major).to.equal(7) - expect(version.minor).to.equal(0) + expect(version.minor).to.equal(1) expect(version.flags.isDebug).to.equal(true) @@ -44,6 +44,7 @@ describe('getVersion', () => { supportsBabbage: true, supportsCIP36Vote: true, supportsConway: true, + supportsMessageSigning: true, }) }) }) diff --git a/test/integration/signMessage.test.ts b/test/integration/signMessage.test.ts new file mode 100644 index 00000000..b4117205 --- /dev/null +++ b/test/integration/signMessage.test.ts @@ -0,0 +1,28 @@ +import chai, {expect} from 'chai' +import chaiAsPromised from 'chai-as-promised' + +import type Ada from '../../src/Ada' +import {getAda} from '../test_utils' +import {tests} from './__fixtures__/signMessage' +chai.use(chaiAsPromised) + +describe('signMessage', () => { + let ada: Ada = {} as Ada + + beforeEach(async () => { + ada = await getAda() + }) + + afterEach(async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await (ada as any).t.close() + }) + + for (const {testName, signMessageData, expected} of tests) { + it(testName, async () => { + const promise = ada.signMessage(signMessageData) + + expect(await promise).to.deep.equal(expected) + }) + } +})