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.
113 lines
3.5 KiB
Python
Executable File
113 lines
3.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""Replace an iBoot payload's embedded IMG3 trust anchor with the lab root."""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
|
|
|
|
ROM_SIZE = 0x10000
|
|
ROOT_CERT_OFFSET = 0xBFD0
|
|
ROOT_CERT_SLOT_SIZE = 1215
|
|
|
|
|
|
class RootPatchError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def der_object_size(data):
|
|
if len(data) < 2 or data[0] != 0x30:
|
|
raise RootPatchError("lab root is not a DER SEQUENCE")
|
|
first = data[1]
|
|
if first < 0x80:
|
|
return 2 + first
|
|
length_bytes = first & 0x7F
|
|
if not 1 <= length_bytes <= 4 or 2 + length_bytes > len(data):
|
|
raise RootPatchError("lab root has an invalid DER length")
|
|
payload_size = int.from_bytes(data[2:2 + length_bytes], "big")
|
|
return 2 + length_bytes + payload_size
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--source-rom", required=True, type=Path)
|
|
parser.add_argument("--identity", required=True, type=Path)
|
|
parser.add_argument("--input", required=True, type=Path)
|
|
parser.add_argument("--output", required=True, type=Path)
|
|
args = parser.parse_args()
|
|
|
|
if args.output.exists():
|
|
raise RootPatchError(f"refusing to overwrite output: {args.output}")
|
|
source_rom = args.source_rom.read_bytes()
|
|
if len(source_rom) != ROM_SIZE:
|
|
raise RootPatchError(f"source SecureROM must be {ROM_SIZE} bytes")
|
|
source_anchor = source_rom[
|
|
ROOT_CERT_OFFSET:ROOT_CERT_OFFSET + ROOT_CERT_SLOT_SIZE
|
|
]
|
|
lab_root = (args.identity / "root.der").read_bytes()
|
|
if der_object_size(lab_root) != len(lab_root):
|
|
raise RootPatchError("lab root contains trailing data")
|
|
if len(lab_root) > ROOT_CERT_SLOT_SIZE:
|
|
raise RootPatchError(
|
|
"lab root does not fit the iBoot trust-anchor slot"
|
|
)
|
|
lab_anchor = lab_root + bytes(ROOT_CERT_SLOT_SIZE - len(lab_root))
|
|
|
|
payload = args.input.read_bytes()
|
|
offsets = []
|
|
start = 0
|
|
while True:
|
|
offset = payload.find(source_anchor, start)
|
|
if offset < 0:
|
|
break
|
|
offsets.append(offset)
|
|
start = offset + 1
|
|
if len(offsets) != 1:
|
|
raise RootPatchError(
|
|
f"expected exactly one embedded source root, found {len(offsets)}"
|
|
)
|
|
|
|
offset = offsets[0]
|
|
patched = (
|
|
payload[:offset]
|
|
+ lab_anchor
|
|
+ payload[offset + ROOT_CERT_SLOT_SIZE:]
|
|
)
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.NamedTemporaryFile(
|
|
prefix=args.output.name + ".", dir=args.output.parent, delete=False
|
|
) as temporary:
|
|
temporary.write(patched)
|
|
temporary_path = Path(temporary.name)
|
|
temporary_path.replace(args.output)
|
|
metadata = {
|
|
"format": 1,
|
|
"input": str(args.input.resolve()),
|
|
"output": args.output.name,
|
|
"trust_anchor_offset": offset,
|
|
"trust_anchor_slot_size": ROOT_CERT_SLOT_SIZE,
|
|
"input_sha256": hashlib.sha256(payload).hexdigest(),
|
|
"output_sha256": hashlib.sha256(patched).hexdigest(),
|
|
"lab_root_sha256": hashlib.sha256(lab_root).hexdigest(),
|
|
}
|
|
args.output.with_name(args.output.name + ".json").write_text(
|
|
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
|
|
)
|
|
print(
|
|
f"Patched {args.output} ({len(patched)} bytes), "
|
|
f"trust anchor at 0x{offset:x}"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except (OSError, RootPatchError) as error:
|
|
print(f"error: {error}", file=sys.stderr)
|
|
sys.exit(2)
|