Files
QEMU-S5L8950X/scripts/fetch-a6-bootloaders.py
T
Yaya48 5d9a60a926 hw/arm: add authenticated A6 IMG3 boot lab
Model the A6 crypto, interrupt, USB, and platform blocks needed to boot SecureROM through iBSS into iBEC Recovery.

Add local lab identity, IMG3, and APTicket tooling, patched macOS recovery utilities, UART and GDB access, and English end-user documentation.
2026-09-01 09:51:49 -07:00

200 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Fetch only A6 boot-chain files from Apple's iOS 10.3.4 IPSW.
The complete IPSW is about 2 GB. This utility reads its ZIP directory with
HTTP range requests, then downloads only BuildManifest.plist and the selected
iBSS/iBEC pair. It does not personalize or decrypt the images.
"""
import argparse
import binascii
import os
from pathlib import Path
import re
import struct
import sys
import urllib.request
import zlib
DEFAULT_URL = (
"https://updates.cdn-apple.com/2019/ios/"
"091-25277-20190722-0C1B94DE-992C-11E9-A2EE-E2C9A77C2E40/"
"iPhone_4.0_32bit_10.3.4_14G61_Restore.ipsw"
)
EOCD = struct.Struct("<4s4H2IH")
CENTRAL = struct.Struct("<4s6H3I5H2I")
LOCAL = struct.Struct("<4s5H3I2H")
class RangeReader:
def __init__(self, url):
self.url = url
self.total_size = None
def read(self, start, end):
request = urllib.request.Request(
self.url,
headers={"Range": f"bytes={start}-{end}"},
)
with urllib.request.urlopen(request, timeout=60) as response:
if response.status != 206:
raise RuntimeError(
f"server ignored byte range {start}-{end} "
f"(HTTP {response.status})"
)
content_range = response.headers.get("Content-Range", "")
match = re.fullmatch(r"bytes (\d+)-(\d+)/(\d+)", content_range)
if not match:
raise RuntimeError(f"invalid Content-Range: {content_range!r}")
actual_start, actual_end, total = map(int, match.groups())
if (actual_start, actual_end) != (start, end):
raise RuntimeError(
f"unexpected byte range {actual_start}-{actual_end}"
)
self.total_size = total
data = response.read(end - start + 1)
if len(data) != end - start + 1:
raise RuntimeError("truncated HTTP range response")
return data
def get_size(self):
self.read(0, 0)
return self.total_size
def read_directory(reader):
total_size = reader.get_size()
tail_size = min(total_size, 65557)
tail_offset = total_size - tail_size
tail = reader.read(tail_offset, total_size - 1)
eocd_offset = tail.rfind(b"PK\x05\x06")
if eocd_offset < 0 or eocd_offset + EOCD.size > len(tail):
raise RuntimeError("ZIP end-of-central-directory record not found")
fields = EOCD.unpack_from(tail, eocd_offset)
(
_, disk, directory_disk, disk_entries, entries,
size, offset, comment,
) = fields
if disk or directory_disk or disk_entries != entries:
raise RuntimeError("multi-disk ZIP archives are unsupported")
if eocd_offset + EOCD.size + comment > len(tail):
raise RuntimeError("truncated ZIP comment")
if offset == 0xFFFFFFFF or size == 0xFFFFFFFF or entries == 0xFFFF:
raise RuntimeError("ZIP64 directory is unsupported")
data = reader.read(offset, offset + size - 1)
result = {}
cursor = 0
for _ in range(entries):
if cursor + CENTRAL.size > len(data):
raise RuntimeError("truncated ZIP central directory")
fields = CENTRAL.unpack_from(data, cursor)
if fields[0] != b"PK\x01\x02":
raise RuntimeError("invalid ZIP central-directory signature")
flags = fields[3]
compressed_size = fields[8]
uncompressed_size = fields[9]
name_length = fields[10]
extra_length = fields[11]
comment_length = fields[12]
local_offset = fields[16]
name_start = cursor + CENTRAL.size
name_end = name_start + name_length
encoding = "utf-8" if flags & 0x800 else "cp437"
name = data[name_start:name_end].decode(encoding)
result[name] = {
"flags": flags,
"method": fields[4],
"crc32": fields[7],
"compressed_size": compressed_size,
"uncompressed_size": uncompressed_size,
"local_offset": local_offset,
}
cursor = name_end + extra_length + comment_length
return result
def extract_entry(reader, entry):
if entry["flags"] & 1:
raise RuntimeError("encrypted ZIP entries are unsupported")
if entry["compressed_size"] == 0xFFFFFFFF:
raise RuntimeError("ZIP64 entries are unsupported")
local_offset = entry["local_offset"]
header = reader.read(local_offset, local_offset + LOCAL.size - 1)
fields = LOCAL.unpack(header)
if fields[0] != b"PK\x03\x04":
raise RuntimeError("invalid ZIP local-header signature")
name_length = fields[9]
extra_length = fields[10]
data_offset = local_offset + LOCAL.size + name_length + extra_length
compressed_size = entry["compressed_size"]
compressed = reader.read(data_offset, data_offset + compressed_size - 1)
if entry["method"] == 0:
data = compressed
elif entry["method"] == 8:
data = zlib.decompress(compressed, -zlib.MAX_WBITS)
else:
raise RuntimeError(f"unsupported ZIP method {entry['method']}")
if len(data) != entry["uncompressed_size"]:
raise RuntimeError("uncompressed size mismatch")
if binascii.crc32(data) & 0xFFFFFFFF != entry["crc32"]:
raise RuntimeError("CRC-32 mismatch")
return data
def write_atomic(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_bytes(data)
os.replace(temporary, path)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--board", choices=("n41", "n42"), default="n41")
parser.add_argument("--url", default=DEFAULT_URL)
parser.add_argument("--output-dir", type=Path, default=Path("firmware"))
args = parser.parse_args()
reader = RangeReader(args.url)
directory = read_directory(reader)
requested = ["BuildManifest.plist"]
ipsw_board = "iphone5"
for component in ("iBSS", "iBEC"):
candidates = (
f"Firmware/dfu/{component}.{args.board}.RELEASE.dfu",
f"Firmware/dfu/{component}.{args.board}ap.RELEASE.dfu",
f"Firmware/dfu/{component}.{ipsw_board}.RELEASE.dfu",
)
match = next((name for name in candidates if name in directory), None)
if not match:
available = sorted(
name for name in directory
if "/dfu/" in name.lower() and component.lower() in name.lower()
)
raise RuntimeError(
"missing IPSW entry; tried: " + ", ".join(candidates) +
"; available: " + (", ".join(available) or "none")
)
requested.append(match)
for name in requested:
destination = args.output_dir / Path(name).name
print(f"Fetching {name} -> {destination}", flush=True)
write_atomic(destination, extract_entry(reader, directory[name]))
print("Raw Apple images extracted; personalization is still required.")
if __name__ == "__main__":
try:
main()
except Exception as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(1)