Skip to content

Commit 2bb6a05

Browse files
committed
test: bind 7.14.2 release evidence semantics
1 parent 2532ac8 commit 2bb6a05

2 files changed

Lines changed: 220 additions & 29 deletions

File tree

scripts/generate-test-report.py

Lines changed: 190 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ def write(self, path):
104104
img_obj_ids = {} # img_name -> obj_id
105105
for img_path, (name, pixels, iw, ih) in self.images.items():
106106
compressed = zlib.compress(pixels)
107-
obj = f'{nxt} 0 obj\n<< /Type /XObject /Subtype /Image /Width {iw} /Height {ih} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode /Length {len(compressed)} >>\nstream\n'.encode() + compressed + b'\nendstream\nendobj\n'
107+
# The report verifier rasterizes at an integer OLED-pixel scale.
108+
# State the PDF default explicitly so a reader cannot smooth a
109+
# security frame and make the final visible pixels ambiguous.
110+
obj = f'{nxt} 0 obj\n<< /Type /XObject /Subtype /Image /Width {iw} /Height {ih} /ColorSpace /DeviceGray /BitsPerComponent 8 /Interpolate false /Filter /FlateDecode /Length {len(compressed)} >>\nstream\n'.encode() + compressed + b'\nendstream\nendobj\n'
108111
objs.append(obj)
109112
img_obj_ids[name] = nxt
110113
nxt += 1
@@ -363,10 +366,141 @@ def junit_reconciliation(path):
363366
('test_msg_resetdevice', 'test_reset_device_24_words'): 7,
364367
('test_msg_resetdevice', 'test_reset_device_pin'): 6,
365368
('test_msg_resetdevice', 'test_failed_pin'): 2,
369+
('test_msg_recoverydevice_cipher', 'test_nopin_nopassphrase'): 43,
370+
('test_msg_recoverydevice_cipher', 'test_pin_passphrase'): 45,
371+
('test_msg_changepin', 'test_set_pin'): 3,
372+
('test_msg_changepin', 'test_change_pin'): 4,
373+
('test_msg_changepin', 'test_remove_pin'): 2,
374+
('test_msg_ethereum_signtx', 'test_ethereum_blind_sign_blocked'): 1,
366375
('test_msg_ping', 'test_authenticator_passphrase_cancel_is_terminal'): 1,
367376
('test_msg_solana_signtx', 'test_solana_sign_system_transfer'): 3,
368377
}
369378

379+
# Counts and unique whole-frame hashes reject missing pages and stale capture,
380+
# but a same-sized set of unrelated screens could still be relabelled as the
381+
# ceremony. Pin stable semantic regions: static prompts in full, seed-page
382+
# titles without random words, and PIN/recovery labels without randomized
383+
# grids. Values are raw 8-bit framebuffer-region SHA-256 digests, not PNG file
384+
# hashes, so metadata/encoding changes cannot satisfy them.
385+
_RECOVERY_PROGRESS_DIGESTS = (
386+
'356da6d4365dc08dc554d7d14d82c6458cd4812682ebcf16521154582c201c9d',
387+
'bd5c4a4e92abb097d0c5f69176d915568b483a5bc1c98e17ba8c26e35bc6e308',
388+
'3cf4d797c321cf56e63c177f64e6d889358fed8f6dd1cecd658b90621c10df8d',
389+
'ac4b89a3020e9c9bc12dc630f95d96e5ac86d381cfad71843aa88835d82f42de',
390+
'dd30561c70c73b39733b6d71d6cbea89d43a81cf032cbed7e013a4a93694d709',
391+
'b6e134f114a1572da2464780eca206a422f31de5f2699ae8979e89c0e3c70825',
392+
'98712d58cb5f17fda59d73cd37c33c6e81ffe011eeb2fbc9bf530a2b8effc052',
393+
'2e9ac61c8da62ed071b8ba9ede76ca1df45d52684ddc0b8192e57ae485aa2d14',
394+
'ffc90a573947af63fb0700a9b7e4d105ef02cfbda1077f9e0da61fd7d5dbecc0',
395+
'6ea6b5da97c73890c2c304d92a08807b4c1f86cd22f35d8011ac9b16545f1e26',
396+
'fe736b5dc3d03ac1beab86df27aec6a8493e8f53776889fcf64afd4ea9b99f8c',
397+
'508ccfc1207a64f4c13cb08d5068cbd20aff4070213ff39b14beaa3d7e5b291c',
398+
'46cdc4cde98844b548431ce166ff7ce9d044f3d2b3f6cf073bc6f5e6991c64c5',
399+
'15ab192e8fb5ad155072a686cf8141b9c031bac561368b090ef03f48fe20be41',
400+
'd96df49ded78365e8d5670d37b2013eb1b15be148f0de3fa24e01749bcf40799',
401+
'8a0857857a5fc98d395229b6f0551a784e60ec7503450d1ff6703c43e93bedf0',
402+
'1d2f9d8b4d89fc531e22ab8861ca8c4855e1e782b45f9e8a94ef18fccc8abc24',
403+
'fb2f1a2e4bf6a55ba80b3ea93ac7f4a4d920d92947fdda5e19b4f36d8ca97c39',
404+
'8baf086979d10099db857889f5e98870b69b533dbb6ad8223759012bfb1883fd',
405+
'4871fb187d2c5d18fab2ff733e44877c4be255b889e5d92809e5c96a764977c7',
406+
'ab63aefd9107d312e155446f1d3b6885408c72f81100bcbe15018ffa10bd2323',
407+
'eecaca7c1159ab9cd9050efd48c6fa2d041fa968b90a36020639723368ae619b',
408+
'0add6eea0290f825a0a96d55859192fa17f91ba641d0acba3aae0da1d82013da',
409+
'613fafd7d89e66bdc6b3df8929ae8189d0628e8fedad164a68dab0d524973f42',
410+
'1bb1ef1cadc2c771ef6ae2cc439cde1c96d54b2dfbfc5c797ccdaab96cea9217',
411+
'0fb5c836d9399a7842a9bfefcdc4e98e9f0beba73fa6a9ddf9fb6d20a45c557a',
412+
'b1872159d11592ba047fffad4c6d4afeb9f08d63efb1da08db4bf7a7333652fa',
413+
'766f7c35fb85b92a63d53f38d85209cacc2c77896a971ce6404069334c985832',
414+
'0037fe837403c31e8afc3dcdcabe59749d827b2056e976e60911c820633a1138',
415+
'1838b5b983fa4ce45b18ee42d6f6eb265a04242bb4d49a731e44c06ee05c0e6f',
416+
'06f177d53c810c8ebaf39183960dba6d516bc4d614e31e3d54d2806d87a53373',
417+
'ac8c1ae7c3106d13467784442149a923b538d2496de6112859a6a9aa35aba9e0',
418+
'ef8f79330bfc7c1433cef9f4bbaa72553187405b6dc953a0289bc23588a89ba5',
419+
'd9beba0b50498295a17d0574af5319340a49c9b0eb584fc6a692bdb8de61120c',
420+
'cdf9f82d26cc510be4245de86eea8d15d475fc7188cad2313985817393c5e98f',
421+
'698e4369c80ededc84089de96e3ce3f75d6e5a0fe0f948c170a72b52596cff18',
422+
'c24ab48b79202080b794e6bbe08fd1addeb3d464d350f8ab870d16d65cbfd892',
423+
'0f132132d36b77080285577210c9584041e96fdadb43ff1623c0a76321bc1ac8',
424+
'cac9864396459cd742266d587c3a4668c83e30cb5c4586c80c8d4467f12b7c29',
425+
'73e185aed618ea31559e32571350d5994ed1a8548ed242c0aea04b3e928a20b6',
426+
'ce7b85a3ab8faa6e04cbaecdc8c26a7afef950c371bf39b5480de3e7b21ab715',
427+
'c511560f884aa174d6980fbc2494daa5e21cf3a2b3c9fefd58a3d52fa2b91990',
428+
)
429+
SECURITY_STAGE_POLICIES = {
430+
('test_msg_resetdevice', 'test_reset_device'): (
431+
((0, 0, 256, 64), 'be76f41dcbb2699274234cc687ae2a136cc23ee57e2b16ebc94a5c95c07d62d5'),
432+
((0, 0, 256, 16), '9ae3b8815317cf35882cbbdd8129bc6fa79c029759edce8cf64103f291809d05'),
433+
((0, 0, 256, 16), '9c70c1af5f9a4da9b6aa69c3beefc5e3c70d82af51212350e757bfd9cabf36de'),
434+
((0, 0, 256, 16), '88e1755feef35658a506016ea50ed1ba5a651d53e1a98dd97f07282ca22c77ef'),
435+
),
436+
('test_msg_resetdevice', 'test_reset_device_18_words'): (
437+
((0, 0, 256, 64), 'be76f41dcbb2699274234cc687ae2a136cc23ee57e2b16ebc94a5c95c07d62d5'),
438+
((0, 0, 256, 16), '5e967b430c4f07a0f8f8dc7594eaa66be7237f3baf41a90f7a5edd9e88912474'),
439+
((0, 0, 256, 16), 'e58ece0276ba86987987ab6fdfd0a12e4ed7dd8478dfe9ed7249a516c8063d69'),
440+
((0, 0, 256, 16), '1669d638b7256699b9ed57615619e365fb4ab67a7c8a6f2ed8b23d758da43027'),
441+
((0, 0, 256, 16), 'b259422a2db01225278f22bf43e7a60969aa202a260b11d2c91b44f5764cbac8'),
442+
((0, 0, 256, 16), 'd4e6ed9a13a1bf88a662d7568094d5a66d2bdbd9ac39f3a576f59108b0578515'),
443+
),
444+
('test_msg_resetdevice', 'test_reset_device_24_words'): (
445+
((0, 0, 256, 64), 'be76f41dcbb2699274234cc687ae2a136cc23ee57e2b16ebc94a5c95c07d62d5'),
446+
((0, 0, 256, 16), '2fb7530c04acb560dbe1a29179bb084bd7003846a9204b26152c363f2fa076ac'),
447+
((0, 0, 256, 16), '46c79190446218f6b33ac2e1cf0407c37fc680db911dff5ad61c27459157dd9b'),
448+
((0, 0, 256, 16), '5e2f25cd0c1e4b6d41fe42316b6f696f747636b7446f33f0b853f322f397e628'),
449+
((0, 0, 256, 16), '934e043c7691ea2bd1e3126da5add2e907f1b9e4d9ae183bba34b0227c69e724'),
450+
((0, 0, 256, 16), '68b48e9ca14c309b3953afe4f0f0c0f289a4a0fa7920d4c5188562754c294d2e'),
451+
((0, 0, 256, 16), 'd0f315000890dce0317553c695a2bd72e8bbb67a28d3640812c7a93945bd2882'),
452+
),
453+
('test_msg_resetdevice', 'test_reset_device_pin'): (
454+
((0, 0, 72, 64), '41cf35c908d63d6f350edbcf0d70f504dd35142906c85402f108c373c37264bc'),
455+
((0, 0, 72, 64), '42775a67b27b34e34bea67aa17a8942eb2bc28215cd5dd8d31c3562f556393db'),
456+
((0, 0, 256, 64), 'be76f41dcbb2699274234cc687ae2a136cc23ee57e2b16ebc94a5c95c07d62d5'),
457+
((0, 0, 256, 16), '9ae3b8815317cf35882cbbdd8129bc6fa79c029759edce8cf64103f291809d05'),
458+
((0, 0, 256, 16), '9c70c1af5f9a4da9b6aa69c3beefc5e3c70d82af51212350e757bfd9cabf36de'),
459+
((0, 0, 256, 16), '88e1755feef35658a506016ea50ed1ba5a651d53e1a98dd97f07282ca22c77ef'),
460+
),
461+
('test_msg_resetdevice', 'test_failed_pin'): (
462+
((0, 0, 72, 64), '41cf35c908d63d6f350edbcf0d70f504dd35142906c85402f108c373c37264bc'),
463+
((0, 0, 72, 64), '42775a67b27b34e34bea67aa17a8942eb2bc28215cd5dd8d31c3562f556393db'),
464+
),
465+
('test_msg_recoverydevice_cipher', 'test_nopin_nopassphrase'):
466+
(((0, 0, 256, 64), '2130e48943d0894d27d00ecc015e9cb15ea8be96044d823ef9b8b6e64897a0d8'),) +
467+
tuple(((0, 32, 72, 64), digest)
468+
for digest in _RECOVERY_PROGRESS_DIGESTS),
469+
('test_msg_recoverydevice_cipher', 'test_pin_passphrase'):
470+
(((0, 0, 72, 64), '41cf35c908d63d6f350edbcf0d70f504dd35142906c85402f108c373c37264bc'),
471+
((0, 0, 72, 64), '42775a67b27b34e34bea67aa17a8942eb2bc28215cd5dd8d31c3562f556393db'),
472+
((0, 0, 256, 64), '2130e48943d0894d27d00ecc015e9cb15ea8be96044d823ef9b8b6e64897a0d8')) +
473+
tuple(((0, 32, 72, 64), digest)
474+
for digest in _RECOVERY_PROGRESS_DIGESTS),
475+
('test_msg_changepin', 'test_set_pin'): (
476+
((0, 0, 256, 64), 'a1181d01ad8cdf156f4d082ad1a976b2d83b863da99ce75fd007053775fb89b9'),
477+
((0, 0, 72, 64), '41cf35c908d63d6f350edbcf0d70f504dd35142906c85402f108c373c37264bc'),
478+
((0, 0, 72, 64), '42775a67b27b34e34bea67aa17a8942eb2bc28215cd5dd8d31c3562f556393db'),
479+
),
480+
('test_msg_changepin', 'test_change_pin'): (
481+
((0, 0, 256, 64), '8358abe3b90dff94b248cf5577758538d9ed6abd8f299c53cb6b452f27beb8f2'),
482+
((0, 0, 72, 64), 'e5addc208c68cf1655e2aa00789159a3039b6a27e18df9891c88fcfe1af71219'),
483+
((0, 0, 72, 64), '41cf35c908d63d6f350edbcf0d70f504dd35142906c85402f108c373c37264bc'),
484+
((0, 0, 72, 64), '42775a67b27b34e34bea67aa17a8942eb2bc28215cd5dd8d31c3562f556393db'),
485+
),
486+
('test_msg_changepin', 'test_remove_pin'): (
487+
((0, 0, 256, 64), '4e0e3515ea141409fe932778a59fe2c4007d8074aa4469e5fba5f4a8527d0c98'),
488+
((0, 0, 72, 64), 'e5addc208c68cf1655e2aa00789159a3039b6a27e18df9891c88fcfe1af71219'),
489+
),
490+
('test_msg_ethereum_signtx', 'test_ethereum_blind_sign_blocked'): (
491+
((0, 0, 80, 16),
492+
'f0d262606b7358aee6b8c9d9dfac9035f89bd8a8b26b2551c11561731b47880d'),
493+
),
494+
('test_msg_ping', 'test_authenticator_passphrase_cancel_is_terminal'): (
495+
((0, 0, 72, 64), 'e5addc208c68cf1655e2aa00789159a3039b6a27e18df9891c88fcfe1af71219'),
496+
),
497+
('test_msg_solana_signtx', 'test_solana_sign_system_transfer'): (
498+
((0, 0, 128, 32), '7c497cdbcbab7e37c4bc0ad6a0276b18cfe4b8744689cad0d6afa37b09b7b37c'),
499+
((0, 0, 80, 32), '6229b9bc36eed4e5dece1deafdcb44e3b6449f7c1329fef3797bea609dad4b8f'),
500+
((0, 0, 256, 64), '0f1acb8bc327fed89037093f9b489a8ce598c8f58d26601ae0d618c03403e823'),
501+
),
502+
}
503+
370504
SECURITY_DISTINCT_FRAME_TESTS = set(SECURITY_FRAME_POLICIES) | {
371505
('test_msg_solana_instruction_disclosure',
372506
'test_all_verified_instruction_fields_change_oled_review'),
@@ -468,8 +602,8 @@ def junit_reconciliation(path):
468602
'- an unresolved token rendered as the literal "Unknown token value" and signed',
469603
'- a truncated memo dropped its last character (Confirm limit 42 vs 420)',
470604
'',
471-
'A test here with an EMPTY screenshot list is deliberate: refusal paths draw nothing,',
472-
'and their evidence is the Failure on the wire plus the absence of a ButtonRequest.',
605+
'An EMPTY screenshot list is valid only for a refusal that asserts no ButtonRequest.',
606+
'A refusal that emits a policy notice must retain that frame and its exact wire order.',
473607
],
474608
[
475609
('DB1', 'test_msg_ethereum_erc20_0x_signtx', 'test__sign_transformERC20',
@@ -582,16 +716,17 @@ def junit_reconciliation(path):
582716
('X', 'Device Specifications', '0.0.0',
583717
'The KeepKey is an open-source hardware wallet built on an ARM Cortex-M3 (STM32F205, 120MHz) '
584718
'with a 256x64 monochrome OLED, single confirmation button, and micro-USB interface. The '
585-
'bootloader (v2.x) is flashed at manufacture and never updated - it is the immutable root of '
586-
'trust. On every boot, the bootloader verifies the firmware signature using redundant F3 checks '
587-
'before transferring control.',
719+
'bootloader (v2.x) verifies the firmware signature using redundant F3 checks before transferring '
720+
'control. Bootloader updates are supported through a separately built updater and therefore '
721+
'remain a security-sensitive release path; the bootloader must not be described as immutable.',
588722
[
589723
'BOOT SEQUENCE:',
590724
'1. USB connect -> bootloader executes (always first)',
591725
'2. F3 signature check (redundant dual-path verify)',
592726
'3. Valid -> KeepKey logo -> firmware runs',
593727
'4. Invalid/missing -> "UPDATE FIRMWARE" screen',
594728
'5. Firmware upload -> verify -> flash -> reboot -> re-verify',
729+
'6. Bootloader updates use the separately audited blupdater artifact',
595730
'',
596731
'HARDWARE:',
597732
'- MCU: STM32F205RET6, 120MHz, 128KB bootloader + 896KB firmware',
@@ -934,9 +1069,12 @@ def junit_reconciliation(path):
9341069
['ETH send confirmation']),
9351070
('E3', 'test_msg_ethereum_signtx', 'test_ethereum_signtx_data',
9361071
'Sign ETH tx with contract data',
937-
'Transaction with data field (contract call). Device shows data as hex since it cannot '
938-
'decode arbitrary ABI without metadata.',
939-
['Contract data hex']),
1072+
'Transaction with a data field (contract call). Without trusted metadata the device does '
1073+
'not show raw calldata as decoded semantics: AdvancedMode is required and the OLED shows '
1074+
'the complete-calldata Keccak-256 commitment with the transaction fields. This test signs '
1075+
'three contract-call variants in sequence; repeated amount/hash/fee frames are intentional '
1076+
'per-variant approvals, not one duplicated approval flow.',
1077+
['Contract data hash and transaction fields for three variants']),
9401078
('E4', 'test_msg_ethereum_signtx', 'test_ethereum_signtx_nodata_eip155',
9411079
'Sign ETH with EIP-155 replay protection',
9421080
'Chain ID embedded in signature v value to prevent cross-chain replay attacks.', []),
@@ -1150,9 +1288,10 @@ def junit_reconciliation(path):
11501288
['Opaque policy warning', 'Complete calldata hash approval']),
11511289
('V9', 'test_msg_ethereum_signtx', 'test_ethereum_blind_sign_blocked',
11521290
'Opaque contract data blocked (AdvancedMode OFF)',
1153-
'On 7.14.2 the same arbitrary calldata is refused by policy before any signing approval. '
1154-
'The wire Failure and absence of a signing ButtonRequest are the evidence.',
1155-
[]),
1291+
'On 7.14.2 the same arbitrary calldata produces one non-approving Blocked notice, then '
1292+
'Failure_ActionCancelled. The exact two-message sequence proves there is no signing '
1293+
'approval after the policy notice.',
1294+
['Blocked policy notice']),
11561295
]),
11571296

11581297
('SOL', 'Solana', '7.14.0',
@@ -1780,6 +1919,23 @@ def _sequence_record(screenshot_root, directory, label, frames):
17801919
}
17811920

17821921

1922+
def _validate_security_stage_digests(mod, meth, stage_policy,
1923+
actual_digests):
1924+
"""Bind each ceremony position to its expected visible semantic stage."""
1925+
if len(stage_policy) != len(actual_digests):
1926+
raise ValueError(
1927+
'security ceremony semantic-stage count mismatch for %s::%s' %
1928+
(mod, meth))
1929+
for index, (actual, stage) in enumerate(zip(
1930+
actual_digests, stage_policy)):
1931+
_region, expected = stage
1932+
allowed = (expected,) if isinstance(expected, str) else expected
1933+
if actual not in allowed:
1934+
raise ValueError(
1935+
'security ceremony frame %d has the wrong semantic stage '
1936+
'for %s::%s' % (index, mod, meth))
1937+
1938+
17831939
def _enforce_security_frame_policy(directory, mod, meth, frames):
17841940
expected_count = SECURITY_FRAME_POLICIES.get((mod, meth))
17851941
if expected_count is not None and len(frames) != expected_count:
@@ -1797,6 +1953,28 @@ def _enforce_security_frame_policy(directory, mod, meth, frames):
17971953
raise ValueError(
17981954
'security ceremony contains a duplicate framebuffer for %s::%s' %
17991955
(mod, meth))
1956+
stage_policy = SECURITY_STAGE_POLICIES.get((mod, meth))
1957+
if stage_policy is None:
1958+
return
1959+
actual_digests = []
1960+
for index, (frame, stage) in enumerate(zip(frames, stage_policy)):
1961+
region, expected = stage
1962+
x0, y0, x1, y1 = region
1963+
try:
1964+
pixels, width, height = _read_png_pixels(
1965+
os.path.join(directory, frame))
1966+
except Exception as exc:
1967+
raise ValueError(
1968+
'security ceremony frame %d is not a valid framebuffer: %s' %
1969+
(index, exc))
1970+
if not (0 <= x0 < x1 <= width and 0 <= y0 < y1 <= height):
1971+
raise ValueError('invalid security semantic region')
1972+
cropped = b''.join(
1973+
pixels[y * width + x0:y * width + x1]
1974+
for y in range(y0, y1))
1975+
actual_digests.append(hashlib.sha256(cropped).hexdigest())
1976+
_validate_security_stage_digests(
1977+
mod, meth, stage_policy, actual_digests)
18001978

18011979

18021980
def selected_screenshot_sequences(screenshot_root, mod, meth, wants_screens):

tests/test_msg_ethereum_signtx.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,29 +101,42 @@ def test_ethereum_signtx_data(self):
101101
def test_ethereum_blind_sign_blocked(self):
102102
"""AdvancedMode OFF + contract data = device refuses to sign (7.14.2+).
103103
104-
OLED explains that arbitrary contract data requires AdvancedMode, then
105-
the device returns the policy failure without requesting approval.
104+
The device displays one non-approving Blocked notice, then returns the
105+
policy failure. The exact ButtonRequest -> Failure sequence proves the
106+
host cannot advance from that notice into a signing approval.
106107
"""
107108
self.requires_firmware("7.14.2")
108109
self.requires_fullFeature()
109110
self.setup_mnemonic_nopin_nopassphrase()
110111
self.client.apply_policy("AdvancedMode", 0)
111112

112-
try:
113-
self.client.ethereum_sign_tx(
114-
n=[0, 0],
115-
nonce=0,
116-
gas_price=20,
117-
gas_limit=20,
118-
to=binascii.unhexlify("1d1c328764a41bda0492b66baa30c4a339ff85ef"),
119-
value=0,
120-
data=b"abcdefghijklmnop" * 16,
121-
chain_id=1,
122-
)
123-
self.fail("Expected Failure -- blind signing should be blocked")
124-
except CallException as e:
125-
self.assertIn("Arbitrary contract data signing disabled by policy",
126-
str(e))
113+
with self.client:
114+
self.client.set_expected_responses([
115+
proto.ButtonRequest(
116+
code=proto_types.ButtonRequest_Other),
117+
proto.Failure(
118+
code=proto_types.Failure_ActionCancelled,
119+
message=(
120+
"Arbitrary contract data signing disabled by policy")),
121+
])
122+
try:
123+
self.client.ethereum_sign_tx(
124+
n=[0, 0],
125+
nonce=0,
126+
gas_price=20,
127+
gas_limit=20,
128+
to=binascii.unhexlify(
129+
"1d1c328764a41bda0492b66baa30c4a339ff85ef"),
130+
value=0,
131+
data=b"abcdefghijklmnop" * 16,
132+
chain_id=1,
133+
)
134+
self.fail(
135+
"Expected Failure -- blind signing should be blocked")
136+
except CallException as e:
137+
self.assertIn(
138+
"Arbitrary contract data signing disabled by policy",
139+
str(e))
127140

128141
def test_ethereum_blind_sign_allowed(self):
129142
"""AdvancedMode ON permits opaque contract-data signing (7.14.2+).

0 commit comments

Comments
 (0)