diff --git a/modules/account-lib/src/index.ts b/modules/account-lib/src/index.ts index bc8564d3df..7ca41d462a 100644 --- a/modules/account-lib/src/index.ts +++ b/modules/account-lib/src/index.ts @@ -206,7 +206,7 @@ export { Vet }; import * as CosmosSharedCoin from '@bitgo/sdk-coin-cosmos'; export { CosmosSharedCoin }; -import { validateAgainstMessageTemplates, MIDNIGHT_TNC_HASH } from './utils'; +import { MIDNIGHT_TNC_HASH } from './utils'; export { MIDNIGHT_TNC_HASH }; const coinBuilderMap = { @@ -431,11 +431,11 @@ export async function verifyMessage( const messageBuilder = messageBuilderFactory.getMessageBuilder(messageStandardType); messageBuilder.setPayload(messageRaw); const message = await messageBuilder.build(); - const isValidMessageEncoded = await message.verifyEncodedPayload(messageEncoded, metadata); - if (!isValidMessageEncoded) { + const isValidRawMessage = message.verifyRawMessage(messageRaw); + if (!isValidRawMessage) { return false; } - return validateAgainstMessageTemplates(messageRaw); + return await message.verifyEncodedPayload(messageEncoded, metadata); } catch (e) { console.error(`Error verifying message for coin ${coinName}:`, e); return false; diff --git a/modules/account-lib/test/unit/verifyMessage.ts b/modules/account-lib/test/unit/verifyMessage.ts index 6c94118d9d..8184ca57cd 100644 --- a/modules/account-lib/test/unit/verifyMessage.ts +++ b/modules/account-lib/test/unit/verifyMessage.ts @@ -53,6 +53,20 @@ describe('verifyMessage', () => { ); should.equal(result, false); }); + + it('should return false if encoded payload verification fails', async () => { + const coinName = 'eth'; + const messageRaw = testnetMessageRaw; + const invalidEncodedHex = '0123456789abcdef'; // Invalid encoded payload + + const result = await accountLib.verifyMessage( + coinName, + messageRaw, + invalidEncodedHex, + MessageStandardType.EIP191, + ); + should.equal(result, false); + }); }); describe('CIP8 Message', function () { @@ -86,5 +100,26 @@ describe('verifyMessage', () => { ); should.equal(result, true); }); + + it('should return false when raw message validation fails for ADA', async () => { + const coinName = 'ada'; + const invalidMessageRaw = 'Invalid ADA message format'; + cip8MessageBuilder.setPayload(testnetMessageRaw); + cip8MessageBuilder.addSigner(adaTestnetOriginAddress); + const message = await cip8MessageBuilder.build(); + const messageEncodedHex = (await message.getSignablePayload()).toString('hex'); + + const metadata = { + signers: [adaTestnetOriginAddress], + }; + const result = await accountLib.verifyMessage( + coinName, + invalidMessageRaw, + messageEncodedHex, + MessageStandardType.CIP8, + metadata, + ); + should.equal(result, false); + }); }); }); diff --git a/modules/sdk-coin-ada/src/lib/messages/cip8/cip8Message.ts b/modules/sdk-coin-ada/src/lib/messages/cip8/cip8Message.ts index 382d601203..3c7c9c9058 100644 --- a/modules/sdk-coin-ada/src/lib/messages/cip8/cip8Message.ts +++ b/modules/sdk-coin-ada/src/lib/messages/cip8/cip8Message.ts @@ -86,6 +86,27 @@ export class Cip8Message extends BaseMessage { return signablePayloadHex === messageEncodedHex; } + /** + * Verifies whether a raw message meets CIP-8 specific requirements for Midnight Glacier Drop claims + * Only allows messages that match the exact Midnight Glacier Drop claim format + * @param rawMessage The raw message content to verify as a string + * @returns True if the raw message matches the expected Midnight Glacier Drop claim format, false otherwise + * @example + * ```typescript + * // Valid format: "STAR 100 to addr1abc123... 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b" + * const message = await builder.build(); + * const isValid = message.verifyRawMessage("STAR 100 to addr1xyz... 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b"); + * // Returns true only for properly formatted Midnight Glacier Drop claims + * ``` + */ + verifyRawMessage(rawMessage: string): boolean { + const MIDNIGHT_TNC_HASH = '31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + const MIDNIGHT_GLACIER_DROP_CLAIM_MESSAGE_TEMPLATE = `STAR \\d+ to addr(?:1|_test1)[a-z0-9]{50,} ${MIDNIGHT_TNC_HASH}`; + + const regex = new RegExp(`^${MIDNIGHT_GLACIER_DROP_CLAIM_MESSAGE_TEMPLATE}$`, 's'); + return regex.test(rawMessage); + } + /** * Validates required fields and returns common setup objects * @private diff --git a/modules/sdk-coin-ada/test/resources/cip8Resources.ts b/modules/sdk-coin-ada/test/resources/cip8Resources.ts index 17168b9b68..e0b828241c 100644 --- a/modules/sdk-coin-ada/test/resources/cip8Resources.ts +++ b/modules/sdk-coin-ada/test/resources/cip8Resources.ts @@ -45,4 +45,33 @@ export const cip8TestResources = { signablePayloads: { simple: 'a0', // Example CBOR hex for simple message (will be replaced with actual values) }, + + // Midnight Glacier Drop claim message test data + midnightGlacierDrop: { + validMessages: { + mainnet: + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + testnet: + 'STAR 250 to addr_test1qpxecfjurjtcnalwy6gxcqzp09je55gvfv79hghqst8p7p6dnsn9c8yh38m7uf5sdsqyz7t9nfgscjeutw3wpqkwrursutfm7h 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + }, + invalidMessages: { + missingStarPrefix: + '100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + invalidNumber: + 'STAR abc to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + invalidAddress: 'STAR 100 to invalid_address 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + shortAddress: 'STAR 100 to addr1short 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + wrongHash: + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an wronghashhere', + missingHash: + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an', + extraContent: + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b extra content', + caseSensitive: + 'star 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + }, + tnc: { + hash: '31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b', + }, + }, }; diff --git a/modules/sdk-coin-ada/test/unit/messages/cip8/cip8Message.ts b/modules/sdk-coin-ada/test/unit/messages/cip8/cip8Message.ts index f82700b532..ad48e2a9b5 100644 --- a/modules/sdk-coin-ada/test/unit/messages/cip8/cip8Message.ts +++ b/modules/sdk-coin-ada/test/unit/messages/cip8/cip8Message.ts @@ -154,4 +154,111 @@ describe('Cip8Message', function () { should.throws(() => message.getBroadcastableSignatures(), /Payload is required to build a CIP8 message/); }); }); + + describe('verifyRawMessage', function () { + it('should return true for valid Midnight Glacier Drop claim message', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const validMessage = + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(validMessage); + result.should.be.true(); + }); + + it('should return true for valid Midnight Glacier Drop claim message with testnet address', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const validTestnetMessage = + 'STAR 250 to addr_test1qpxecfjurjtcnalwy6gxcqzp09je55gvfv79hghqst8p7p6dnsn9c8yh38m7uf5sdsqyz7t9nfgscjeutw3wpqkwrursutfm7h 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(validTestnetMessage); + result.should.be.true(); + }); + + it('should return false for message without STAR prefix', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = + '100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for message with invalid number format', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = + 'STAR abc to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for message with invalid address format', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = + 'STAR 100 to invalid_address 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for message with short address', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = 'STAR 100 to addr1short 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for message with wrong TnC hash', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an wronghashhere'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for message with missing TnC hash', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for message with extra content', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const invalidMessage = + 'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b extra content'; + + const result = message.verifyRawMessage(invalidMessage); + result.should.be.false(); + }); + + it('should return false for empty message', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const emptyMessage = ''; + + const result = message.verifyRawMessage(emptyMessage); + result.should.be.false(); + }); + + it('should return false for completely different message format', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const differentMessage = 'Hello, this is a regular message'; + + const result = message.verifyRawMessage(differentMessage); + result.should.be.false(); + }); + + it('should handle case sensitivity correctly', function () { + const message = new Cip8Message(createDefaultMessageOptions()); + const caseInsensitiveMessage = + 'star 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b'; + + const result = message.verifyRawMessage(caseInsensitiveMessage); + result.should.be.false(); // Should be case sensitive + }); + }); }); diff --git a/modules/sdk-core/src/account-lib/baseCoin/messages/baseMessage.ts b/modules/sdk-core/src/account-lib/baseCoin/messages/baseMessage.ts index 1ab37342f9..3eefda43f4 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/messages/baseMessage.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/messages/baseMessage.ts @@ -180,4 +180,14 @@ export abstract class BaseMessage implements IMessage { } return signablePayloadHex === messageEncodedHex; } + + /** + * Verifies whether a raw message payload meets coin-specific format requirements + * Base implementation validates that the message is not null, undefined, or empty + * @param rawMessage The raw message content to verify as a string + * @returns True if the raw message is valid and can be safely processed, false otherwise + */ + verifyRawMessage(rawMessage: string): boolean { + return Boolean(rawMessage?.trim()); + } } diff --git a/modules/sdk-core/src/account-lib/baseCoin/messages/iface.ts b/modules/sdk-core/src/account-lib/baseCoin/messages/iface.ts index 43303d77b2..99760954f4 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/messages/iface.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/messages/iface.ts @@ -82,6 +82,20 @@ export interface IMessage { * @returns A Promise resolving to true if the message is valid, false otherwise */ verifyEncodedPayload(messageEncodedHex: string, metadata?: Record): Promise; + + /** + * Verifies whether a raw message payload meets coin-specific format requirements + * This method performs validation on the raw message content before signing + * @param rawMessage The raw message content to verify as a string + * @returns True if the raw message is valid and can be safely processed, false otherwise + * @example + * ```typescript + * const message = await builder.build(); + * const isValid = message.verifyRawMessage("Hello World"); + * // Returns true for most coins, false for coins with strict format requirements + * ``` + */ + verifyRawMessage(rawMessage: string): boolean; } /** diff --git a/modules/sdk-core/test/unit/account-lib/baseCoin/messages/baseMessage.ts b/modules/sdk-core/test/unit/account-lib/baseCoin/messages/baseMessage.ts index dba88d730b..0c06ab3f8e 100644 --- a/modules/sdk-core/test/unit/account-lib/baseCoin/messages/baseMessage.ts +++ b/modules/sdk-core/test/unit/account-lib/baseCoin/messages/baseMessage.ts @@ -237,4 +237,98 @@ describe('Base Message', () => { should.deepEqual(parsed, expectedBroadcastString); }); }); + + describe('verifyEncodedPayload', () => { + let message: TestMessage; + + beforeEach(() => { + message = new TestMessage({ + coinConfig, + payload: 'test payload', + }); + }); + + it('should return true when encoded message matches signable payload', async () => { + const signablePayload = await message.getSignablePayload(); + const expectedHex = (signablePayload as Buffer).toString('hex'); + + const result = await message.verifyEncodedPayload(expectedHex); + should.equal(result, true); + }); + + it('should return false when encoded message does not match signable payload', async () => { + const wrongHex = '1234567890abcdef'; + + const result = await message.verifyEncodedPayload(wrongHex); + should.equal(result, false); + }); + + it('should handle string signable payload', async () => { + // Create a custom test message that returns string payload + class StringTestMessage extends TestMessage { + async getSignablePayload(): Promise { + return 'string payload'; + } + } + + const messageWithStringPayload = new StringTestMessage({ + coinConfig, + payload: 'test', + }); + + const result = await messageWithStringPayload.verifyEncodedPayload('string payload'); + should.equal(result, true); + }); + + it('should accept optional metadata parameter', async () => { + const signablePayload = await message.getSignablePayload(); + const expectedHex = (signablePayload as Buffer).toString('hex'); + const metadata = { chainId: 1, version: '1.0' }; + + const result = await message.verifyEncodedPayload(expectedHex, metadata); + should.equal(result, true); + }); + }); + + describe('verifyRawMessage', () => { + let message: TestMessage; + + beforeEach(() => { + message = new TestMessage({ + coinConfig, + payload: 'test payload', + }); + }); + + it('should return true for any non-empty raw message (base implementation)', () => { + const result = message.verifyRawMessage('Any message content'); + should.equal(result, true); + }); + + it('should return false for empty string', () => { + const result = message.verifyRawMessage(''); + should.equal(result, false); + }); + + it('should return false for null', () => { + const result = message.verifyRawMessage(null as any); + should.equal(result, false); + }); + + it('should return false for undefined', () => { + const result = message.verifyRawMessage(undefined as any); + should.equal(result, false); + }); + + it('should return false for whitespace-only string', () => { + const result = message.verifyRawMessage(' \t\n\r '); + should.equal(result, false); + }); + + it('should return true for JSON format', () => { + const jsonMessage = JSON.stringify({ message: 'test', data: [1, 2, 3] }); + const result = message.verifyRawMessage(jsonMessage); + should.equal(result, true); + }); + }); });