Skip to content

Commit f275388

Browse files
committed
fix(metadata): make signing preconditions survive python -O
serialize_metadata, serialize_schema_metadata, token_amount_value and schema_calldata checked their preconditions with `assert`, which is stripped under python -O. An optimized signing process would therefore serialize and sign a 19-byte contract address, a 5-byte selector, a 65-byte method name or a 9th argument instead of refusing it. These functions build the bytes that get signed; a signing precondition must not be an assertion. Replaced with a _require() helper raising ValueError, carrying the offending value in the message. Verified under -O that a 19-byte address and a 65-character method name are both still rejected. No test depended on AssertionError from these paths.
1 parent d46a985 commit f275388

1 file changed

Lines changed: 50 additions & 22 deletions

File tree

keepkeylib/signed_metadata.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,31 @@
3333
METADATA_MAX_ARG_VALUE_LEN = 44
3434

3535

36+
def _require(condition, message):
37+
"""Precondition check that survives `python -O`.
38+
39+
These serializers build the bytes that get SIGNED. `assert` is stripped
40+
under -O, so an optimized process would serialize and sign an over-long
41+
method name, a 19-byte address or a 9th argument instead of refusing it.
42+
A signing precondition must not be an assertion.
43+
"""
44+
if not condition:
45+
raise ValueError(message)
46+
47+
3648
def token_amount_value(amount, decimals, symbol):
3749
"""Build an ARG_FORMAT_TOKEN_AMOUNT value: decimals + symbol + amount.
3850
3951
amount: non-negative int (raw on-chain units). decimals: int 0..36.
4052
symbol: short ticker, [A-Za-z0-9], <=10 chars.
4153
"""
4254
sym = symbol.encode('ascii')
43-
assert 0 < len(sym) <= 10 and sym.isalnum()
44-
assert 0 <= decimals <= 36
55+
_require(0 < len(sym) <= 10 and sym.isalnum(),
56+
'symbol must be 1-10 alphanumeric characters, got %r' % symbol)
57+
_require(0 <= decimals <= 36, 'decimals must be 0..36, got %r' % decimals)
4558
# Minimal big-endian amount, at least 1 byte, at most 32.
4659
n = amount.to_bytes(32, 'big').lstrip(b'\x00') or b'\x00'
47-
assert len(n) <= 32
60+
_require(len(n) <= 32, 'amount does not fit in 32 bytes')
4861
return bytes([decimals, len(sym)]) + sym + n
4962

5063
CLASSIFICATION_OPAQUE = 0
@@ -186,11 +199,14 @@ def serialize_metadata(
186199
if timestamp is None:
187200
timestamp = int(time.time())
188201

189-
assert len(contract_address) == 20
190-
assert len(selector) == 4
191-
assert len(tx_hash) == 32
192-
assert len(method_name.encode('utf-8')) <= 64
193-
assert len(args) <= 8
202+
_require(len(contract_address) == 20,
203+
'contract_address must be 20 bytes, got %d' % len(contract_address))
204+
_require(len(selector) == 4, 'selector must be 4 bytes, got %d' % len(selector))
205+
_require(len(tx_hash) == 32, 'tx_hash must be 32 bytes, got %d' % len(tx_hash))
206+
_require(len(method_name.encode('utf-8')) <= 64,
207+
'method_name must be <=64 UTF-8 bytes, got %d'
208+
% len(method_name.encode('utf-8')))
209+
_require(len(args) <= 8, 'at most 8 args, got %d' % len(args))
194210

195211
buf = bytearray()
196212

@@ -221,7 +237,8 @@ def serialize_metadata(
221237
for arg in args:
222238
# name (1-byte length prefix + UTF-8)
223239
arg_name = arg['name'].encode('utf-8')
224-
assert len(arg_name) <= 32
240+
_require(len(arg_name) <= 32,
241+
'arg name must be <=32 UTF-8 bytes, got %d' % len(arg_name))
225242
buf.append(len(arg_name))
226243
buf.extend(arg_name)
227244

@@ -230,7 +247,9 @@ def serialize_metadata(
230247

231248
# value (2-byte length prefix + raw bytes)
232249
val = arg['value']
233-
assert len(val) <= METADATA_MAX_ARG_VALUE_LEN
250+
_require(len(val) <= METADATA_MAX_ARG_VALUE_LEN,
251+
'arg value must be <=%d bytes, got %d'
252+
% (METADATA_MAX_ARG_VALUE_LEN, len(val)))
234253
buf.extend(struct.pack('>H', len(val)))
235254
buf.extend(val)
236255

@@ -286,10 +305,13 @@ def serialize_schema_metadata(
286305
if timestamp is None:
287306
timestamp = int(time.time())
288307

289-
assert len(contract_address) == 20
290-
assert len(selector) == 4
291-
assert len(method_name.encode('utf-8')) <= 64
292-
assert len(args) <= 8
308+
_require(len(contract_address) == 20,
309+
'contract_address must be 20 bytes, got %d' % len(contract_address))
310+
_require(len(selector) == 4, 'selector must be 4 bytes, got %d' % len(selector))
311+
_require(len(method_name.encode('utf-8')) <= 64,
312+
'method_name must be <=64 UTF-8 bytes, got %d'
313+
% len(method_name.encode('utf-8')))
314+
_require(len(args) <= 8, 'at most 8 args, got %d' % len(args))
293315

294316
buf = bytearray()
295317
buf.append(METADATA_VERSION_SCHEMA)
@@ -304,19 +326,24 @@ def serialize_schema_metadata(
304326
buf.append(len(args))
305327
for arg in args:
306328
arg_name = arg['name'].encode('utf-8')
307-
assert len(arg_name) <= 32
329+
_require(len(arg_name) <= 32,
330+
'arg name must be <=32 UTF-8 bytes, got %d' % len(arg_name))
308331
buf.append(len(arg_name))
309332
buf.extend(arg_name)
310333

311334
fmt = arg['format']
312-
assert fmt in (ARG_FORMAT_ADDRESS, ARG_FORMAT_AMOUNT,
313-
ARG_FORMAT_TOKEN_AMOUNT), \
314-
'v2 supports only fixed-word ADDRESS/AMOUNT/TOKEN_AMOUNT'
335+
_require(fmt in (ARG_FORMAT_ADDRESS, ARG_FORMAT_AMOUNT,
336+
ARG_FORMAT_TOKEN_AMOUNT),
337+
'v2 supports only fixed-word ADDRESS/AMOUNT/TOKEN_AMOUNT, '
338+
'got %r' % fmt)
315339
buf.append(fmt)
316340
if fmt == ARG_FORMAT_TOKEN_AMOUNT:
317341
sym = arg['symbol'].encode('ascii')
318-
assert 0 < len(sym) <= 10 and sym.isalnum()
319-
assert 0 <= arg['decimals'] <= 36
342+
_require(0 < len(sym) <= 10 and sym.isalnum(),
343+
'symbol must be 1-10 alphanumeric characters, got %r'
344+
% arg['symbol'])
345+
_require(0 <= arg['decimals'] <= 36,
346+
'decimals must be 0..36, got %r' % arg['decimals'])
320347
buf.append(arg['decimals'])
321348
buf.append(len(sym))
322349
buf.extend(sym)
@@ -342,12 +369,13 @@ def schema_calldata(selector: bytes, args: list) -> bytes:
342369
fmt = arg['format']
343370
if fmt == ARG_FORMAT_ADDRESS:
344371
addr = arg['address']
345-
assert len(addr) == 20
372+
_require(len(addr) == 20,
373+
'ADDRESS arg must be 20 bytes, got %d' % len(addr))
346374
data.extend(b'\x00' * 12 + addr)
347375
elif fmt in (ARG_FORMAT_AMOUNT, ARG_FORMAT_TOKEN_AMOUNT):
348376
data.extend(int(arg['amount']).to_bytes(32, 'big'))
349377
else:
350-
raise AssertionError('unsupported v2 arg format %r' % fmt)
378+
raise ValueError('unsupported v2 arg format %r' % fmt)
351379
return bytes(data)
352380

353381

0 commit comments

Comments
 (0)