Skip to content

Commit 6c2c903

Browse files
fix: include v0 preamble in offchain message size checks (#1741)
* fix: enforce v0 offchain message preamble size * add changeset for offchain message size fix
1 parent fd27432 commit 6c2c903

3 files changed

Lines changed: 117 additions & 0 deletions

File tree

.changeset/slow-cameras-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solana/offchain-messages': patch
3+
---
4+
5+
Include the v0 preamble bytes when enforcing the 1232-byte limit for hardware-wallet-signable offchain messages.

packages/offchain-messages/src/__tests__/message-v0-codec-test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ const SIGNER_B_BYTES = new Uint8Array([
5252
0x94, 0x8f, 0xd8, 0xbb, 0x2c, 0x10, 0xa1, 0x01, 0x02, 0xce, 0x98, 0xb3, 0xa6,
5353
]);
5454

55+
const PREAMBLE_LENGTH_WITH_TWO_SIGNERS_BYTES =
56+
OFFCHAIN_MESSAGE_SIGNING_DOMAIN_BYTES.length +
57+
1 + // Version
58+
APPLICATION_DOMAIN_BYTES.length +
59+
1 + // Message format
60+
1 + // Signer count
61+
SIGNER_A_BYTES.length +
62+
SIGNER_B_BYTES.length +
63+
2; // Message length
64+
const MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES = 1232 - PREAMBLE_LENGTH_WITH_TWO_SIGNERS_BYTES;
65+
const BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES = MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES + 1;
66+
5567
describe('getOffchainMessageV0Decoder()', () => {
5668
let decoder: VariableSizeDecoder<OffchainMessageV0>;
5769
beforeEach(() => {
@@ -423,6 +435,39 @@ describe('getOffchainMessageV0Decoder()', () => {
423435
}),
424436
);
425437
});
438+
it.each([
439+
[OffchainMessageContentFormat.RESTRICTED_ASCII_1232_BYTES_MAX, 'ASCII'],
440+
[OffchainMessageContentFormat.UTF8_1232_BYTES_MAX, '1232-byte-max UTF-8'],
441+
])('throws when decoding a %s message whose preamble plus body is too long', messageFormat => {
442+
const text = '!'.repeat(BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES);
443+
const encodedMessage =
444+
// prettier-ignore
445+
new Uint8Array([
446+
// Signing domain
447+
...OFFCHAIN_MESSAGE_SIGNING_DOMAIN_BYTES,
448+
// Version
449+
0x00,
450+
// Application domain
451+
...APPLICATION_DOMAIN_BYTES,
452+
// Message format
453+
messageFormat,
454+
// Signer count
455+
0x02,
456+
// Signer addresses
457+
...SIGNER_A_BYTES,
458+
...SIGNER_B_BYTES,
459+
// Message length
460+
text.length & 0xff, text.length >> 8,
461+
// Message
462+
...Array.from(text, character => character.charCodeAt(0)),
463+
]);
464+
expect(() => decoder.decode(encodedMessage)).toThrow(
465+
new SolanaError(SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, {
466+
actualBytes: BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES,
467+
maxBytes: MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES,
468+
}),
469+
);
470+
});
426471
it('throws when decoding a 1232-byte-max UTF-8 message whose message content does not match the length specified in the preamble', () => {
427472
const encodedMessage =
428473
// prettier-ignore
@@ -792,6 +837,26 @@ describe('getOffchainMessageEncoder()', () => {
792837
}),
793838
);
794839
});
840+
it.each([
841+
[OffchainMessageContentFormat.RESTRICTED_ASCII_1232_BYTES_MAX, 'ASCII'],
842+
[OffchainMessageContentFormat.UTF8_1232_BYTES_MAX, '1232-byte-max UTF-8'],
843+
])('throws when encoding a %s message whose preamble plus body is too long', format => {
844+
const offchainMessage = {
845+
applicationDomain: APPLICATION_DOMAIN,
846+
content: {
847+
format,
848+
text: '!'.repeat(BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES),
849+
},
850+
requiredSignatories: [{ address: SIGNER_A }, { address: SIGNER_B }],
851+
version: 0,
852+
} as unknown as OffchainMessageV0;
853+
expect(() => encoder.encode(offchainMessage)).toThrow(
854+
new SolanaError(SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, {
855+
actualBytes: BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES,
856+
maxBytes: MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES,
857+
}),
858+
);
859+
});
795860
it('encodes a well-formed 65535-byte-max UTF-8 message according to spec', () => {
796861
const offchainMessage: OffchainMessageV0 = {
797862
applicationDomain: APPLICATION_DOMAIN,

packages/offchain-messages/src/message-v0.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { getUtf8Encoder } from '@solana/codecs-strings';
2+
import { SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, SolanaError } from '@solana/errors';
3+
14
import {
25
assertIsOffchainMessageContentRestrictedAsciiOf1232BytesMax,
36
assertIsOffchainMessageContentUtf8Of1232BytesMax,
@@ -10,6 +13,15 @@ import {
1013
import { OffchainMessagePreambleV0 } from './preamble-v0';
1114
import { OffchainMessageWithRequiredSignatories } from './signatures';
1215

16+
const MAX_HARDWARE_WALLET_SIGNABLE_MESSAGE_BYTES = 1232;
17+
const SIGNING_DOMAIN_LENGTH_BYTES = 16;
18+
const VERSION_LENGTH_BYTES = 1;
19+
const APPLICATION_DOMAIN_LENGTH_BYTES = 32;
20+
const MESSAGE_FORMAT_LENGTH_BYTES = 1;
21+
const SIGNER_COUNT_LENGTH_BYTES = 1;
22+
const SIGNER_ADDRESS_LENGTH_BYTES = 32;
23+
const MESSAGE_LENGTH_BYTES = 2;
24+
1325
export type BaseOffchainMessageV0 = Omit<
1426
OffchainMessagePreambleV0,
1527
'messageFormat' | 'messageLength' | 'requiredSignatories'
@@ -54,6 +66,39 @@ export type OffchainMessageV0 = BaseOffchainMessageV0 &
5466
OffchainMessageWithContent &
5567
OffchainMessageWithRequiredSignatories;
5668

69+
function getPreambleLengthForRequiredSignatories(
70+
requiredSignatories: OffchainMessageV0['requiredSignatories'],
71+
): number {
72+
return (
73+
SIGNING_DOMAIN_LENGTH_BYTES +
74+
VERSION_LENGTH_BYTES +
75+
APPLICATION_DOMAIN_LENGTH_BYTES +
76+
MESSAGE_FORMAT_LENGTH_BYTES +
77+
SIGNER_COUNT_LENGTH_BYTES +
78+
requiredSignatories.length * SIGNER_ADDRESS_LENGTH_BYTES +
79+
MESSAGE_LENGTH_BYTES
80+
);
81+
}
82+
83+
function assertFitsHardwareWalletSignableMessageByteLimit(
84+
putativeMessage: Pick<OffchainMessageV0, 'requiredSignatories'> & {
85+
content: {
86+
text: string;
87+
};
88+
},
89+
) {
90+
const messageBodyLength = getUtf8Encoder().getSizeFromValue(putativeMessage.content.text);
91+
const maxBodyLength =
92+
MAX_HARDWARE_WALLET_SIGNABLE_MESSAGE_BYTES -
93+
getPreambleLengthForRequiredSignatories(putativeMessage.requiredSignatories);
94+
if (messageBodyLength > maxBodyLength) {
95+
throw new SolanaError(SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, {
96+
actualBytes: messageBodyLength,
97+
maxBytes: maxBodyLength,
98+
});
99+
}
100+
}
101+
57102
/**
58103
* In the event that you receive a v0 offchain message from an untrusted source, use this function
59104
* to assert that it is one whose content conforms to the
@@ -71,6 +116,7 @@ export function assertIsOffchainMessageRestrictedAsciiOf1232BytesMax<TMessage ex
71116
}>,
72117
): asserts putativeMessage is OffchainMessageWithRestrictedAsciiOf1232BytesMaxContent & Omit<TMessage, 'content'> {
73118
assertIsOffchainMessageContentRestrictedAsciiOf1232BytesMax(putativeMessage.content);
119+
assertFitsHardwareWalletSignableMessageByteLimit(putativeMessage);
74120
}
75121

76122
/**
@@ -91,6 +137,7 @@ export function assertIsOffchainMessageUtf8Of1232BytesMax<TMessage extends Offch
91137
}>,
92138
): asserts putativeMessage is OffchainMessageWithUtf8Of1232BytesMaxContent & Omit<TMessage, 'content'> {
93139
assertIsOffchainMessageContentUtf8Of1232BytesMax(putativeMessage.content);
140+
assertFitsHardwareWalletSignableMessageByteLimit(putativeMessage);
94141
}
95142

96143
/**

0 commit comments

Comments
 (0)