@ledgerhq/coin-solana 0.56.0 → 0.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/hw-signMessage.d.ts.map +1 -1
- package/lib/hw-signMessage.js +50 -2
- package/lib/hw-signMessage.js.map +1 -1
- package/lib/offchainMessage/format.d.ts +14 -0
- package/lib/offchainMessage/format.d.ts.map +1 -1
- package/lib/offchainMessage/format.js +69 -32
- package/lib/offchainMessage/format.js.map +1 -1
- package/lib-es/hw-signMessage.d.ts.map +1 -1
- package/lib-es/hw-signMessage.js +51 -3
- package/lib-es/hw-signMessage.js.map +1 -1
- package/lib-es/offchainMessage/format.d.ts +14 -0
- package/lib-es/offchainMessage/format.d.ts.map +1 -1
- package/lib-es/offchainMessage/format.js +68 -32
- package/lib-es/offchainMessage/format.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/unit/hw-signMessage.unit.test.ts +94 -2
- package/src/hw-signMessage.ts +53 -3
- package/src/offchainMessage/format.ts +82 -38
- package/src/offchainMessage/format.unit.test.ts +49 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ledgerhq/coin-solana
|
|
2
2
|
|
|
3
|
+
## 0.56.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#19512](https://github.com/LedgerHQ/ledger-live/pull/19512) [`7c0729b`](https://github.com/LedgerHQ/ledger-live/commit/7c0729b140c2d1e7bfb929eac701a4c6bba2f9a9) Thanks [@fAnselmi-Ledger](https://github.com/fAnselmi-Ledger)! - Add new OCMS v1 msg format
|
|
8
|
+
|
|
9
|
+
## 0.56.1-hotfix.0
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#19512](https://github.com/LedgerHQ/ledger-live/pull/19512) [`7c0729b`](https://github.com/LedgerHQ/ledger-live/commit/7c0729b140c2d1e7bfb929eac701a4c6bba2f9a9) Thanks [@fAnselmi-Ledger](https://github.com/fAnselmi-Ledger)! - Add new OCMS v1 msg format
|
|
14
|
+
|
|
3
15
|
## 0.56.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAOrE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAkBxC,eAAO,MAAM,WAAW,GACrB,eAAe,aAAa,CAAC,YAAY,CAAC,MAEzC,UAAU,QAAQ,EAClB,SAAS,OAAO,EAChB,gBAAgB,UAAU,KACzB,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CA4DA,CAAC"}
|
package/lib/hw-signMessage.js
CHANGED
|
@@ -4,11 +4,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.signMessage = void 0;
|
|
7
|
+
const errors_1 = require("@ledgerhq/errors");
|
|
7
8
|
const bs58_1 = __importDefault(require("bs58"));
|
|
8
9
|
const invariant_1 = __importDefault(require("invariant"));
|
|
9
10
|
const semver_1 = __importDefault(require("semver"));
|
|
10
11
|
const config_1 = __importDefault(require("./config"));
|
|
11
12
|
const format_1 = require("./offchainMessage/format");
|
|
13
|
+
// Returns true for device rejections that should trigger a format fallback
|
|
14
|
+
// (e.g. firmware does not support this OCM layout). Propagates user refusals
|
|
15
|
+
// and locked-device errors so they are never silently retried.
|
|
16
|
+
// Handles both DmkSignerSol (maps to typed errors) and LegacySignerSolana
|
|
17
|
+
// (propagates raw TransportStatusError with a numeric statusCode).
|
|
18
|
+
function isFormatRejection(err) {
|
|
19
|
+
if (err instanceof errors_1.UserRefusedOnDevice)
|
|
20
|
+
return false;
|
|
21
|
+
if (err instanceof errors_1.LockedDeviceError)
|
|
22
|
+
return false;
|
|
23
|
+
// TransportStatusError duck-type: 0x6985 = user refused, 0x5515 = device locked
|
|
24
|
+
if (err && typeof err === "object" && "statusCode" in err) {
|
|
25
|
+
const { statusCode } = err;
|
|
26
|
+
if (statusCode === 0x6985 || statusCode === 0x5515)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
12
31
|
const signMessage = (signerContext) => async (deviceId, account, messageOptions) => {
|
|
13
32
|
const message = messageOptions.message;
|
|
14
33
|
if (!message || typeof message !== "string") {
|
|
@@ -18,8 +37,37 @@ const signMessage = (signerContext) => async (deviceId, account, messageOptions)
|
|
|
18
37
|
const result = await signerContext(deviceId, async (signer) => {
|
|
19
38
|
const { version } = await signer.getAppConfiguration();
|
|
20
39
|
const isLegacy = semver_1.default.lt(version, config_1.default.getCoinConfig().legacyOCMSMaxVersion);
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
if (isLegacy) {
|
|
41
|
+
const ocm = (0, format_1.toOffChainMessage)(message, account.freshAddress, true);
|
|
42
|
+
signedMessage = ocm;
|
|
43
|
+
return signer.signMessage(account.freshAddressPath, ocm.toString("hex"));
|
|
44
|
+
}
|
|
45
|
+
// Step 1 — V1 without length prefix (finalised sRFC 38 spec).
|
|
46
|
+
const v1NoLength = (0, format_1.toOffChainMessageV1)(message, account.freshAddress, false);
|
|
47
|
+
try {
|
|
48
|
+
const sig = await signer.signMessage(account.freshAddressPath, v1NoLength.toString("hex"));
|
|
49
|
+
signedMessage = v1NoLength;
|
|
50
|
+
return sig;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (!isFormatRejection(err))
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
// Step 2 — V1 with length prefix (pre-sRFC-38 firmware).
|
|
57
|
+
const v1WithLength = (0, format_1.toOffChainMessageV1)(message, account.freshAddress, true);
|
|
58
|
+
try {
|
|
59
|
+
const sig = await signer.signMessage(account.freshAddressPath, v1WithLength.toString("hex"));
|
|
60
|
+
signedMessage = v1WithLength;
|
|
61
|
+
return sig;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (!isFormatRejection(err))
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
// Step 3 — V0 fallback (older firmware that does not support V1 at all).
|
|
68
|
+
const v0 = (0, format_1.toOffChainMessage)(message, account.freshAddress, false);
|
|
69
|
+
signedMessage = v0;
|
|
70
|
+
return signer.signMessage(account.freshAddressPath, v0.toString("hex"));
|
|
23
71
|
});
|
|
24
72
|
(0, invariant_1.default)(signedMessage, "signedMessage should exist");
|
|
25
73
|
const signatureCount = Buffer.from([1]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":";;;;;;AAEA,gDAAwB;AACxB,0DAAkC;AAClC,oDAA4B;AAC5B,sDAAkC;AAClC,
|
|
1
|
+
{"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,5 +3,19 @@
|
|
|
3
3
|
* Thus, have not been fully tested (unit tests + manual tests)
|
|
4
4
|
* We should add more tests in the future
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* V1 wire format (sRFC 38, per app-solana parser):
|
|
8
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
9
|
+
* version ( 1 B): 0x01
|
|
10
|
+
* signer count ( 1 B): 1
|
|
11
|
+
* signer (32 B)
|
|
12
|
+
* [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
|
|
13
|
+
* message body (variable)
|
|
14
|
+
*
|
|
15
|
+
* The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
|
|
16
|
+
* reproduces the pre-spec-update layout for backward compatibility with older
|
|
17
|
+
* firmware that rejects the no-prefix form with 6a81.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toOffChainMessageV1(message: string, signerAddress: string, includeLengthPrefix?: boolean): Buffer;
|
|
6
20
|
export declare function toOffChainMessage(message: string, signerAddress: string, isLegacy: boolean): Buffer;
|
|
7
21
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2CH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,mBAAmB,UAAQ,GAC1B,MAAM,CAkBR;AA0BD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,GAChB,MAAM,CA6BR"}
|
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
* We should add more tests in the future
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.toOffChainMessageV1 = toOffChainMessageV1;
|
|
8
9
|
exports.toOffChainMessage = toOffChainMessage;
|
|
9
10
|
const web3_js_1 = require("@solana/web3.js");
|
|
10
|
-
// Max off-chain message length supported by Ledger
|
|
11
|
+
// Max off-chain message length supported by Ledger (= app-solana MAX_OFFCHAIN_MESSAGE_LENGTH)
|
|
11
12
|
const OFFCM_MAX_LEDGER_LEN = 15 * 1024 - 40 - 8;
|
|
12
13
|
const LEGACY_OFFCM_MAX_LEDGER_LEN = 1280 - 40 - 8;
|
|
13
|
-
// Max length of version 0 off-chain message
|
|
14
|
-
const OFFCM_MAX_V0_LEN = 65515;
|
|
15
14
|
const MAX_PRINTABLE_ASCII = 0x7e;
|
|
16
15
|
const MIN_PRINTABLE_ASCII = 0x20;
|
|
17
16
|
const LINE_FEED_ASCII = 0x0a;
|
|
@@ -35,32 +34,63 @@ function isUTF8(buffer) {
|
|
|
35
34
|
}
|
|
36
35
|
function findMessageFormat(messageBuffer, isLegacy) {
|
|
37
36
|
const maxLedgerLen = isLegacy ? LEGACY_OFFCM_MAX_LEDGER_LEN : OFFCM_MAX_LEDGER_LEN;
|
|
38
|
-
if (messageBuffer.length
|
|
39
|
-
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
else if (isUTF8(messageBuffer)) {
|
|
43
|
-
return 1;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
else if (messageBuffer.length <= OFFCM_MAX_V0_LEN) {
|
|
47
|
-
if (isUTF8(messageBuffer)) {
|
|
48
|
-
return 2;
|
|
49
|
-
}
|
|
37
|
+
if (messageBuffer.length > maxLedgerLen) {
|
|
38
|
+
throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${maxLedgerLen})`);
|
|
50
39
|
}
|
|
40
|
+
if (isPrintableASCII(messageBuffer, isLegacy))
|
|
41
|
+
return 0;
|
|
42
|
+
if (isUTF8(messageBuffer))
|
|
43
|
+
return 1;
|
|
51
44
|
return 0;
|
|
52
45
|
}
|
|
46
|
+
const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
|
|
53
47
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
48
|
+
* V1 wire format (sRFC 38, per app-solana parser):
|
|
49
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
50
|
+
* version ( 1 B): 0x01
|
|
51
|
+
* signer count ( 1 B): 1
|
|
52
|
+
* signer (32 B)
|
|
53
|
+
* [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
|
|
54
|
+
* message body (variable)
|
|
55
|
+
*
|
|
56
|
+
* The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
|
|
57
|
+
* reproduces the pre-spec-update layout for backward compatibility with older
|
|
58
|
+
* firmware that rejects the no-prefix form with 6a81.
|
|
61
59
|
*/
|
|
62
|
-
|
|
63
|
-
const
|
|
60
|
+
function toOffChainMessageV1(message, signerAddress, includeLengthPrefix = false) {
|
|
61
|
+
const messageBuffer = Buffer.from(message);
|
|
62
|
+
if (messageBuffer.length > OFFCM_MAX_LEDGER_LEN) {
|
|
63
|
+
throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${OFFCM_MAX_LEDGER_LEN})`);
|
|
64
|
+
}
|
|
65
|
+
const version = Buffer.from([0x01]);
|
|
66
|
+
const signerCount = Buffer.from([0x01]);
|
|
67
|
+
const signer = new web3_js_1.PublicKey(signerAddress).toBuffer();
|
|
68
|
+
if (includeLengthPrefix) {
|
|
69
|
+
const messageLength = Buffer.alloc(2);
|
|
70
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
71
|
+
return Buffer.concat([signingDomain, version, signerCount, signer, messageLength, messageBuffer]);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat([signingDomain, version, signerCount, signer, messageBuffer]);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* V0 wire format:
|
|
77
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
78
|
+
* version ( 1 B): 0x00
|
|
79
|
+
* application domain (32 B): zero-padded
|
|
80
|
+
* format ( 1 B): 0 = ASCII, 1 = UTF-8
|
|
81
|
+
* signer count ( 1 B): 1
|
|
82
|
+
* signer (32 B)
|
|
83
|
+
* message length ( 2 B): little-endian uint16
|
|
84
|
+
* message body (variable)
|
|
85
|
+
*
|
|
86
|
+
* Legacy format (Nano S / old firmware):
|
|
87
|
+
* signing domain (16 B)
|
|
88
|
+
* version ( 1 B): 0x00
|
|
89
|
+
* format ( 1 B)
|
|
90
|
+
* message length ( 2 B): always present
|
|
91
|
+
* message body (variable)
|
|
92
|
+
*/
|
|
93
|
+
const v0HeaderVersion = Buffer.alloc(1); // 0x00
|
|
64
94
|
const applicationDomain = Buffer.alloc(32);
|
|
65
95
|
const messageFormat = Buffer.alloc(1);
|
|
66
96
|
const signerCount = Buffer.alloc(1);
|
|
@@ -70,18 +100,25 @@ function toOffChainMessage(message, signerAddress, isLegacy) {
|
|
|
70
100
|
const messageBuffer = Buffer.from(message);
|
|
71
101
|
messageFormat.writeUInt8(findMessageFormat(messageBuffer, isLegacy));
|
|
72
102
|
const signers = new web3_js_1.PublicKey(signerAddress).toBuffer();
|
|
73
|
-
messageLength.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
: [
|
|
103
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
104
|
+
if (isLegacy) {
|
|
105
|
+
return Buffer.concat([
|
|
77
106
|
signingDomain,
|
|
78
|
-
|
|
79
|
-
applicationDomain,
|
|
107
|
+
v0HeaderVersion,
|
|
80
108
|
messageFormat,
|
|
81
|
-
signerCount,
|
|
82
|
-
signers,
|
|
83
109
|
messageLength,
|
|
84
110
|
messageBuffer,
|
|
85
111
|
]);
|
|
112
|
+
}
|
|
113
|
+
return Buffer.concat([
|
|
114
|
+
signingDomain,
|
|
115
|
+
v0HeaderVersion,
|
|
116
|
+
applicationDomain,
|
|
117
|
+
messageFormat,
|
|
118
|
+
signerCount,
|
|
119
|
+
signers,
|
|
120
|
+
messageLength,
|
|
121
|
+
messageBuffer,
|
|
122
|
+
]);
|
|
86
123
|
}
|
|
87
124
|
//# sourceMappingURL=format.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"hw-signMessage.d.ts","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,0CAA0C,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAOrE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAkBxC,eAAO,MAAM,WAAW,GACrB,eAAe,aAAa,CAAC,YAAY,CAAC,MAEzC,UAAU,QAAQ,EAClB,SAAS,OAAO,EAChB,gBAAgB,UAAU,KACzB,OAAO,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;CACnB,CA4DA,CAAC"}
|
package/lib-es/hw-signMessage.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
+
import { LockedDeviceError, UserRefusedOnDevice } from "@ledgerhq/errors";
|
|
1
2
|
import bs58 from "bs58";
|
|
2
3
|
import invariant from "invariant";
|
|
3
4
|
import semver from "semver";
|
|
4
5
|
import coinConfig from "./config";
|
|
5
|
-
import { toOffChainMessage } from "./offchainMessage/format";
|
|
6
|
+
import { toOffChainMessage, toOffChainMessageV1 } from "./offchainMessage/format";
|
|
7
|
+
// Returns true for device rejections that should trigger a format fallback
|
|
8
|
+
// (e.g. firmware does not support this OCM layout). Propagates user refusals
|
|
9
|
+
// and locked-device errors so they are never silently retried.
|
|
10
|
+
// Handles both DmkSignerSol (maps to typed errors) and LegacySignerSolana
|
|
11
|
+
// (propagates raw TransportStatusError with a numeric statusCode).
|
|
12
|
+
function isFormatRejection(err) {
|
|
13
|
+
if (err instanceof UserRefusedOnDevice)
|
|
14
|
+
return false;
|
|
15
|
+
if (err instanceof LockedDeviceError)
|
|
16
|
+
return false;
|
|
17
|
+
// TransportStatusError duck-type: 0x6985 = user refused, 0x5515 = device locked
|
|
18
|
+
if (err && typeof err === "object" && "statusCode" in err) {
|
|
19
|
+
const { statusCode } = err;
|
|
20
|
+
if (statusCode === 0x6985 || statusCode === 0x5515)
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
6
25
|
export const signMessage = (signerContext) => async (deviceId, account, messageOptions) => {
|
|
7
26
|
const message = messageOptions.message;
|
|
8
27
|
if (!message || typeof message !== "string") {
|
|
@@ -12,8 +31,37 @@ export const signMessage = (signerContext) => async (deviceId, account, messageO
|
|
|
12
31
|
const result = await signerContext(deviceId, async (signer) => {
|
|
13
32
|
const { version } = await signer.getAppConfiguration();
|
|
14
33
|
const isLegacy = semver.lt(version, coinConfig.getCoinConfig().legacyOCMSMaxVersion);
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
if (isLegacy) {
|
|
35
|
+
const ocm = toOffChainMessage(message, account.freshAddress, true);
|
|
36
|
+
signedMessage = ocm;
|
|
37
|
+
return signer.signMessage(account.freshAddressPath, ocm.toString("hex"));
|
|
38
|
+
}
|
|
39
|
+
// Step 1 — V1 without length prefix (finalised sRFC 38 spec).
|
|
40
|
+
const v1NoLength = toOffChainMessageV1(message, account.freshAddress, false);
|
|
41
|
+
try {
|
|
42
|
+
const sig = await signer.signMessage(account.freshAddressPath, v1NoLength.toString("hex"));
|
|
43
|
+
signedMessage = v1NoLength;
|
|
44
|
+
return sig;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (!isFormatRejection(err))
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
// Step 2 — V1 with length prefix (pre-sRFC-38 firmware).
|
|
51
|
+
const v1WithLength = toOffChainMessageV1(message, account.freshAddress, true);
|
|
52
|
+
try {
|
|
53
|
+
const sig = await signer.signMessage(account.freshAddressPath, v1WithLength.toString("hex"));
|
|
54
|
+
signedMessage = v1WithLength;
|
|
55
|
+
return sig;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (!isFormatRejection(err))
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
// Step 3 — V0 fallback (older firmware that does not support V1 at all).
|
|
62
|
+
const v0 = toOffChainMessage(message, account.freshAddress, false);
|
|
63
|
+
signedMessage = v0;
|
|
64
|
+
return signer.signMessage(account.freshAddressPath, v0.toString("hex"));
|
|
17
65
|
});
|
|
18
66
|
invariant(signedMessage, "signedMessage should exist");
|
|
19
67
|
const signatureCount = Buffer.from([1]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"AAEA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,SAAS,MAAM,WAAW,CAAC;AAClC,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,UAAU,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"hw-signMessage.js","sourceRoot":"","sources":["../src/hw-signMessage.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,5 +3,19 @@
|
|
|
3
3
|
* Thus, have not been fully tested (unit tests + manual tests)
|
|
4
4
|
* We should add more tests in the future
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* V1 wire format (sRFC 38, per app-solana parser):
|
|
8
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
9
|
+
* version ( 1 B): 0x01
|
|
10
|
+
* signer count ( 1 B): 1
|
|
11
|
+
* signer (32 B)
|
|
12
|
+
* [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
|
|
13
|
+
* message body (variable)
|
|
14
|
+
*
|
|
15
|
+
* The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
|
|
16
|
+
* reproduces the pre-spec-update layout for backward compatibility with older
|
|
17
|
+
* firmware that rejects the no-prefix form with 6a81.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toOffChainMessageV1(message: string, signerAddress: string, includeLengthPrefix?: boolean): Buffer;
|
|
6
20
|
export declare function toOffChainMessage(message: string, signerAddress: string, isLegacy: boolean): Buffer;
|
|
7
21
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2CH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,mBAAmB,UAAQ,GAC1B,MAAM,CAkBR;AA0BD,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,GAChB,MAAM,CA6BR"}
|
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
* We should add more tests in the future
|
|
5
5
|
*/
|
|
6
6
|
import { PublicKey } from "@solana/web3.js";
|
|
7
|
-
// Max off-chain message length supported by Ledger
|
|
7
|
+
// Max off-chain message length supported by Ledger (= app-solana MAX_OFFCHAIN_MESSAGE_LENGTH)
|
|
8
8
|
const OFFCM_MAX_LEDGER_LEN = 15 * 1024 - 40 - 8;
|
|
9
9
|
const LEGACY_OFFCM_MAX_LEDGER_LEN = 1280 - 40 - 8;
|
|
10
|
-
// Max length of version 0 off-chain message
|
|
11
|
-
const OFFCM_MAX_V0_LEN = 65515;
|
|
12
10
|
const MAX_PRINTABLE_ASCII = 0x7e;
|
|
13
11
|
const MIN_PRINTABLE_ASCII = 0x20;
|
|
14
12
|
const LINE_FEED_ASCII = 0x0a;
|
|
@@ -32,32 +30,63 @@ function isUTF8(buffer) {
|
|
|
32
30
|
}
|
|
33
31
|
function findMessageFormat(messageBuffer, isLegacy) {
|
|
34
32
|
const maxLedgerLen = isLegacy ? LEGACY_OFFCM_MAX_LEDGER_LEN : OFFCM_MAX_LEDGER_LEN;
|
|
35
|
-
if (messageBuffer.length
|
|
36
|
-
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
else if (isUTF8(messageBuffer)) {
|
|
40
|
-
return 1;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else if (messageBuffer.length <= OFFCM_MAX_V0_LEN) {
|
|
44
|
-
if (isUTF8(messageBuffer)) {
|
|
45
|
-
return 2;
|
|
46
|
-
}
|
|
33
|
+
if (messageBuffer.length > maxLedgerLen) {
|
|
34
|
+
throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${maxLedgerLen})`);
|
|
47
35
|
}
|
|
36
|
+
if (isPrintableASCII(messageBuffer, isLegacy))
|
|
37
|
+
return 0;
|
|
38
|
+
if (isUTF8(messageBuffer))
|
|
39
|
+
return 1;
|
|
48
40
|
return 0;
|
|
49
41
|
}
|
|
42
|
+
const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
|
|
50
43
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
44
|
+
* V1 wire format (sRFC 38, per app-solana parser):
|
|
45
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
46
|
+
* version ( 1 B): 0x01
|
|
47
|
+
* signer count ( 1 B): 1
|
|
48
|
+
* signer (32 B)
|
|
49
|
+
* [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
|
|
50
|
+
* message body (variable)
|
|
51
|
+
*
|
|
52
|
+
* The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
|
|
53
|
+
* reproduces the pre-spec-update layout for backward compatibility with older
|
|
54
|
+
* firmware that rejects the no-prefix form with 6a81.
|
|
58
55
|
*/
|
|
59
|
-
|
|
60
|
-
const
|
|
56
|
+
export function toOffChainMessageV1(message, signerAddress, includeLengthPrefix = false) {
|
|
57
|
+
const messageBuffer = Buffer.from(message);
|
|
58
|
+
if (messageBuffer.length > OFFCM_MAX_LEDGER_LEN) {
|
|
59
|
+
throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${OFFCM_MAX_LEDGER_LEN})`);
|
|
60
|
+
}
|
|
61
|
+
const version = Buffer.from([0x01]);
|
|
62
|
+
const signerCount = Buffer.from([0x01]);
|
|
63
|
+
const signer = new PublicKey(signerAddress).toBuffer();
|
|
64
|
+
if (includeLengthPrefix) {
|
|
65
|
+
const messageLength = Buffer.alloc(2);
|
|
66
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
67
|
+
return Buffer.concat([signingDomain, version, signerCount, signer, messageLength, messageBuffer]);
|
|
68
|
+
}
|
|
69
|
+
return Buffer.concat([signingDomain, version, signerCount, signer, messageBuffer]);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* V0 wire format:
|
|
73
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
74
|
+
* version ( 1 B): 0x00
|
|
75
|
+
* application domain (32 B): zero-padded
|
|
76
|
+
* format ( 1 B): 0 = ASCII, 1 = UTF-8
|
|
77
|
+
* signer count ( 1 B): 1
|
|
78
|
+
* signer (32 B)
|
|
79
|
+
* message length ( 2 B): little-endian uint16
|
|
80
|
+
* message body (variable)
|
|
81
|
+
*
|
|
82
|
+
* Legacy format (Nano S / old firmware):
|
|
83
|
+
* signing domain (16 B)
|
|
84
|
+
* version ( 1 B): 0x00
|
|
85
|
+
* format ( 1 B)
|
|
86
|
+
* message length ( 2 B): always present
|
|
87
|
+
* message body (variable)
|
|
88
|
+
*/
|
|
89
|
+
const v0HeaderVersion = Buffer.alloc(1); // 0x00
|
|
61
90
|
const applicationDomain = Buffer.alloc(32);
|
|
62
91
|
const messageFormat = Buffer.alloc(1);
|
|
63
92
|
const signerCount = Buffer.alloc(1);
|
|
@@ -67,18 +96,25 @@ export function toOffChainMessage(message, signerAddress, isLegacy) {
|
|
|
67
96
|
const messageBuffer = Buffer.from(message);
|
|
68
97
|
messageFormat.writeUInt8(findMessageFormat(messageBuffer, isLegacy));
|
|
69
98
|
const signers = new PublicKey(signerAddress).toBuffer();
|
|
70
|
-
messageLength.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
: [
|
|
99
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
100
|
+
if (isLegacy) {
|
|
101
|
+
return Buffer.concat([
|
|
74
102
|
signingDomain,
|
|
75
|
-
|
|
76
|
-
applicationDomain,
|
|
103
|
+
v0HeaderVersion,
|
|
77
104
|
messageFormat,
|
|
78
|
-
signerCount,
|
|
79
|
-
signers,
|
|
80
105
|
messageLength,
|
|
81
106
|
messageBuffer,
|
|
82
107
|
]);
|
|
108
|
+
}
|
|
109
|
+
return Buffer.concat([
|
|
110
|
+
signingDomain,
|
|
111
|
+
v0HeaderVersion,
|
|
112
|
+
applicationDomain,
|
|
113
|
+
messageFormat,
|
|
114
|
+
signerCount,
|
|
115
|
+
signers,
|
|
116
|
+
messageLength,
|
|
117
|
+
messageBuffer,
|
|
118
|
+
]);
|
|
83
119
|
}
|
|
84
120
|
//# sourceMappingURL=format.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/offchainMessage/format.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer";
|
|
2
2
|
import { Account, AnyMessage, TypedEvmMessage } from "@ledgerhq/types-live";
|
|
3
|
+
import { LockedDeviceError, UserRefusedOnDevice } from "@ledgerhq/errors";
|
|
3
4
|
import bs58 from "bs58";
|
|
4
5
|
import coinConfig from "../../config";
|
|
5
6
|
import { signMessage } from "../../hw-signMessage";
|
|
@@ -20,7 +21,7 @@ describe("Testing call to hardware off-chain sign message on Solana", () => {
|
|
|
20
21
|
const BASE58_SIGNATURE =
|
|
21
22
|
"23mXB2pc8EQzc2mT3VJR4KkBFZaUVL9Pn7LUn8NMKeHKbS1hMj1QqGsUHHD3JMGhAWtFfcmnPhFSpPttChTNzsB9";
|
|
22
23
|
const BASE58_ENVELOPE =
|
|
23
|
-
"
|
|
24
|
+
"nHpqPBfv1CJUvmpaDaHp4hZZmoucL7f9qFkdwdQj1hFGbLWwWP5rBaBzh2kSFSNJSj5g8yC3Yo5TBHQMCE4ezkQthvpDMpcRvoLkYdwvvDZsqjt3AQnYGzV5fCCrZGnA1pecUUgczB4weoTSQmocrFX2dhqdZ39B71jwWShjn9pW3NY3DtvvMTug9vEkZW8b2GWPbaqoiXMZXAwGQXu4hVBYc8ScN1qV1BXHzX8FCPA";
|
|
24
25
|
|
|
25
26
|
const getAppConfigurationMock = jest.fn(() => {
|
|
26
27
|
return Promise.resolve({
|
|
@@ -44,7 +45,7 @@ describe("Testing call to hardware off-chain sign message on Solana", () => {
|
|
|
44
45
|
const freshAddress = "8DpKDisipx6f76cEmuGvCX9TrA3SjeR76HaTRePxHBDe";
|
|
45
46
|
const messageHex = "54657374696E67206F6E20536F6C616E61";
|
|
46
47
|
const offchainMessage =
|
|
47
|
-
"
|
|
48
|
+
"ff736f6c616e61206f6666636861696e01016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be0323535343635373337343639364536373230364636453230353336463643363136453631";
|
|
48
49
|
|
|
49
50
|
const result = await signMessage(signerContext)(
|
|
50
51
|
"",
|
|
@@ -104,6 +105,97 @@ describe("Testing call to hardware off-chain sign message on Solana", () => {
|
|
|
104
105
|
expect(signMessageMock).toHaveBeenCalledWith(accountFreshAddressPath, offchainMessage);
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
describe("non-legacy fallback sequencing", () => {
|
|
109
|
+
const APP_VERSION = "1.8.2";
|
|
110
|
+
const SIGNATURE =
|
|
111
|
+
"4gVuB1KsM58fb3vRpnDucwW4Vi6fVGA51QDQd9ARvx4GH5yYVDPzDnvzUbSJf3YLWWdsX7zCMSN9N1GMnTYwWiJf";
|
|
112
|
+
const accountFreshAddressPath = "44'/501'/0'/0/0";
|
|
113
|
+
const freshAddress = "8DpKDisipx6f76cEmuGvCX9TrA3SjeR76HaTRePxHBDe";
|
|
114
|
+
const messageHex = "54657374696E67206F6E20536F6C616E61";
|
|
115
|
+
|
|
116
|
+
const V1_NO_LENGTH =
|
|
117
|
+
"ff736f6c616e61206f6666636861696e01016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be0323535343635373337343639364536373230364636453230353336463643363136453631";
|
|
118
|
+
const V1_WITH_LENGTH =
|
|
119
|
+
"ff736f6c616e61206f6666636861696e01016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be03235220035343635373337343639364536373230364636453230353336463643363136453631";
|
|
120
|
+
const V0 =
|
|
121
|
+
"ff736f6c616e61206f6666636861696e00000000000000000000000000000000000000000000000000000000000000000000016b4a46c53959cac0eff146ab323053cfc503321adfd453a7c67c91a24be03235220035343635373337343639364536373230364636453230353336463643363136453631";
|
|
122
|
+
|
|
123
|
+
function makeContext(signMessageMock: jest.Mock): SignerContext<SolanaSigner> {
|
|
124
|
+
const signer: SolanaSigner = {
|
|
125
|
+
getAppConfiguration: jest.fn(() =>
|
|
126
|
+
Promise.resolve({
|
|
127
|
+
version: APP_VERSION,
|
|
128
|
+
blindSigningEnabled: false,
|
|
129
|
+
pubKeyDisplayMode: PubKeyDisplayMode.LONG,
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
getAddress: jest.fn(),
|
|
133
|
+
signTransaction: jest.fn(),
|
|
134
|
+
signMessage: signMessageMock,
|
|
135
|
+
};
|
|
136
|
+
return (_, fn) => fn(signer);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
it("falls back from V1 no-length to V1 with-length on format rejection", async () => {
|
|
140
|
+
const signMessageMock = jest
|
|
141
|
+
.fn()
|
|
142
|
+
.mockRejectedValueOnce(new Error("format not supported"))
|
|
143
|
+
.mockResolvedValueOnce({ signature: Buffer.from(SIGNATURE) });
|
|
144
|
+
|
|
145
|
+
await signMessage(makeContext(signMessageMock))(
|
|
146
|
+
"",
|
|
147
|
+
{ freshAddressPath: accountFreshAddressPath, freshAddress } as Account,
|
|
148
|
+
{ message: messageHex },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(signMessageMock).toHaveBeenCalledTimes(2);
|
|
152
|
+
expect(signMessageMock).toHaveBeenNthCalledWith(1, accountFreshAddressPath, V1_NO_LENGTH);
|
|
153
|
+
expect(signMessageMock).toHaveBeenNthCalledWith(2, accountFreshAddressPath, V1_WITH_LENGTH);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("falls back all the way to V0 when both V1 variants are rejected", async () => {
|
|
157
|
+
const signMessageMock = jest
|
|
158
|
+
.fn()
|
|
159
|
+
.mockRejectedValueOnce(new Error("format not supported"))
|
|
160
|
+
.mockRejectedValueOnce(new Error("format not supported"))
|
|
161
|
+
.mockResolvedValueOnce({ signature: Buffer.from(SIGNATURE) });
|
|
162
|
+
|
|
163
|
+
await signMessage(makeContext(signMessageMock))(
|
|
164
|
+
"",
|
|
165
|
+
{ freshAddressPath: accountFreshAddressPath, freshAddress } as Account,
|
|
166
|
+
{ message: messageHex },
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(signMessageMock).toHaveBeenCalledTimes(3);
|
|
170
|
+
expect(signMessageMock).toHaveBeenNthCalledWith(1, accountFreshAddressPath, V1_NO_LENGTH);
|
|
171
|
+
expect(signMessageMock).toHaveBeenNthCalledWith(2, accountFreshAddressPath, V1_WITH_LENGTH);
|
|
172
|
+
expect(signMessageMock).toHaveBeenNthCalledWith(3, accountFreshAddressPath, V0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it.each([
|
|
176
|
+
["UserRefusedOnDevice", new UserRefusedOnDevice()],
|
|
177
|
+
["LockedDeviceError", new LockedDeviceError()],
|
|
178
|
+
["TransportStatusError 0x6985 (user refused)", { statusCode: 0x6985 }],
|
|
179
|
+
["TransportStatusError 0x5515 (device locked)", { statusCode: 0x5515 }],
|
|
180
|
+
])("does not retry on %s", async (_label, error) => {
|
|
181
|
+
const signMessageMock = jest.fn().mockRejectedValue(error);
|
|
182
|
+
|
|
183
|
+
let caught: unknown;
|
|
184
|
+
try {
|
|
185
|
+
await signMessage(makeContext(signMessageMock))(
|
|
186
|
+
"",
|
|
187
|
+
{ freshAddressPath: accountFreshAddressPath, freshAddress } as Account,
|
|
188
|
+
{ message: messageHex },
|
|
189
|
+
);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
caught = e;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
expect(caught).toBe(error);
|
|
195
|
+
expect(signMessageMock).toHaveBeenCalledTimes(1);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
107
199
|
it.each([{} as AnyMessage, {} as TypedEvmMessage])(
|
|
108
200
|
"should throw an error when message options have not DefaultMessage type",
|
|
109
201
|
async (messageOptions: AnyMessage) => {
|
package/src/hw-signMessage.ts
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { SignerContext } from "@ledgerhq/ledger-wallet-framework/signer";
|
|
2
2
|
import { Account, AnyMessage, DeviceId } from "@ledgerhq/types-live";
|
|
3
|
+
import { LockedDeviceError, UserRefusedOnDevice } from "@ledgerhq/errors";
|
|
3
4
|
import bs58 from "bs58";
|
|
4
5
|
import invariant from "invariant";
|
|
5
6
|
import semver from "semver";
|
|
6
7
|
import coinConfig from "./config";
|
|
7
|
-
import { toOffChainMessage } from "./offchainMessage/format";
|
|
8
|
+
import { toOffChainMessage, toOffChainMessageV1 } from "./offchainMessage/format";
|
|
8
9
|
import { SolanaSigner } from "./signer";
|
|
9
10
|
|
|
11
|
+
// Returns true for device rejections that should trigger a format fallback
|
|
12
|
+
// (e.g. firmware does not support this OCM layout). Propagates user refusals
|
|
13
|
+
// and locked-device errors so they are never silently retried.
|
|
14
|
+
// Handles both DmkSignerSol (maps to typed errors) and LegacySignerSolana
|
|
15
|
+
// (propagates raw TransportStatusError with a numeric statusCode).
|
|
16
|
+
function isFormatRejection(err: unknown): boolean {
|
|
17
|
+
if (err instanceof UserRefusedOnDevice) return false;
|
|
18
|
+
if (err instanceof LockedDeviceError) return false;
|
|
19
|
+
// TransportStatusError duck-type: 0x6985 = user refused, 0x5515 = device locked
|
|
20
|
+
if (err && typeof err === "object" && "statusCode" in err) {
|
|
21
|
+
const { statusCode } = err as { statusCode: number };
|
|
22
|
+
if (statusCode === 0x6985 || statusCode === 0x5515) return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
export const signMessage =
|
|
11
28
|
(signerContext: SignerContext<SolanaSigner>) =>
|
|
12
29
|
async (
|
|
@@ -29,9 +46,42 @@ export const signMessage =
|
|
|
29
46
|
const { version } = await signer.getAppConfiguration();
|
|
30
47
|
const isLegacy = semver.lt(version, coinConfig.getCoinConfig().legacyOCMSMaxVersion);
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
if (isLegacy) {
|
|
50
|
+
const ocm = toOffChainMessage(message, account.freshAddress, true);
|
|
51
|
+
signedMessage = ocm;
|
|
52
|
+
return signer.signMessage(account.freshAddressPath, ocm.toString("hex"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step 1 — V1 without length prefix (finalised sRFC 38 spec).
|
|
56
|
+
const v1NoLength = toOffChainMessageV1(message, account.freshAddress, false);
|
|
57
|
+
try {
|
|
58
|
+
const sig = await signer.signMessage(
|
|
59
|
+
account.freshAddressPath,
|
|
60
|
+
v1NoLength.toString("hex"),
|
|
61
|
+
);
|
|
62
|
+
signedMessage = v1NoLength;
|
|
63
|
+
return sig;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
if (!isFormatRejection(err)) throw err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 2 — V1 with length prefix (pre-sRFC-38 firmware).
|
|
69
|
+
const v1WithLength = toOffChainMessageV1(message, account.freshAddress, true);
|
|
70
|
+
try {
|
|
71
|
+
const sig = await signer.signMessage(
|
|
72
|
+
account.freshAddressPath,
|
|
73
|
+
v1WithLength.toString("hex"),
|
|
74
|
+
);
|
|
75
|
+
signedMessage = v1WithLength;
|
|
76
|
+
return sig;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (!isFormatRejection(err)) throw err;
|
|
79
|
+
}
|
|
33
80
|
|
|
34
|
-
|
|
81
|
+
// Step 3 — V0 fallback (older firmware that does not support V1 at all).
|
|
82
|
+
const v0 = toOffChainMessage(message, account.freshAddress, false);
|
|
83
|
+
signedMessage = v0;
|
|
84
|
+
return signer.signMessage(account.freshAddressPath, v0.toString("hex"));
|
|
35
85
|
});
|
|
36
86
|
|
|
37
87
|
invariant(signedMessage, "signedMessage should exist");
|
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { PublicKey } from "@solana/web3.js";
|
|
8
8
|
|
|
9
|
-
// Max off-chain message length supported by Ledger
|
|
9
|
+
// Max off-chain message length supported by Ledger (= app-solana MAX_OFFCHAIN_MESSAGE_LENGTH)
|
|
10
10
|
const OFFCM_MAX_LEDGER_LEN = 15 * 1024 - 40 - 8;
|
|
11
11
|
const LEGACY_OFFCM_MAX_LEDGER_LEN = 1280 - 40 - 8;
|
|
12
|
-
// Max length of version 0 off-chain message
|
|
13
|
-
const OFFCM_MAX_V0_LEN = 65515;
|
|
14
12
|
|
|
15
13
|
const MAX_PRINTABLE_ASCII = 0x7e;
|
|
16
14
|
const MIN_PRINTABLE_ASCII = 0x20;
|
|
@@ -37,32 +35,72 @@ function isUTF8(buffer: Buffer) {
|
|
|
37
35
|
|
|
38
36
|
function findMessageFormat(messageBuffer: Buffer, isLegacy: boolean): number {
|
|
39
37
|
const maxLedgerLen = isLegacy ? LEGACY_OFFCM_MAX_LEDGER_LEN : OFFCM_MAX_LEDGER_LEN;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (isPrintableASCII(messageBuffer, isLegacy)) {
|
|
43
|
-
return 0;
|
|
44
|
-
} else if (isUTF8(messageBuffer)) {
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
} else if (messageBuffer.length <= OFFCM_MAX_V0_LEN) {
|
|
48
|
-
if (isUTF8(messageBuffer)) {
|
|
49
|
-
return 2;
|
|
50
|
-
}
|
|
38
|
+
if (messageBuffer.length > maxLedgerLen) {
|
|
39
|
+
throw new Error(`Message too long: ${messageBuffer.length} bytes (max is ${maxLedgerLen})`);
|
|
51
40
|
}
|
|
41
|
+
if (isPrintableASCII(messageBuffer, isLegacy)) return 0;
|
|
42
|
+
if (isUTF8(messageBuffer)) return 1;
|
|
52
43
|
return 0;
|
|
53
44
|
}
|
|
54
45
|
|
|
46
|
+
const signingDomain = Buffer.concat([Buffer.from([255]), Buffer.from("solana offchain")]);
|
|
47
|
+
|
|
55
48
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
49
|
+
* V1 wire format (sRFC 38, per app-solana parser):
|
|
50
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
51
|
+
* version ( 1 B): 0x01
|
|
52
|
+
* signer count ( 1 B): 1
|
|
53
|
+
* signer (32 B)
|
|
54
|
+
* [message length ( 2 B): little-endian uint16] — only when includeLengthPrefix
|
|
55
|
+
* message body (variable)
|
|
56
|
+
*
|
|
57
|
+
* The finalised sRFC 38 layout drops the length prefix. `includeLengthPrefix`
|
|
58
|
+
* reproduces the pre-spec-update layout for backward compatibility with older
|
|
59
|
+
* firmware that rejects the no-prefix form with 6a81.
|
|
63
60
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
export function toOffChainMessageV1(
|
|
62
|
+
message: string,
|
|
63
|
+
signerAddress: string,
|
|
64
|
+
includeLengthPrefix = false,
|
|
65
|
+
): Buffer {
|
|
66
|
+
const messageBuffer = Buffer.from(message);
|
|
67
|
+
if (messageBuffer.length > OFFCM_MAX_LEDGER_LEN) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Message too long: ${messageBuffer.length} bytes (max is ${OFFCM_MAX_LEDGER_LEN})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const version = Buffer.from([0x01]);
|
|
74
|
+
const signerCount = Buffer.from([0x01]);
|
|
75
|
+
const signer = new PublicKey(signerAddress).toBuffer();
|
|
76
|
+
|
|
77
|
+
if (includeLengthPrefix) {
|
|
78
|
+
const messageLength = Buffer.alloc(2);
|
|
79
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
80
|
+
return Buffer.concat([signingDomain, version, signerCount, signer, messageLength, messageBuffer]);
|
|
81
|
+
}
|
|
82
|
+
return Buffer.concat([signingDomain, version, signerCount, signer, messageBuffer]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* V0 wire format:
|
|
87
|
+
* signing domain (16 B): 0xFF + "solana offchain"
|
|
88
|
+
* version ( 1 B): 0x00
|
|
89
|
+
* application domain (32 B): zero-padded
|
|
90
|
+
* format ( 1 B): 0 = ASCII, 1 = UTF-8
|
|
91
|
+
* signer count ( 1 B): 1
|
|
92
|
+
* signer (32 B)
|
|
93
|
+
* message length ( 2 B): little-endian uint16
|
|
94
|
+
* message body (variable)
|
|
95
|
+
*
|
|
96
|
+
* Legacy format (Nano S / old firmware):
|
|
97
|
+
* signing domain (16 B)
|
|
98
|
+
* version ( 1 B): 0x00
|
|
99
|
+
* format ( 1 B)
|
|
100
|
+
* message length ( 2 B): always present
|
|
101
|
+
* message body (variable)
|
|
102
|
+
*/
|
|
103
|
+
const v0HeaderVersion = Buffer.alloc(1); // 0x00
|
|
66
104
|
const applicationDomain = Buffer.alloc(32);
|
|
67
105
|
const messageFormat = Buffer.alloc(1);
|
|
68
106
|
const signerCount = Buffer.alloc(1);
|
|
@@ -79,20 +117,26 @@ export function toOffChainMessage(
|
|
|
79
117
|
|
|
80
118
|
const signers = new PublicKey(signerAddress).toBuffer();
|
|
81
119
|
|
|
82
|
-
messageLength.
|
|
120
|
+
messageLength.writeUInt16LE(messageBuffer.length);
|
|
121
|
+
|
|
122
|
+
if (isLegacy) {
|
|
123
|
+
return Buffer.concat([
|
|
124
|
+
signingDomain,
|
|
125
|
+
v0HeaderVersion,
|
|
126
|
+
messageFormat,
|
|
127
|
+
messageLength,
|
|
128
|
+
messageBuffer,
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
83
131
|
|
|
84
|
-
return Buffer.concat(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
messageLength,
|
|
95
|
-
messageBuffer,
|
|
96
|
-
],
|
|
97
|
-
);
|
|
132
|
+
return Buffer.concat([
|
|
133
|
+
signingDomain,
|
|
134
|
+
v0HeaderVersion,
|
|
135
|
+
applicationDomain,
|
|
136
|
+
messageFormat,
|
|
137
|
+
signerCount,
|
|
138
|
+
signers,
|
|
139
|
+
messageLength,
|
|
140
|
+
messageBuffer,
|
|
141
|
+
]);
|
|
98
142
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PublicKey } from "@solana/web3.js";
|
|
2
|
-
import { toOffChainMessage } from "./format";
|
|
2
|
+
import { toOffChainMessage, toOffChainMessageV1 } from "./format";
|
|
3
3
|
|
|
4
4
|
type MessageCase = {
|
|
5
5
|
value: string;
|
|
@@ -78,8 +78,9 @@ Issued At: now`,
|
|
|
78
78
|
const addressHex = new PublicKey(address).toBuffer().toString("hex");
|
|
79
79
|
const emptyApplicationDomain = "0000000000000000000000000000000000000000000000000000000000000000";
|
|
80
80
|
|
|
81
|
+
// V0 layout: header(32) + version(2) + appDomain(64) + format(2) + signerCount(2) + signer(64) + length(4) + body
|
|
81
82
|
it.each(cases)(
|
|
82
|
-
'should format message "$value" correctly for off-chain signing on Solana',
|
|
83
|
+
'should format message "$value" correctly for off-chain signing on Solana (V0)',
|
|
83
84
|
({ value, hexadecimal, messageFormat, messageLength }) => {
|
|
84
85
|
const result = toOffChainMessage(value, address, false).toString("hex");
|
|
85
86
|
|
|
@@ -106,11 +107,56 @@ Issued At: now`,
|
|
|
106
107
|
const length = result.substring(SOLANA_HEADER.length + 134, SOLANA_HEADER.length + 138);
|
|
107
108
|
expect(length).toEqual(messageLength);
|
|
108
109
|
|
|
109
|
-
// message should end with message parameter
|
|
110
110
|
expect(result.substring(SOLANA_HEADER.length + 138)).toEqual(hexadecimal);
|
|
111
111
|
},
|
|
112
112
|
);
|
|
113
113
|
|
|
114
|
+
// V1 layout (no length): header(32) + version(2) + signerCount(2) + signer(64) + body
|
|
115
|
+
it.each(cases)(
|
|
116
|
+
'should format message "$value" correctly for off-chain signing on Solana (V1, no length)',
|
|
117
|
+
({ value, hexadecimal, messageLength: _ignored }) => {
|
|
118
|
+
const result = toOffChainMessageV1(value, address).toString("hex");
|
|
119
|
+
|
|
120
|
+
expect(result.startsWith(SOLANA_HEADER)).toBe(true);
|
|
121
|
+
|
|
122
|
+
const version = result.substring(SOLANA_HEADER.length, SOLANA_HEADER.length + 2);
|
|
123
|
+
expect(version).toEqual("01");
|
|
124
|
+
|
|
125
|
+
const signerCount = result.substring(SOLANA_HEADER.length + 2, SOLANA_HEADER.length + 4);
|
|
126
|
+
expect(signerCount).toEqual("01");
|
|
127
|
+
|
|
128
|
+
const signerAddress = result.substring(SOLANA_HEADER.length + 4, SOLANA_HEADER.length + 68);
|
|
129
|
+
expect(signerAddress).toEqual(addressHex);
|
|
130
|
+
|
|
131
|
+
// no length field — message body starts immediately after signer
|
|
132
|
+
expect(result.substring(SOLANA_HEADER.length + 68)).toEqual(hexadecimal);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// V1 layout (with length): header(32) + version(2) + signerCount(2) + signer(64) + length(4) + body
|
|
137
|
+
it.each(cases)(
|
|
138
|
+
'should format message "$value" correctly for off-chain signing on Solana (V1, with length prefix)',
|
|
139
|
+
({ value, hexadecimal, messageLength }) => {
|
|
140
|
+
const result = toOffChainMessageV1(value, address, true).toString("hex");
|
|
141
|
+
|
|
142
|
+
expect(result.startsWith(SOLANA_HEADER)).toBe(true);
|
|
143
|
+
|
|
144
|
+
const version = result.substring(SOLANA_HEADER.length, SOLANA_HEADER.length + 2);
|
|
145
|
+
expect(version).toEqual("01");
|
|
146
|
+
|
|
147
|
+
const signerCount = result.substring(SOLANA_HEADER.length + 2, SOLANA_HEADER.length + 4);
|
|
148
|
+
expect(signerCount).toEqual("01");
|
|
149
|
+
|
|
150
|
+
const signerAddress = result.substring(SOLANA_HEADER.length + 4, SOLANA_HEADER.length + 68);
|
|
151
|
+
expect(signerAddress).toEqual(addressHex);
|
|
152
|
+
|
|
153
|
+
const length = result.substring(SOLANA_HEADER.length + 68, SOLANA_HEADER.length + 72);
|
|
154
|
+
expect(length).toEqual(messageLength);
|
|
155
|
+
|
|
156
|
+
expect(result.substring(SOLANA_HEADER.length + 72)).toEqual(hexadecimal);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
114
160
|
const legacyCases: MessageCase[] = [
|
|
115
161
|
...sharedCases,
|
|
116
162
|
{
|