Skip to content

Commit 1e97617

Browse files
committed
fix(sdk-core): coins specific message sign validate
TICKET: SC-2419
1 parent 2b3634d commit 1e97617

File tree

8 files changed

+314
-4
lines changed

8 files changed

+314
-4
lines changed

modules/account-lib/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ export { Vet };
206206
import * as CosmosSharedCoin from '@bitgo/sdk-coin-cosmos';
207207
export { CosmosSharedCoin };
208208

209-
import { validateAgainstMessageTemplates, MIDNIGHT_TNC_HASH } from './utils';
209+
import { MIDNIGHT_TNC_HASH } from './utils';
210210
export { MIDNIGHT_TNC_HASH };
211211

212212
const coinBuilderMap = {
@@ -431,11 +431,11 @@ export async function verifyMessage(
431431
const messageBuilder = messageBuilderFactory.getMessageBuilder(messageStandardType);
432432
messageBuilder.setPayload(messageRaw);
433433
const message = await messageBuilder.build();
434-
const isValidMessageEncoded = await message.verifyEncodedPayload(messageEncoded, metadata);
435-
if (!isValidMessageEncoded) {
434+
const isValidRawMessage = message.verifyRawMessage(messageRaw);
435+
if (!isValidRawMessage) {
436436
return false;
437437
}
438-
return validateAgainstMessageTemplates(messageRaw);
438+
return await message.verifyEncodedPayload(messageEncoded, metadata);
439439
} catch (e) {
440440
console.error(`Error verifying message for coin ${coinName}:`, e);
441441
return false;

modules/account-lib/test/unit/verifyMessage.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ describe('verifyMessage', () => {
5353
);
5454
should.equal(result, false);
5555
});
56+
57+
it('should return false if encoded payload verification fails', async () => {
58+
const coinName = 'eth';
59+
const messageRaw = testnetMessageRaw;
60+
const invalidEncodedHex = '0123456789abcdef'; // Invalid encoded payload
61+
62+
const result = await accountLib.verifyMessage(
63+
coinName,
64+
messageRaw,
65+
invalidEncodedHex,
66+
MessageStandardType.EIP191,
67+
);
68+
should.equal(result, false);
69+
});
5670
});
5771

5872
describe('CIP8 Message', function () {
@@ -86,5 +100,26 @@ describe('verifyMessage', () => {
86100
);
87101
should.equal(result, true);
88102
});
103+
104+
it('should return false when raw message validation fails for ADA', async () => {
105+
const coinName = 'ada';
106+
const invalidMessageRaw = 'Invalid ADA message format';
107+
cip8MessageBuilder.setPayload(testnetMessageRaw);
108+
cip8MessageBuilder.addSigner(adaTestnetOriginAddress);
109+
const message = await cip8MessageBuilder.build();
110+
const messageEncodedHex = (await message.getSignablePayload()).toString('hex');
111+
112+
const metadata = {
113+
signers: [adaTestnetOriginAddress],
114+
};
115+
const result = await accountLib.verifyMessage(
116+
coinName,
117+
invalidMessageRaw,
118+
messageEncodedHex,
119+
MessageStandardType.CIP8,
120+
metadata,
121+
);
122+
should.equal(result, false);
123+
});
89124
});
90125
});

modules/sdk-coin-ada/src/lib/messages/cip8/cip8Message.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ export class Cip8Message extends BaseMessage {
8686
return signablePayloadHex === messageEncodedHex;
8787
}
8888

89+
/**
90+
* Verifies whether a raw message meets CIP-8 specific requirements for Midnight Glacier Drop claims
91+
* Only allows messages that match the exact Midnight Glacier Drop claim format
92+
* @param rawMessage The raw message content to verify as a string
93+
* @returns True if the raw message matches the expected Midnight Glacier Drop claim format, false otherwise
94+
* @example
95+
* ```typescript
96+
* // Valid format: "STAR 100 to addr1abc123... 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b"
97+
* const message = await builder.build();
98+
* const isValid = message.verifyRawMessage("STAR 100 to addr1xyz... 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b");
99+
* // Returns true only for properly formatted Midnight Glacier Drop claims
100+
* ```
101+
*/
102+
verifyRawMessage(rawMessage: string): boolean {
103+
const MIDNIGHT_TNC_HASH = '31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
104+
const MIDNIGHT_GLACIER_DROP_CLAIM_MESSAGE_TEMPLATE = `STAR \\d+ to addr(?:1|_test1)[a-z0-9]{50,} ${MIDNIGHT_TNC_HASH}`;
105+
106+
const regex = new RegExp(`^${MIDNIGHT_GLACIER_DROP_CLAIM_MESSAGE_TEMPLATE}$`, 's');
107+
return regex.test(rawMessage);
108+
}
109+
89110
/**
90111
* Validates required fields and returns common setup objects
91112
* @private

modules/sdk-coin-ada/test/resources/cip8Resources.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,33 @@ export const cip8TestResources = {
4545
signablePayloads: {
4646
simple: 'a0', // Example CBOR hex for simple message (will be replaced with actual values)
4747
},
48+
49+
// Midnight Glacier Drop claim message test data
50+
midnightGlacierDrop: {
51+
validMessages: {
52+
mainnet:
53+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
54+
testnet:
55+
'STAR 250 to addr_test1qpxecfjurjtcnalwy6gxcqzp09je55gvfv79hghqst8p7p6dnsn9c8yh38m7uf5sdsqyz7t9nfgscjeutw3wpqkwrursutfm7h 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
56+
},
57+
invalidMessages: {
58+
missingStarPrefix:
59+
'100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
60+
invalidNumber:
61+
'STAR abc to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
62+
invalidAddress: 'STAR 100 to invalid_address 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
63+
shortAddress: 'STAR 100 to addr1short 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
64+
wrongHash:
65+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an wronghashhere',
66+
missingHash:
67+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an',
68+
extraContent:
69+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b extra content',
70+
caseSensitive:
71+
'star 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
72+
},
73+
tnc: {
74+
hash: '31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b',
75+
},
76+
},
4877
};

modules/sdk-coin-ada/test/unit/messages/cip8/cip8Message.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,111 @@ describe('Cip8Message', function () {
154154
should.throws(() => message.getBroadcastableSignatures(), /Payload is required to build a CIP8 message/);
155155
});
156156
});
157+
158+
describe('verifyRawMessage', function () {
159+
it('should return true for valid Midnight Glacier Drop claim message', function () {
160+
const message = new Cip8Message(createDefaultMessageOptions());
161+
const validMessage =
162+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
163+
164+
const result = message.verifyRawMessage(validMessage);
165+
result.should.be.true();
166+
});
167+
168+
it('should return true for valid Midnight Glacier Drop claim message with testnet address', function () {
169+
const message = new Cip8Message(createDefaultMessageOptions());
170+
const validTestnetMessage =
171+
'STAR 250 to addr_test1qpxecfjurjtcnalwy6gxcqzp09je55gvfv79hghqst8p7p6dnsn9c8yh38m7uf5sdsqyz7t9nfgscjeutw3wpqkwrursutfm7h 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
172+
173+
const result = message.verifyRawMessage(validTestnetMessage);
174+
result.should.be.true();
175+
});
176+
177+
it('should return false for message without STAR prefix', function () {
178+
const message = new Cip8Message(createDefaultMessageOptions());
179+
const invalidMessage =
180+
'100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
181+
182+
const result = message.verifyRawMessage(invalidMessage);
183+
result.should.be.false();
184+
});
185+
186+
it('should return false for message with invalid number format', function () {
187+
const message = new Cip8Message(createDefaultMessageOptions());
188+
const invalidMessage =
189+
'STAR abc to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
190+
191+
const result = message.verifyRawMessage(invalidMessage);
192+
result.should.be.false();
193+
});
194+
195+
it('should return false for message with invalid address format', function () {
196+
const message = new Cip8Message(createDefaultMessageOptions());
197+
const invalidMessage =
198+
'STAR 100 to invalid_address 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
199+
200+
const result = message.verifyRawMessage(invalidMessage);
201+
result.should.be.false();
202+
});
203+
204+
it('should return false for message with short address', function () {
205+
const message = new Cip8Message(createDefaultMessageOptions());
206+
const invalidMessage = 'STAR 100 to addr1short 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
207+
208+
const result = message.verifyRawMessage(invalidMessage);
209+
result.should.be.false();
210+
});
211+
212+
it('should return false for message with wrong TnC hash', function () {
213+
const message = new Cip8Message(createDefaultMessageOptions());
214+
const invalidMessage =
215+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an wronghashhere';
216+
217+
const result = message.verifyRawMessage(invalidMessage);
218+
result.should.be.false();
219+
});
220+
221+
it('should return false for message with missing TnC hash', function () {
222+
const message = new Cip8Message(createDefaultMessageOptions());
223+
const invalidMessage =
224+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an';
225+
226+
const result = message.verifyRawMessage(invalidMessage);
227+
result.should.be.false();
228+
});
229+
230+
it('should return false for message with extra content', function () {
231+
const message = new Cip8Message(createDefaultMessageOptions());
232+
const invalidMessage =
233+
'STAR 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b extra content';
234+
235+
const result = message.verifyRawMessage(invalidMessage);
236+
result.should.be.false();
237+
});
238+
239+
it('should return false for empty message', function () {
240+
const message = new Cip8Message(createDefaultMessageOptions());
241+
const emptyMessage = '';
242+
243+
const result = message.verifyRawMessage(emptyMessage);
244+
result.should.be.false();
245+
});
246+
247+
it('should return false for completely different message format', function () {
248+
const message = new Cip8Message(createDefaultMessageOptions());
249+
const differentMessage = 'Hello, this is a regular message';
250+
251+
const result = message.verifyRawMessage(differentMessage);
252+
result.should.be.false();
253+
});
254+
255+
it('should handle case sensitivity correctly', function () {
256+
const message = new Cip8Message(createDefaultMessageOptions());
257+
const caseInsensitiveMessage =
258+
'star 100 to addr1qxy2lshz9na88lslkj8gzd0y7t9h8j7jr0sgg30qnrylvfx4u2hwvqalq5fj9vmhxf06jgz0zt2j2qxjmzwf3rhqzqsehw0an 31a6bab50a84b8439adcfb786bb2020f6807e6e8fda629b424110fc7bb1c6b8b';
259+
260+
const result = message.verifyRawMessage(caseInsensitiveMessage);
261+
result.should.be.false(); // Should be case sensitive
262+
});
263+
});
157264
});

modules/sdk-core/src/account-lib/baseCoin/messages/baseMessage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,14 @@ export abstract class BaseMessage implements IMessage {
180180
}
181181
return signablePayloadHex === messageEncodedHex;
182182
}
183+
184+
/**
185+
* Verifies whether a raw message payload meets coin-specific format requirements
186+
* Base implementation validates that the message is not null, undefined, or empty
187+
* @param rawMessage The raw message content to verify as a string
188+
* @returns True if the raw message is valid and can be safely processed, false otherwise
189+
*/
190+
verifyRawMessage(rawMessage: string): boolean {
191+
return Boolean(rawMessage?.trim());
192+
}
183193
}

modules/sdk-core/src/account-lib/baseCoin/messages/iface.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ export interface IMessage {
8282
* @returns A Promise resolving to true if the message is valid, false otherwise
8383
*/
8484
verifyEncodedPayload(messageEncodedHex: string, metadata?: Record<string, unknown>): Promise<boolean>;
85+
86+
/**
87+
* Verifies whether a raw message payload meets coin-specific format requirements
88+
* This method performs validation on the raw message content before signing
89+
* @param rawMessage The raw message content to verify as a string
90+
* @returns True if the raw message is valid and can be safely processed, false otherwise
91+
* @example
92+
* ```typescript
93+
* const message = await builder.build();
94+
* const isValid = message.verifyRawMessage("Hello World");
95+
* // Returns true for most coins, false for coins with strict format requirements
96+
* ```
97+
*/
98+
verifyRawMessage(rawMessage: string): boolean;
8599
}
86100

87101
/**

modules/sdk-core/test/unit/account-lib/baseCoin/messages/baseMessage.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,98 @@ describe('Base Message', () => {
237237
should.deepEqual(parsed, expectedBroadcastString);
238238
});
239239
});
240+
241+
describe('verifyEncodedPayload', () => {
242+
let message: TestMessage;
243+
244+
beforeEach(() => {
245+
message = new TestMessage({
246+
coinConfig,
247+
payload: 'test payload',
248+
});
249+
});
250+
251+
it('should return true when encoded message matches signable payload', async () => {
252+
const signablePayload = await message.getSignablePayload();
253+
const expectedHex = (signablePayload as Buffer).toString('hex');
254+
255+
const result = await message.verifyEncodedPayload(expectedHex);
256+
should.equal(result, true);
257+
});
258+
259+
it('should return false when encoded message does not match signable payload', async () => {
260+
const wrongHex = '1234567890abcdef';
261+
262+
const result = await message.verifyEncodedPayload(wrongHex);
263+
should.equal(result, false);
264+
});
265+
266+
it('should handle string signable payload', async () => {
267+
// Create a custom test message that returns string payload
268+
class StringTestMessage extends TestMessage {
269+
async getSignablePayload(): Promise<string | Buffer> {
270+
return 'string payload';
271+
}
272+
}
273+
274+
const messageWithStringPayload = new StringTestMessage({
275+
coinConfig,
276+
payload: 'test',
277+
});
278+
279+
const result = await messageWithStringPayload.verifyEncodedPayload('string payload');
280+
should.equal(result, true);
281+
});
282+
283+
it('should accept optional metadata parameter', async () => {
284+
const signablePayload = await message.getSignablePayload();
285+
const expectedHex = (signablePayload as Buffer).toString('hex');
286+
const metadata = { chainId: 1, version: '1.0' };
287+
288+
const result = await message.verifyEncodedPayload(expectedHex, metadata);
289+
should.equal(result, true);
290+
});
291+
});
292+
293+
describe('verifyRawMessage', () => {
294+
let message: TestMessage;
295+
296+
beforeEach(() => {
297+
message = new TestMessage({
298+
coinConfig,
299+
payload: 'test payload',
300+
});
301+
});
302+
303+
it('should return true for any non-empty raw message (base implementation)', () => {
304+
const result = message.verifyRawMessage('Any message content');
305+
should.equal(result, true);
306+
});
307+
308+
it('should return false for empty string', () => {
309+
const result = message.verifyRawMessage('');
310+
should.equal(result, false);
311+
});
312+
313+
it('should return false for null', () => {
314+
const result = message.verifyRawMessage(null as any);
315+
should.equal(result, false);
316+
});
317+
318+
it('should return false for undefined', () => {
319+
const result = message.verifyRawMessage(undefined as any);
320+
should.equal(result, false);
321+
});
322+
323+
it('should return false for whitespace-only string', () => {
324+
const result = message.verifyRawMessage(' \t\n\r ');
325+
should.equal(result, false);
326+
});
327+
328+
it('should return true for JSON format', () => {
329+
const jsonMessage = JSON.stringify({ message: 'test', data: [1, 2, 3] });
330+
const result = message.verifyRawMessage(jsonMessage);
331+
should.equal(result, true);
332+
});
333+
});
240334
});

0 commit comments

Comments
 (0)