From 1b779cb086a15baf9cd0c86794f735f8d8f016f8 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:03:53 +0200 Subject: [PATCH 1/2] adding introspection scripts for signed or initial binaries --- tools/introspect-binary.py | 312 +++++++++++++++++++++++++++++++++++++ tools/parse_pubkeys.py | 148 ++++++++++++++++++ 2 files changed, 460 insertions(+) create mode 100644 tools/introspect-binary.py create mode 100644 tools/parse_pubkeys.py diff --git a/tools/introspect-binary.py b/tools/introspect-binary.py new file mode 100644 index 0000000..ee19db1 --- /dev/null +++ b/tools/introspect-binary.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Binary introspection tool for signature validation""" + +import click +import sys +import os +from core.blsection import * +from core.signature import verify, pubkey_fingerprint +from core.integritychk import * +from parse_pubkeys import get_pubkey_info + +# Import functions from upgrade-generator +import importlib.util +spec = importlib.util.spec_from_file_location("upgrade_generator", + os.path.join(os.path.dirname(__file__), "upgrade-generator.py")) +upgrade_gen = importlib.util.module_from_spec(spec) +spec.loader.exec_module(upgrade_gen) + +# Import the needed functions +load_sections = upgrade_gen.load_sections +parse_sections = upgrade_gen.parse_sections +make_signature_message = upgrade_gen.make_signature_message + +__author__ = "Mike Tolkachev " +__copyright__ = "Copyright 2020 Crypto Advance GmbH. All rights reserved" +__version__ = "1.0.0" + +# Default signature thresholds +DEFAULT_BOOTLOADER_THRESHOLD = 2 +DEFAULT_MAIN_FW_THRESHOLD = 1 + +@click.command() +@click.option( + '--pubkeys', 'pubkeys_file', + type=click.Path(exists=True), + help='Path to pubkeys.c file (default: ../keys/production/pubkeys.c)' +) +@click.option( + '--boot-threshold', 'boot_threshold', + type=int, + default=2, + help='Required signatures for bootloader updates (default: 2)' +) +@click.option( + '--main-threshold', 'main_threshold', + type=int, + default=1, + help='Required signatures for main firmware updates (default: 1)' +) +@click.option( + '--type', 'file_type', + type=click.Choice(['upgrade', 'initial', 'auto']), + default='auto', + help='Binary file type (default: auto-detect)' +) +@click.option( + '--debug', 'debug_mode', + is_flag=True, + help='Enable debug output with detailed information' +) +@click.argument( + 'binary_file', + required=True, + type=click.File('rb'), + metavar='' +) +def introspect(binary_file, pubkeys_file, boot_threshold, main_threshold, file_type, debug_mode): + """Introspects a binary file for signature validation.""" + + # Default pubkeys file location + if not pubkeys_file: + script_dir = os.path.dirname(os.path.abspath(__file__)) + pubkeys_file = os.path.join(script_dir, '../keys/production/pubkeys.c') + + # Load public keys + try: + pubkey_info = get_pubkey_info(pubkeys_file) + print(f"šŸ“‹ Loaded keys from: {pubkeys_file}") + print(f" Vendor keys: {len(pubkey_info['vendor'])}") + print(f" Maintainer keys: {len(pubkey_info['maintainer'])}") + + if debug_mode: + print(f"\nšŸ” Debug - Key details:") + for key_type in ['vendor', 'maintainer']: + print(f" {key_type.title()} keys:") + for owner, fp_hex, pubkey in pubkey_info[key_type]: + print(f" {owner}: {fp_hex}") + print(f" Key length: {len(pubkey)} bytes") + print(f" Key starts: {pubkey[:8].hex()}") + print(f" Key ends: {pubkey[-8:].hex()}") + except Exception as e: + print(f"āŒ Failed to load public keys: {e}") + sys.exit(1) + + # Handle file type detection/processing + if file_type == 'upgrade': + sections = load_sections(binary_file) + analyze_upgrade_file(sections, pubkey_info, boot_threshold, main_threshold, debug_mode) + elif file_type == 'initial': + binary_data = binary_file.read() + analyze_initial_firmware(binary_data, pubkey_info, debug_mode) + else: # auto-detect + try: + sections = load_sections(binary_file) + analyze_upgrade_file(sections, pubkey_info, boot_threshold, main_threshold, debug_mode) + except Exception as e: + if debug_mode: + print(f"šŸ” Debug - Failed to parse as upgrade file: {e}") + print("šŸ”„ Trying as initial firmware binary...") + binary_file.seek(0) + try: + binary_data = binary_file.read() + analyze_initial_firmware(binary_data, pubkey_info, debug_mode) + except Exception as e2: + print(f"āŒ Failed to parse as initial firmware: {e2}") + if debug_mode: + print(f"šŸ” Debug - Original upgrade file error: {e}") + sys.exit(1) + +def analyze_upgrade_file(sections, pubkey_info, boot_threshold, main_threshold, debug_mode): + """Analyze upgrade file sections for signatures""" + + payload_sections, sig_section = parse_sections(sections) + + if not sig_section: + print("āŒ No signature section found") + sys.exit(1) + + print(f"\nšŸ“¦ Upgrade file analysis:") + print(f" Payload sections: {len(payload_sections)}") + + # Determine if this is bootloader or main firmware + is_bootloader = any(s.name == 'boot' for s in payload_sections) + threshold = boot_threshold if is_bootloader else main_threshold + section_type = "Bootloader" if is_bootloader else "Main Firmware" + + print(f" Type: {section_type}") + print(f" Required signatures: {threshold}") + + # Get signature message + sig_message = make_signature_message(payload_sections) + print(f" Message hash: {sig_message.decode('ascii')}") + + if debug_mode: + print(f"\nšŸ” Debug - Payload sections:") + for section in payload_sections: + print(f" Section '{section.name}': {len(section.data)} bytes") + print(f" First 16 bytes: {section.data[:16].hex()}") + + # Analyze signatures + signatures = sig_section.signatures + print(f"\nšŸ” Signature analysis:") + print(f" Found {len(signatures)} signature(s)") + + if debug_mode: + print(f"\nšŸ” Debug - Raw signature data:") + for i, (fingerprint, signature) in enumerate(signatures.items()): + print(f" Signature {i+1}:") + print(f" Fingerprint: {fingerprint.hex()}") + print(f" Signature length: {len(signature)} bytes") + print(f" Signature hex: {signature.hex()}") + + valid_sigs = 0 + used_keys = [] + + # Create fingerprint lookup with owner names + fingerprint_to_key = {} + for key_type in ['vendor', 'maintainer']: + for owner, fp_hex, pubkey in pubkey_info[key_type]: + fingerprint_to_key[bytes.fromhex(fp_hex)] = (key_type, owner, pubkey) + + print(f"\nšŸ” Signature verification:") + for fingerprint, signature in signatures.items(): + fp_hex = fingerprint.hex() + + if fingerprint in fingerprint_to_key: + key_type, owner, pubkey = fingerprint_to_key[fingerprint] + + # Verify signature + try: + is_valid = verify(signature, sig_message, pubkey) + status = "āœ…" if is_valid else "āŒ" + + if is_valid: + valid_sigs += 1 + used_keys.append((key_type, owner)) + + print(f" {status} {key_type} ({owner}): {fp_hex}") + + if debug_mode and is_valid: + print(f" Signature verified successfully") + elif debug_mode: + print(f" Signature verification failed") + + except Exception as e: + print(f" āŒ {key_type} ({owner}): {fp_hex} (verification failed: {e})") + if debug_mode: + print(f" Error details: {e}") + else: + print(f" ā“ Unknown: {fp_hex} (key not in pubkeys.c)") + + # Check threshold + threshold_met = valid_sigs >= threshold + status = "āœ…" if threshold_met else "āŒ" + print(f"\n{status} Threshold verification:") + print(f" Valid signatures: {valid_sigs}/{threshold}") + + if used_keys: + key_list = [f"{owner}({t})" for t, owner in used_keys] + print(f" Signed by: {', '.join(key_list)}") + + if threshold_met: + print(f" Result: Upgrade file is valid and can be installed") + else: + print(f" Result: Upgrade file is invalid (insufficient signatures)") + sys.exit(1) + +def analyze_initial_firmware(binary_data, pubkey_info, debug_mode): + """Analyze initial firmware binary""" + + print(f"\nšŸ“¦ Initial firmware analysis:") + print(f" Binary size: {len(binary_data)} bytes") + + # Look for ICR (Integrity Check Record) at the end + if len(binary_data) < 32: + print("āŒ Binary too small to contain ICR") + return + + # Check for INTG magic (from integritychk.py) + intg_magic = int.from_bytes(binary_data[-32:-28], 'little') + if intg_magic == 0x47544E49: # "INTG" in little endian + print("āœ… Found ICR (Integrity Check Record)") + + # Parse ICR structure (simplified) + struct_rev = int.from_bytes(binary_data[-28:-24], 'little') + print(f" ICR structure revision: {struct_rev}") + + if debug_mode: + pl_ver = int.from_bytes(binary_data[-24:-20], 'little') + pl_size = int.from_bytes(binary_data[-20:-16], 'little') + pl_crc = int.from_bytes(binary_data[-16:-12], 'little') + + print(f"\nšŸ” Debug - ICR details:") + print(f" Payload version: {pl_ver}") + print(f" Payload size: {pl_size} bytes") + print(f" Payload CRC32: 0x{pl_crc:08x}") + + # Verify CRC if possible + if pl_size <= len(binary_data) - 32: + import zlib + actual_crc = zlib.crc32(binary_data[:pl_size]) & 0xffffffff + crc_valid = actual_crc == pl_crc + status = "āœ…" if crc_valid else "āŒ" + print(f" CRC verification: {status} (calculated: 0x{actual_crc:08x})") + + # Search for embedded public keys + print(f"\nļæ½ Public key analysis:") + print(f" Searching for embedded keys...") + + keys_found = {} # Use dict to avoid duplicates by fingerprint + + for key_type in ['vendor', 'maintainer']: + for owner, fp_hex, pubkey in pubkey_info[key_type]: + # Search for the full public key (65 bytes) + pos = binary_data.find(pubkey) + if pos >= 0: + if fp_hex not in keys_found: + keys_found[fp_hex] = { + 'owner': owner, + 'position': pos, + 'types': set() + } + keys_found[fp_hex]['types'].add(key_type) + + if keys_found: + print(f" Found {len(keys_found)} embedded public keys:") + for fp_hex, info in keys_found.items(): + types_str = '/'.join(sorted(info['types'])) + print(f" āœ… {info['owner']} ({types_str}): {fp_hex}") + if debug_mode: + print(f" Location: 0x{info['position']:08x}") + + # Show context around the key + pos = info['position'] + context_start = max(0, pos - 32) + context_end = min(len(binary_data), pos + 65 + 32) + context = binary_data[context_start:context_end] + + print(f" Context around key:") + for i in range(0, min(len(context), 128), 16): # Show first 8 lines + chunk = context[i:i+16] + hex_str = ' '.join(f'{b:02x}' for b in chunk) + ascii_str = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in chunk) + offset = context_start + i + marker = " <-- KEY START" if context_start + i == pos else "" + print(f" {offset:08x}: {hex_str:<48} {ascii_str}{marker}") + + print(f"\nāœ… Key verification:") + print(f" Result: Initial firmware contains the public keys needed for upgrade verification") + + else: + print("āŒ No known public keys found in initial firmware") + print(" Result: This firmware may not support signed upgrades") + + else: + print("āŒ No valid ICR found") + print(" Result: This may not be a valid initial firmware binary") + +if __name__ == '__main__': + introspect() diff --git a/tools/parse_pubkeys.py b/tools/parse_pubkeys.py new file mode 100644 index 0000000..d05e126 --- /dev/null +++ b/tools/parse_pubkeys.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Parse public keys from bootloader/pubkeys.c file""" + +import re +import hashlib +from typing import List, Dict, Tuple + +def parse_pubkeys_c(file_path: str) -> Dict[str, List[Tuple[str, bytes]]]: + """Parse public keys from pubkeys.c file + + Returns: + Dict with 'vendor' and 'maintainer' keys, each containing list of (owner, key_bytes) tuples + """ + + with open(file_path, 'r') as f: + content = f.read() + + # Find vendor keys + vendor_match = re.search( + r'static const bl_pubkey_t vendor_pubkey_list\[\]\s*=\s*\{(.*?)\};', + content, + re.DOTALL + ) + + # Find maintainer keys + maintainer_match = re.search( + r'static const bl_pubkey_t maintainer_pubkey_list\[\]\s*=\s*\{(.*?)\};', + content, + re.DOTALL + ) + + result = { + 'vendor': [], + 'maintainer': [] + } + + if vendor_match: + result['vendor'] = parse_key_array(vendor_match.group(1)) + + if maintainer_match: + result['maintainer'] = parse_key_array(maintainer_match.group(1)) + + return result + +def parse_key_array(array_content: str) -> List[Tuple[str, bytes]]: + """Parse array of bl_pubkey_t structures with owner comments + + Returns: + List of (owner_name, key_bytes) tuples + """ + + keys = [] + + # Split content into lines for easier parsing + lines = array_content.split('\n') + current_owner = "Unknown" + + for i, line in enumerate(lines): + # Look for comment lines that indicate owner + comment_match = re.search(r'//\s*(.+)', line.strip()) + if comment_match: + current_owner = extract_owner_from_comment(line) + + # Look for key structure start + if '{.bytes = {' in line: + # Collect all hex bytes for this key across multiple lines + hex_values = [] + j = i + while j < len(lines) and '}}' not in lines[j]: + hex_matches = re.findall(r'0x([0-9A-Fa-f]{2})U?', lines[j]) + hex_values.extend(hex_matches) + j += 1 + + # Get the closing line too + if j < len(lines): + hex_matches = re.findall(r'0x([0-9A-Fa-f]{2})U?', lines[j]) + hex_values.extend(hex_matches) + + if len(hex_values) == 65: # Uncompressed public key + key_bytes = bytes([int(h, 16) for h in hex_values]) + keys.append((current_owner, key_bytes)) + current_owner = "Unknown" # Reset for next key + + return keys + +def extract_owner_from_comment(comment_line): + """Extract owner name from comment line""" + # Remove // and strip whitespace + comment = comment_line.strip().replace('//', '').strip() + + # Replace whitespace with underscores for consistency + comment = comment.replace(' ', '_') + + # Skip generic comments (but not "Backup m/99h" style comments) + if any(skip in comment.lower() for skip in ['corresponding', 'the_following', 'bip39']): + return 'Unknown' + + return comment + +def pubkey_to_fingerprint(pubkey_bytes: bytes) -> str: + """Convert public key to fingerprint (hash160)""" + + if len(pubkey_bytes) != 65 or pubkey_bytes[0] != 0x04: + raise ValueError("Invalid uncompressed public key format") + + # Use SHA256 first 16 bytes like core.signature does + sha256_hash = hashlib.sha256(pubkey_bytes).digest() + return sha256_hash[:16] # Return first 16 bytes, not RIPEMD160 + +def get_pubkey_info(file_path: str) -> Dict[str, List[Tuple[str, str, bytes]]]: + """Get public key info with fingerprints and owners + + Returns: + Dict with 'vendor' and 'maintainer' keys, each containing list of (owner, fingerprint_hex, pubkey_bytes) + """ + + keys = parse_pubkeys_c(file_path) + + result = { + 'vendor': [], + 'maintainer': [] + } + + for key_type in ['vendor', 'maintainer']: + for owner, pubkey in keys[key_type]: + fingerprint = pubkey_to_fingerprint(pubkey) + result[key_type].append((owner, fingerprint.hex(), pubkey)) + + return result + +if __name__ == '__main__': + import sys + + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + pubkey_info = get_pubkey_info(sys.argv[1]) + + print("Vendor Keys:") + for i, (owner, fingerprint, pubkey) in enumerate(pubkey_info['vendor']): + print(f" {i+1}. {owner}: {fingerprint} ({len(pubkey)} bytes)") + + print("\nMaintainer Keys:") + for i, (owner, fingerprint, pubkey) in enumerate(pubkey_info['maintainer']): + print(f" {i+1}. {owner}: {fingerprint} ({len(pubkey)} bytes)") From ab3e80c11ef5ecea084c1341864f3fe2ad622122 Mon Sep 17 00:00:00 2001 From: k9ert <117085+k9ert@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:31:02 +0200 Subject: [PATCH 2/2] chore: updating cryptography to 3.4.8 --- tools/requirements.txt | 44 ++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/tools/requirements.txt b/tools/requirements.txt index 6aca50f..75b76dc 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: # # pip-compile --allow-unsafe --generate-hashes requirements.in # @@ -89,21 +89,26 @@ coverage==5.1 \ # via # -r requirements.in # pytest-cov -cryptography==3.3.2 \ - --hash=sha256:0d7b69674b738068fa6ffade5c962ecd14969690585aaca0a1b1fc9058938a72 \ - --hash=sha256:1bd0ccb0a1ed775cd7e2144fe46df9dc03eefd722bbcf587b3e0616ea4a81eff \ - --hash=sha256:3c284fc1e504e88e51c428db9c9274f2da9f73fdf5d7e13a36b8ecb039af6e6c \ - --hash=sha256:49570438e60f19243e7e0d504527dd5fe9b4b967b5a1ff21cc12b57602dd85d3 \ - --hash=sha256:541dd758ad49b45920dda3b5b48c968f8b2533d8981bcdb43002798d8f7a89ed \ - --hash=sha256:5a60d3780149e13b7a6ff7ad6526b38846354d11a15e21068e57073e29e19bed \ - --hash=sha256:7951a966613c4211b6612b0352f5bf29989955ee592c4a885d8c7d0f830d0433 \ - --hash=sha256:922f9602d67c15ade470c11d616f2b2364950602e370c76f0c94c94ae672742e \ - --hash=sha256:a0f0b96c572fc9f25c3f4ddbf4688b9b38c69836713fb255f4a2715d93cbaf44 \ - --hash=sha256:a777c096a49d80f9d2979695b835b0f9c9edab73b59e4ceb51f19724dda887ed \ - --hash=sha256:a9a4ac9648d39ce71c2f63fe7dc6db144b9fa567ddfc48b9fde1b54483d26042 \ - --hash=sha256:aa4969f24d536ae2268c902b2c3d62ab464b5a66bcb247630d208a79a8098e9b \ - --hash=sha256:c7390f9b2119b2b43160abb34f63277a638504ef8df99f11cb52c1fda66a2e6f \ - --hash=sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da +cryptography==3.4.8 \ + --hash=sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e \ + --hash=sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b \ + --hash=sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7 \ + --hash=sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085 \ + --hash=sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc \ + --hash=sha256:3c4129fc3fdc0fa8e40861b5ac0c673315b3c902bbdc05fc176764815b43dd1d \ + --hash=sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a \ + --hash=sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498 \ + --hash=sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89 \ + --hash=sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9 \ + --hash=sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c \ + --hash=sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7 \ + --hash=sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb \ + --hash=sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14 \ + --hash=sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af \ + --hash=sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e \ + --hash=sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5 \ + --hash=sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06 \ + --hash=sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7 # via -r requirements.in exceptiongroup==1.0.1 \ --hash=sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a \ @@ -184,7 +189,6 @@ six==1.15.0 \ --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced # via # -r requirements.in - # cryptography # packaging toml==0.10.1 \ --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ @@ -206,7 +210,9 @@ wcwidth==0.2.4 \ wheel==0.38.2 \ --hash=sha256:3d492ef22379a156ec923d2a77051cedfd4df4b667864e9e41ba83f0b70b7149 \ --hash=sha256:7a5a3095dceca97a3cac869b8fef4e89b83fafde21b6688f47b6fda7600eb441 - # via pip-tools + # via + # -r requirements.in + # pip-tools zipp==3.1.0 \ --hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \ --hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96