#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-2.0-or-later """Build a signed, optionally GID-wrapped IMG3 for the A6 lab identity.""" import argparse import hashlib import json import os from pathlib import Path import struct import subprocess import sys import tempfile IMG3_HEADER = struct.Struct("<4s4I") TAG_HEADER = struct.Struct("<4s2I") IMG3_MAGIC = b"3gmI" SHSH_SIZE = 128 DEFAULT_ECID = 0x200000 DEFAULT_SECURITY_DOMAIN = 3 DEFAULT_PRODUCTION_MODE = 1 DEFAULT_BOARD_ID = 0 DEFAULT_CHIP_EPOCH = 0x10 class Img3BuildError(RuntimeError): pass def raw_tag(name): return name.encode("ascii")[::-1] def display_tag(value): return value[::-1].decode("ascii", errors="replace") def parse_template(data): if len(data) < IMG3_HEADER.size: raise Img3BuildError("template is shorter than the IMG3 header") magic, full_size, data_size, _, image_type = IMG3_HEADER.unpack_from(data) if magic != IMG3_MAGIC: raise Img3BuildError("template does not have IMG3 magic") if full_size > len(data) or data_size > full_size - IMG3_HEADER.size: raise Img3BuildError("template IMG3 sizes are out of bounds") tags = [] offset = IMG3_HEADER.size end = IMG3_HEADER.size + data_size while offset < end: if end - offset < TAG_HEADER.size: raise Img3BuildError(f"truncated tag at 0x{offset:x}") tag, total_size, payload_size = TAG_HEADER.unpack_from(data, offset) if ( total_size < TAG_HEADER.size or payload_size > total_size - TAG_HEADER.size or total_size > end - offset ): raise Img3BuildError(f"invalid tag at 0x{offset:x}") tags.append({ "name": display_tag(tag), "payload": data[ offset + TAG_HEADER.size:offset + TAG_HEADER.size + payload_size ], "padding": total_size - TAG_HEADER.size - payload_size, }) offset += total_size if offset != end: raise Img3BuildError("template tag sizes do not cover data_size") return image_type, tags def make_tag(name, payload, padding=0): total_size = TAG_HEADER.size + len(payload) + padding return ( TAG_HEADER.pack(raw_tag(name), total_size, len(payload)) + payload + bytes(padding) ) def openssl_filter(arguments, payload): try: result = subprocess.run( ["openssl", *arguments], input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) except FileNotFoundError as error: raise Img3BuildError("openssl is required") from error except subprocess.CalledProcessError as error: detail = error.stderr.decode("utf-8", errors="replace").strip() raise Img3BuildError(f"openssl failed: {detail}") from error return result.stdout def aes256_cbc(payload, key, iv, decrypt=False): if len(payload) % 16: raise Img3BuildError("AES-CBC input must be a multiple of 16 bytes") arguments = [ "enc", "-aes-256-cbc", "-nopad", "-K", key.hex(), "-iv", iv.hex() ] if decrypt: arguments.append("-d") return openssl_filter(arguments, payload) def sign_sha1(payload, private_key): return openssl_filter( ["dgst", "-sha1", "-sign", str(private_key)], payload ) def build_image(template, payload, identity, ecid, encrypted, ticketed, security_domain, production_mode, board_id, chip_epoch): image_type, template_tags = parse_template(template) chain = None leaf_key = None if not ticketed: chain = (identity / "cert-chain.der").read_bytes() leaf_key = identity / "img3-leaf-key.pem" if not leaf_key.is_file(): raise Img3BuildError(f"leaf private key not found: {leaf_key}") if encrypted and len(payload) % 16: raise Img3BuildError( "encrypted DATA length must be a multiple of the AES block size" ) content_iv = os.urandom(16) if encrypted else None content_key = os.urandom(32) if encrypted else None image_data = ( aes256_cbc(payload, content_key, content_iv) if encrypted else payload ) elements = [] saw_type = False saw_data = False for tag in template_tags: name = tag["name"] if name in {"KBAG", "ECID", "SHSH", "CERT"}: continue if name == "TYPE": saw_type = True if name == "DATA": saw_data = True padding = (-(TAG_HEADER.size + len(image_data))) % 4 elements.append(make_tag("DATA", image_data, padding)) else: elements.append(make_tag(name, tag["payload"], tag["padding"])) if not saw_type or not saw_data: raise Img3BuildError("template must contain TYPE and DATA tags") if not ticketed: # The public IPSW template is unpersonalized and therefore omits the # device-bound scalar tags that SecureROM requires after SHSH # validation. The iBEC path carries these values in its external # APTicket instead, matching the original iOS 10 boot flow. existing_tags = {tag["name"] for tag in template_tags} for name, value in ( ("SDOM", security_domain), ("PROD", production_mode), ("CEPO", chip_epoch), ("BORD", board_id), ): if name not in existing_tags: elements.append(make_tag(name, struct.pack("