@@ -813,13 +813,15 @@ def parse_junit(path):
813813 ]),
814814
815815 ('S' , 'Solana' , '7.14.0' ,
816- 'NEW: Full Solana with Ed25519 (SLIP-10), base58 addresses, 37 instruction types across 7 '
816+ 'Full Solana with Ed25519 (SLIP-10), base58 addresses, 37 instruction types across 7 '
817817 'programs. Key security fix: full 44-character address display replaces old 8-char truncation '
818- 'that was a spoofing vector.' ,
818+ 'that was a spoofing vector. Off-chain message signing (domain-separated envelope) added '
819+ 'in 7.14.x — the proper fix for the SignMessage AdvancedMode band-aid.' ,
819820 [
820821 'ADDRESS: m/44\' /501\' /0\' Ed25519 -> full 44-char base58 on OLED' ,
821822 'SIGN TX: Parse instructions -> per-instruction confirmation -> Ed25519 sign' ,
822- 'SIGN MESSAGE: Arbitrary bytes -> hex display -> Ed25519 sign' ,
823+ 'SIGN MESSAGE: Arbitrary bytes -> hex display -> Ed25519 sign (AdvancedMode-gated)' ,
824+ 'SIGN OFFCHAIN: "\\ xff" || "solana offchain" || ver || fmt || len || msg -> Ed25519 sign' ,
823825 ],
824826 [
825827 ('S1' , 'test_msg_solana_getaddress' , 'test_solana_get_address' ,
@@ -858,14 +860,41 @@ def parse_junit(path):
858860 'SPL Token with metadata' ,
859861 'Token transfer with SolanaTokenInfo (mint, symbol, decimals). OLED shows human-readable token name.' ,
860862 ['Token name + amount' ]),
863+ ('S13' , 'test_msg_solana_signoffchainmessage' , 'test_sign_ascii_message' ,
864+ 'Off-chain ASCII (fmt 0)' ,
865+ 'Domain-separated envelope; format 0 (printable ASCII) renders + signs.' ,
866+ ['Off-chain message confirm' ]),
867+ ('S14' , 'test_msg_solana_signoffchainmessage' , 'test_sign_at_size_ceiling' ,
868+ 'Off-chain at 1212-byte ceiling' ,
869+ 'Spec ceiling for formats 0/1; firmware accepts at the limit.' , []),
870+ ('S15' , 'test_msg_solana_signoffchainmessage' , 'test_format_2_rejected' ,
871+ 'Reject format 2' ,
872+ 'Extended UTF-8 (Ledger-only blind sign) explicitly rejected on this device.' , []),
873+ ('S16' , 'test_msg_solana_signoffchainmessage' , 'test_invalid_version_rejected' ,
874+ 'Reject non-zero version' ,
875+ 'Spec defines only version 0; firmware rejects others.' , []),
876+ ('S17' , 'test_msg_solana_signoffchainmessage' , 'test_empty_message_rejected' ,
877+ 'Reject empty message' ,
878+ 'Zero-length payload rejected with SyntaxError.' , []),
879+ ('S18' , 'test_msg_solana_signoffchainmessage' , 'test_pubkey_matches_get_address' ,
880+ 'Pubkey binding' ,
881+ 'Returned public_key Base58 == solana_get_address() at same path.' , []),
882+ ('S19' , 'test_msg_solana_signoffchainmessage' , 'test_envelope_signature_verifies_offdevice' ,
883+ 'Envelope (not raw msg) is signed' ,
884+ 'Off-device Ed25519 verify against the envelope succeeds; against the bare message fails. Catches firmware regressions that drop the envelope.' ,
885+ []),
861886 ]),
862887
863888 ('T' , 'TRON' , '7.14.0' ,
864- 'NEW: TRON with secp256k1 signing, base58 addresses. Blind-sign via raw_data. '
865- 'Structured reconstruct-then-sign and TRC-20 clear-signing deferred to 7.15+.' ,
889+ 'TRON with secp256k1 signing, base58 addresses. Blind-sign via raw_data. '
890+ 'TIP-191 personal_sign + VerifyMessage and TIP-712 typed-data hash mode '
891+ 'added in 7.14.x. Structured reconstruct-then-sign and TRC-20 clear-signing '
892+ 'deferred to 7.15+.' ,
866893 [
867894 'ADDRESS: m/44\' /195\' /0\' /0/0 -> full 34-char base58 TRON address' ,
868895 'BLIND-SIGN: Raw protobuf data -> hash + sign' ,
896+ 'TIP-191: keccak256("\\ x19TRON Signed Message:\\ n" + len + msg) -> secp256k1' ,
897+ 'TIP-712: keccak256("\\ x19\\ x01" + domainHash + msgHash) -> secp256k1' ,
869898 ],
870899 [
871900 ('T1' , 'test_msg_tron_getaddress' , 'test_tron_get_address' ,
@@ -880,16 +909,45 @@ def parse_junit(path):
880909 'Sign TRX blind (raw_data)' , 'Raw protobuf data triggers blind sign path. Shows amount + address if provided.' , ['TRON blind sign' ]),
881910 ('T5' , 'test_msg_tron_signtx' , 'test_tron_sign_missing_fields_rejected' ,
882911 'Missing fields rejected' , 'Incomplete transaction data is refused.' , []),
912+ ('T6' , 'test_msg_tron_signmessage' , 'test_sign_text_roundtrip' ,
913+ 'TIP-191 sign + verify text' , 'Sign printable ASCII message; verify the returned signature with the same client.' , ['TRON message confirm' ]),
914+ ('T7' , 'test_msg_tron_signmessage' , 'test_sign_bytes_roundtrip' ,
915+ 'TIP-191 sign + verify bytes' , 'Non-printable byte payload renders as hex preview, round-trips through verify.' , ['TRON bytes confirm' ]),
916+ ('T8' , 'test_msg_tron_signmessage' , 'test_sign_empty_message' ,
917+ 'TIP-191 empty message' , 'Zero-length message is valid per TIP-191 (ASCII "0" length encoding).' , []),
918+ ('T9' , 'test_msg_tron_signmessage' , 'test_sign_address_matches_get_address' ,
919+ 'Address binding' , 'Signature response address equals tron_get_address() at the same path.' , []),
920+ ('T10' , 'test_msg_tron_signmessage' , 'test_verify_rejects_corrupted_signature' ,
921+ 'Reject corrupted signature' , 'Flipping a byte in r/s causes verify to recover a different pubkey -> Failure.' , []),
922+ ('T11' , 'test_msg_tron_signmessage' , 'test_verify_rejects_wrong_message' ,
923+ 'Reject wrong message' , 'Valid signature against a different message -> Failure.' , []),
924+ ('T12' , 'test_msg_tron_signmessage' , 'test_invalid_path_rejected' ,
925+ 'Reject non-TRON path' , 'm/44\' /60\' /... rejected by firmware path guard before signing.' , []),
926+ ('T13' , 'test_msg_tron_signtypedhash' , 'test_sign_typed_hash_with_message' ,
927+ 'TIP-712 sign hash + message' , 'Domain + message hash mode; 65-byte recoverable secp256k1 sig.' , ['TIP-712 confirm dialogs' ]),
928+ ('T14' , 'test_msg_tron_signtypedhash' , 'test_sign_typed_hash_domain_only' ,
929+ 'TIP-712 domain-only' , 'primaryType=EIP712Domain case: no message hash, domain digest only.' , []),
930+ ('T15' , 'test_msg_tron_signtypedhash' , 'test_sign_typed_hash_address_matches_get_address' ,
931+ 'Address binding' , 'Sig response address matches tron_get_address() at same path.' , []),
932+ ('T16' , 'test_msg_tron_signtypedhash' , 'test_invalid_domain_hash_length_rejected' ,
933+ 'Reject bad domain hash' , '31-byte domain_separator_hash rejected; must be exactly 32 bytes.' , []),
934+ ('T17' , 'test_msg_tron_signtypedhash' , 'test_invalid_message_hash_length_rejected' ,
935+ 'Reject bad message hash' , '33-byte message_hash rejected; must be exactly 32 bytes.' , []),
936+ ('T18' , 'test_msg_tron_signtypedhash' , 'test_invalid_path_rejected' ,
937+ 'Reject non-TRON path' , 'm/44\' /60\' /... rejected by path guard before TIP-712 signing.' , []),
883938 ]),
884939
885940 ('N' , 'TON' , '7.14.0' ,
886- 'NEW: TON v4r2 wallet contracts. Ed25519 signing with structured field display. '
887- 'Blind-sign for raw transactions. Memo/comment support. '
888- 'Full clear-sign with cell tree reconstruction deferred to 7.15+.' ,
941+ 'TON v4r2 wallet contracts. Ed25519 signing with structured field display. '
942+ 'Blind-sign for raw transactions. Memo/comment support. Bare Ed25519 SignMessage '
943+ 'added in 7.14.x with AdvancedMode policy gate (no domain separation; TON Connect '
944+ 'ton_proof envelope deferred to a follow-up). Full clear-sign with cell tree '
945+ 'reconstruction deferred to 7.15+.' ,
889946 [
890947 'ADDRESS: m/44\' /607\' /0\' -> full 48-char base64url TON address' ,
891948 'STRUCTURED: Amount + address + memo shown as display context -> sign' ,
892949 'BLIND-SIGN: Raw tx without structured fields -> "BLIND SIGNATURE" warning' ,
950+ 'SIGN-MESSAGE: Raw Ed25519 over message bytes; gated by AdvancedMode policy' ,
893951 ],
894952 [
895953 ('N1' , 'test_msg_ton_getaddress' , 'test_ton_get_address' ,
@@ -908,6 +966,18 @@ def parse_junit(path):
908966 'Sign TON blind' , 'Raw tx without structured fields triggers blind sign.' , ['Blind warning' ]),
909967 ('N7' , 'test_msg_ton_signtx' , 'test_ton_sign_missing_fields_rejected' ,
910968 'Missing fields rejected' , 'Incomplete data refused.' , []),
969+ ('N8' , 'test_msg_ton_signmessage' , 'test_blocked_when_advanced_mode_disabled' ,
970+ 'AdvancedMode gate blocks default' , 'Without AdvancedMode policy, request fails with ActionCancelled.' , []),
971+ ('N9' , 'test_msg_ton_signmessage' , 'test_sign_text_advanced_mode' ,
972+ 'Sign text (AdvancedMode)' , '64-byte Ed25519 sig + 32-byte pubkey returned for printable text.' , ['TON message confirm' ]),
973+ ('N10' , 'test_msg_ton_signmessage' , 'test_sign_bytes_advanced_mode' ,
974+ 'Sign bytes (AdvancedMode)' , 'Non-printable bytes render as hex preview before signing.' , ['TON bytes confirm' ]),
975+ ('N11' , 'test_msg_ton_signmessage' , 'test_empty_message_rejected' ,
976+ 'Reject empty message' , 'Zero-length message rejected with SyntaxError.' , []),
977+ ('N12' , 'test_msg_ton_signmessage' , 'test_invalid_path_rejected' ,
978+ 'Reject non-TON path' , 'Solana / non-TON BIP-44 path rejected by guard.' , []),
979+ ('N13' , 'test_msg_ton_signmessage' , 'test_pubkey_consistency' ,
980+ 'Pubkey stable, sig varies' , 'Same path returns same Ed25519 pubkey across signs; different msgs -> different sigs.' , []),
911981 ]),
912982
913983 ('Z' , 'Zcash Orchard' , '7.14.0' ,
@@ -972,6 +1042,10 @@ def parse_junit(path):
9721042def render (output_path , fw_version , results , screenshot_dir = None ):
9731043 pdf = PDF (); pb = PB (pdf )
9741044 ts = datetime .now ().strftime ('%Y-%m-%d %H:%M' )
1045+ # Optional build label (branch + commit) for per-PR differentiation.
1046+ # CI sets KK_BUILD_LABEL from "${head_ref}@${sha:0:8}" so identical
1047+ # SECTIONS + same firmware version still produce distinct PDFs.
1048+ build_label = os .environ .get ('KK_BUILD_LABEL' , '' ).strip ()
9751049 active = [(l ,t ,mf ,bg ,fl ,tests ) for l ,t ,mf ,bg ,fl ,tests in SECTIONS if ver_ge (fw_version , mf )]
9761050 # Separate specs section (no tests) from test sections
9771051 specs = [s for s in active if not s [5 ]]
@@ -994,6 +1068,8 @@ def render(output_path, fw_version, results, screenshot_dir=None):
9941068 pb .text (11 , f'Firmware { fw_version } | { ts } | { failed } FAILED of { total } tests' , bold = True , color = RED )
9951069 else :
9961070 pb .text (10 , f'Firmware { fw_version } | { ts } | { total } tests: { passed } passed, { skipped } pending' )
1071+ if build_label :
1072+ pb .text (9 , f'Build: { build_label } ' )
9971073 pb .gap (6 )
9981074 pb .text (12 , 'Sections' , bold = True )
9991075 _shown_tested = _shown_pending = False
0 commit comments