Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slow-cameras-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solana/offchain-messages': patch
---

Include the v0 preamble bytes when enforcing the 1232-byte limit for hardware-wallet-signable offchain messages.
65 changes: 65 additions & 0 deletions packages/offchain-messages/src/__tests__/message-v0-codec-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ const SIGNER_B_BYTES = new Uint8Array([
0x94, 0x8f, 0xd8, 0xbb, 0x2c, 0x10, 0xa1, 0x01, 0x02, 0xce, 0x98, 0xb3, 0xa6,
]);

const PREAMBLE_LENGTH_WITH_TWO_SIGNERS_BYTES =
OFFCHAIN_MESSAGE_SIGNING_DOMAIN_BYTES.length +
1 + // Version
APPLICATION_DOMAIN_BYTES.length +
1 + // Message format
1 + // Signer count
SIGNER_A_BYTES.length +
SIGNER_B_BYTES.length +
2; // Message length
const MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES = 1232 - PREAMBLE_LENGTH_WITH_TWO_SIGNERS_BYTES;
const BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES = MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES + 1;

describe('getOffchainMessageV0Decoder()', () => {
let decoder: VariableSizeDecoder<OffchainMessageV0>;
beforeEach(() => {
Expand Down Expand Up @@ -423,6 +435,39 @@ describe('getOffchainMessageV0Decoder()', () => {
}),
);
});
it.each([
[OffchainMessageContentFormat.RESTRICTED_ASCII_1232_BYTES_MAX, 'ASCII'],
[OffchainMessageContentFormat.UTF8_1232_BYTES_MAX, '1232-byte-max UTF-8'],
])('throws when decoding a %s message whose preamble plus body is too long', messageFormat => {
const text = '!'.repeat(BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES);
const encodedMessage =
// prettier-ignore
new Uint8Array([
// Signing domain
...OFFCHAIN_MESSAGE_SIGNING_DOMAIN_BYTES,
// Version
0x00,
// Application domain
...APPLICATION_DOMAIN_BYTES,
// Message format
messageFormat,
// Signer count
0x02,
// Signer addresses
...SIGNER_A_BYTES,
...SIGNER_B_BYTES,
// Message length
text.length & 0xff, text.length >> 8,
// Message
...Array.from(text, character => character.charCodeAt(0)),
]);
expect(() => decoder.decode(encodedMessage)).toThrow(
new SolanaError(SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, {
actualBytes: BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES,
maxBytes: MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES,
}),
);
});
it('throws when decoding a 1232-byte-max UTF-8 message whose message content does not match the length specified in the preamble', () => {
const encodedMessage =
// prettier-ignore
Expand Down Expand Up @@ -792,6 +837,26 @@ describe('getOffchainMessageEncoder()', () => {
}),
);
});
it.each([
[OffchainMessageContentFormat.RESTRICTED_ASCII_1232_BYTES_MAX, 'ASCII'],
[OffchainMessageContentFormat.UTF8_1232_BYTES_MAX, '1232-byte-max UTF-8'],
])('throws when encoding a %s message whose preamble plus body is too long', format => {
const offchainMessage = {
applicationDomain: APPLICATION_DOMAIN,
content: {
format,
text: '!'.repeat(BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES),
},
requiredSignatories: [{ address: SIGNER_A }, { address: SIGNER_B }],
version: 0,
} as unknown as OffchainMessageV0;
expect(() => encoder.encode(offchainMessage)).toThrow(
new SolanaError(SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, {
actualBytes: BODY_LENGTH_EXCEEDING_TWO_SIGNER_PREAMBLE_BYTES,
maxBytes: MAX_BODY_LENGTH_WITH_TWO_SIGNERS_BYTES,
}),
);
});
it('encodes a well-formed 65535-byte-max UTF-8 message according to spec', () => {
const offchainMessage: OffchainMessageV0 = {
applicationDomain: APPLICATION_DOMAIN,
Expand Down
47 changes: 47 additions & 0 deletions packages/offchain-messages/src/message-v0.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getUtf8Encoder } from '@solana/codecs-strings';
import { SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, SolanaError } from '@solana/errors';

import {
assertIsOffchainMessageContentRestrictedAsciiOf1232BytesMax,
assertIsOffchainMessageContentUtf8Of1232BytesMax,
Expand All @@ -10,6 +13,15 @@ import {
import { OffchainMessagePreambleV0 } from './preamble-v0';
import { OffchainMessageWithRequiredSignatories } from './signatures';

const MAX_HARDWARE_WALLET_SIGNABLE_MESSAGE_BYTES = 1232;
const SIGNING_DOMAIN_LENGTH_BYTES = 16;
const VERSION_LENGTH_BYTES = 1;
const APPLICATION_DOMAIN_LENGTH_BYTES = 32;
const MESSAGE_FORMAT_LENGTH_BYTES = 1;
const SIGNER_COUNT_LENGTH_BYTES = 1;
const SIGNER_ADDRESS_LENGTH_BYTES = 32;
const MESSAGE_LENGTH_BYTES = 2;

export type BaseOffchainMessageV0 = Omit<
OffchainMessagePreambleV0,
'messageFormat' | 'messageLength' | 'requiredSignatories'
Expand Down Expand Up @@ -54,6 +66,39 @@ export type OffchainMessageV0 = BaseOffchainMessageV0 &
OffchainMessageWithContent &
OffchainMessageWithRequiredSignatories;

function getPreambleLengthForRequiredSignatories(
requiredSignatories: OffchainMessageV0['requiredSignatories'],
): number {
return (
SIGNING_DOMAIN_LENGTH_BYTES +
VERSION_LENGTH_BYTES +
APPLICATION_DOMAIN_LENGTH_BYTES +
MESSAGE_FORMAT_LENGTH_BYTES +
SIGNER_COUNT_LENGTH_BYTES +
requiredSignatories.length * SIGNER_ADDRESS_LENGTH_BYTES +
MESSAGE_LENGTH_BYTES
);
}

function assertFitsHardwareWalletSignableMessageByteLimit(
putativeMessage: Pick<OffchainMessageV0, 'requiredSignatories'> & {
content: {
text: string;
};
},
) {
const messageBodyLength = getUtf8Encoder().getSizeFromValue(putativeMessage.content.text);
const maxBodyLength =
MAX_HARDWARE_WALLET_SIGNABLE_MESSAGE_BYTES -
getPreambleLengthForRequiredSignatories(putativeMessage.requiredSignatories);
if (messageBodyLength > maxBodyLength) {
throw new SolanaError(SOLANA_ERROR__OFFCHAIN_MESSAGE__MAXIMUM_LENGTH_EXCEEDED, {
actualBytes: messageBodyLength,
maxBytes: maxBodyLength,
});
}
}

/**
* In the event that you receive a v0 offchain message from an untrusted source, use this function
* to assert that it is one whose content conforms to the
Expand All @@ -71,6 +116,7 @@ export function assertIsOffchainMessageRestrictedAsciiOf1232BytesMax<TMessage ex
}>,
): asserts putativeMessage is OffchainMessageWithRestrictedAsciiOf1232BytesMaxContent & Omit<TMessage, 'content'> {
assertIsOffchainMessageContentRestrictedAsciiOf1232BytesMax(putativeMessage.content);
assertFitsHardwareWalletSignableMessageByteLimit(putativeMessage);
}

/**
Expand All @@ -91,6 +137,7 @@ export function assertIsOffchainMessageUtf8Of1232BytesMax<TMessage extends Offch
}>,
): asserts putativeMessage is OffchainMessageWithUtf8Of1232BytesMaxContent & Omit<TMessage, 'content'> {
assertIsOffchainMessageContentUtf8Of1232BytesMax(putativeMessage.content);
assertFitsHardwareWalletSignableMessageByteLimit(putativeMessage);
}

/**
Expand Down