diff --git a/CHANGELOG.md b/CHANGELOG.md index d348a85..04ec6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- Preserve the sign of positive EIP-712 signed integers supplied as JavaScript numbers. - Reset unfinished sessions when connecting to firmware v9.28.0 or newer, allowing host reconnects while the device remains powered on. diff --git a/src/internal/eth/eip712.ts b/src/internal/eth/eip712.ts index f985551..2357f9a 100644 --- a/src/internal/eth/eip712.ts +++ b/src/internal/eth/eip712.ts @@ -255,13 +255,6 @@ export function encodeValue( } case DataType.INT: { const n = parseIntValue(value); - if (typeof value === 'number') { - const bytes = bigIntToSignedBytesBE(n); - if (bytes.length > 0 && bytes[0] === 0x00) { - return bytes.subarray(1); - } - return bytes; - } return bigIntToSignedBytesBE(n); } case DataType.BOOL: { diff --git a/test/eth-eip712.test.ts b/test/eth-eip712.test.ts index 8057283..7eca418 100644 --- a/test/eth-eip712.test.ts +++ b/test/eth-eip712.test.ts @@ -163,6 +163,21 @@ describe('encodeValue', () => { ]); }); + it.each([ + [0, '00'], [1, '01'], [-1, 'ff'], + [127, '7f'], [128, '0080'], [129, '0081'], + [-127, '81'], [-128, '80'], [-129, 'ff7f'], + [255, '00ff'], [256, '0100'], [-255, 'ff01'], [-256, 'ff00'], + [32767, '7fff'], [32768, '008000'], + [-32768, '8000'], [-32769, 'ff7fff'], + [65535, '00ffff'], [65536, '010000'], + ] as const)('encodes signed %s consistently across input types', (value, expected) => { + const typ = parseType('int64', empty); + for (const input of [value, String(value), BigInt(value)]) { + expect(Buffer.from(encodeValue(typ, input)).toString('hex')).toBe(expected); + } + }); + it('encodes booleans', () => { expect(Array.from(encodeValue(parseType('bool', empty), false))).toEqual([0]); expect(Array.from(encodeValue(parseType('bool', empty), true))).toEqual([1]); diff --git a/test/simulator-eth.test.ts b/test/simulator-eth.test.ts index 22b99be..2fdcb6b 100644 --- a/test/simulator-eth.test.ts +++ b/test/simulator-eth.test.ts @@ -265,6 +265,39 @@ describe.skipIf(!ENABLED).sequential.each(simulatorCases())('simulator eth $name expect(bytesToHex(signatureBytes(sig1))).not.toBe(bytesToHex(signatureBytes(sig2))); }, 60_000); + it('ethSignTypedMessage preserves signed integer values', async () => { + const msg = { + types: { + EIP712Domain: [{ name: 'name', type: 'string' }], + SignedIntegers: [ + { name: 'positive', type: 'int16' }, + { name: 'negative', type: 'int8' }, + { name: 'zero', type: 'int8' }, + ], + }, + primaryType: 'SignedIntegers', + domain: { name: 'Signed integers' }, + message: { positive: 128, negative: -128, zero: 0 }, + }; + const sig = await paired!.ethSignTypedMessage(1n, ETH_KEYPATH, msg, true); + expect(signatureBytes(sig)).toHaveLength(65); + + // Compute the EIP-712 digest independently of the wire encoder. The device + // must sign +128, -128 and zero, not reinterpret or reject their sign bytes. + const domainHash = keccak_256(concatBytes( + keccak_256(utf8ToBytes('EIP712Domain(string name)')), + keccak_256(utf8ToBytes('Signed integers')), + )); + const messageHash = keccak_256(concatBytes( + keccak_256(utf8ToBytes('SignedIntegers(int16 positive,int8 negative,int8 zero)')), + concatBytes(new Uint8Array(31), new Uint8Array([0x80])), + concatBytes(new Uint8Array(31).fill(0xff), new Uint8Array([0x80])), + new Uint8Array(32), + )); + const digest = keccak_256(concatBytes(new Uint8Array([0x19, 0x01]), domainHash, messageHash)); + expectSignatureFromSimulatorAddress(digest, sig); + }, 60_000); + it.skipIf(!atLeast926)('ethSignTypedMessage with anti-klepto disabled is deterministic', async () => { const sig1 = await paired!.ethSignTypedMessage(1n, ETH_KEYPATH, EIP712_MSG, false); const sig2 = await paired!.ethSignTypedMessage(1n, ETH_KEYPATH, EIP712_MSG, false);