Files
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

289 lines
9.5 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Create an isolated A6 IMG3 lab identity and a derived SecureROM image."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import stat
import subprocess
import sys
ROM_SIZE = 0x10000
ROOT_CERT_OFFSET = 0xBFD0
ROOT_CERT_SLOT_SIZE = 1215
ROOT_SUBJECT = (
"/C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA"
)
INTERMEDIATE_SUBJECT = (
"/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/"
"CN=Apple Secure Boot Certification Authority"
)
IMG3_LEAF_SUBJECT = (
"/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/"
"CN=A6-Darwin-Prod-CEPO10-SDOM3-Lab"
)
TICKET_LEAF_SUBJECT = (
"/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/"
"CN=H5P-Darwin-Prod-CEPO1-Ticket-DataCenter"
)
class LabIdentityError(RuntimeError):
pass
def run(command, cwd):
try:
subprocess.run(command, cwd=cwd, check=True)
except FileNotFoundError as error:
raise LabIdentityError(
f"required program not found: {command[0]}"
) from error
except subprocess.CalledProcessError as error:
raise LabIdentityError(
f"command failed with exit status {error.returncode}: "
+ " ".join(command)
) from error
def openssl(*arguments, cwd):
run(["openssl", *arguments], cwd)
def sha256(data):
return hashlib.sha256(data).hexdigest()
def der_object_size(data, offset):
if offset >= len(data) or data[offset] != 0x30:
raise LabIdentityError(
f"no DER SEQUENCE at SecureROM offset 0x{offset:x}"
)
if offset + 2 > len(data):
raise LabIdentityError("truncated DER length")
first = data[offset + 1]
if first < 0x80:
return 2 + first
length_bytes = first & 0x7F
if not 1 <= length_bytes <= 4 or offset + 2 + length_bytes > len(data):
raise LabIdentityError("invalid DER length")
payload_size = int.from_bytes(
data[offset + 2:offset + 2 + length_bytes], "big"
)
return 2 + length_bytes + payload_size
def ensure_clean_output(path):
if path.exists():
if not path.is_dir():
raise LabIdentityError(
f"output exists and is not a directory: {path}"
)
if any(path.iterdir()):
raise LabIdentityError(
f"output directory is not empty: {path}; choose a new directory"
)
else:
path.mkdir(parents=True, mode=0o700)
def create_identity(source_rom, output, config):
source = source_rom.read_bytes()
if len(source) != ROM_SIZE:
raise LabIdentityError(
f"SecureROM must be exactly {ROM_SIZE} bytes, got {len(source)}"
)
embedded_size = der_object_size(source, ROOT_CERT_OFFSET)
if embedded_size != ROOT_CERT_SLOT_SIZE:
raise LabIdentityError(
"unexpected embedded trust anchor size at 0xbfd0: "
f"{embedded_size}, expected {ROOT_CERT_SLOT_SIZE}"
)
ensure_clean_output(output)
os.chmod(output, 0o700)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048",
"-out", "root-key.pem", cwd=output,
)
openssl(
"req", "-new", "-x509", "-sha1", "-days", "3650",
"-set_serial", "0x02", "-key", "root-key.pem",
# iBSS parses the complete fixed 1215-byte root slot. Its public
# subject layout plus the standard policy extension in the config
# preserves that DER size while all trust material remains lab-owned.
"-subj", ROOT_SUBJECT,
"-config", str(config), "-extensions", "v3_root",
"-out", "root.pem", cwd=output,
)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048",
"-out", "intermediate-key.pem", cwd=output,
)
openssl(
"req", "-new", "-sha256", "-key", "intermediate-key.pem",
"-subj", INTERMEDIATE_SUBJECT,
"-config", str(config),
"-out", "intermediate.csr", cwd=output,
)
openssl(
"x509", "-req", "-sha1", "-days", "3650",
"-set_serial", "0xA60010", "-in", "intermediate.csr",
"-CA", "root.pem", "-CAkey", "root-key.pem",
"-extfile", str(config), "-extensions", "v3_intermediate",
"-out", "intermediate.pem", cwd=output,
)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:1024",
"-out", "img3-leaf-key.pem", cwd=output,
)
openssl(
"req", "-new", "-sha256", "-key", "img3-leaf-key.pem",
"-subj", IMG3_LEAF_SUBJECT,
"-config", str(config),
"-out", "img3-leaf.csr", cwd=output,
)
openssl(
"x509", "-req", "-sha1", "-days", "3650",
"-set_serial", "0xA60105", "-in", "img3-leaf.csr",
"-CA", "intermediate.pem", "-CAkey", "intermediate-key.pem",
"-extfile", str(config), "-extensions", "v3_img3_leaf",
"-out", "img3-leaf.pem", cwd=output,
)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:1024",
"-out", "ticket-leaf-key.pem", cwd=output,
)
openssl(
"req", "-new", "-sha256", "-key", "ticket-leaf-key.pem",
# iBSS compares this public ticket-class name while certificate trust
# still comes exclusively from the generated lab root and keys.
"-subj", TICKET_LEAF_SUBJECT,
"-config", str(config),
"-out", "ticket-leaf.csr", cwd=output,
)
openssl(
"x509", "-req", "-sha1", "-days", "3650",
"-set_serial", "0xA6010B", "-in", "ticket-leaf.csr",
"-CA", "intermediate.pem", "-CAkey", "intermediate-key.pem",
"-extfile", str(config), "-extensions", "v3_ticket_leaf",
"-out", "ticket-leaf.pem", cwd=output,
)
for name in ("root", "intermediate", "img3-leaf", "ticket-leaf"):
openssl(
"x509", "-in", f"{name}.pem", "-outform", "DER",
"-out", f"{name}.der", cwd=output,
)
openssl(
"verify", "-auth_level", "0", "-ignore_critical",
"-CAfile", "root.pem",
"-untrusted", "intermediate.pem", "img3-leaf.pem", cwd=output,
)
openssl(
"verify", "-auth_level", "0", "-ignore_critical",
"-CAfile", "root.pem",
"-untrusted", "intermediate.pem", "ticket-leaf.pem", cwd=output,
)
root_der = (output / "root.der").read_bytes()
intermediate_der = (output / "intermediate.der").read_bytes()
leaf_der = (output / "img3-leaf.der").read_bytes()
ticket_leaf_der = (output / "ticket-leaf.der").read_bytes()
if len(root_der) > ROOT_CERT_SLOT_SIZE:
raise LabIdentityError(
f"lab root DER is {len(root_der)} bytes and does not fit the "
f"{ROOT_CERT_SLOT_SIZE}-byte SecureROM slot"
)
(output / "cert-chain.der").write_bytes(intermediate_der + leaf_der)
(output / "ticket-cert-chain.der").write_bytes(
intermediate_der + ticket_leaf_der
)
gid_key = os.urandom(32)
(output / "gid-key.bin").write_bytes(gid_key)
patched = bytearray(source)
patched[ROOT_CERT_OFFSET:ROOT_CERT_OFFSET + ROOT_CERT_SLOT_SIZE] = (
root_der + bytes(ROOT_CERT_SLOT_SIZE - len(root_der))
)
derived_rom = output / "s5l8950x-secure-rom-lab.bin"
derived_rom.write_bytes(patched)
for name in (
"root-key.pem", "intermediate-key.pem", "img3-leaf-key.pem",
"ticket-leaf-key.pem", "gid-key.bin",
):
os.chmod(output / name, stat.S_IRUSR | stat.S_IWUSR)
for name in ("intermediate.csr", "img3-leaf.csr", "ticket-leaf.csr"):
(output / name).unlink()
manifest = {
"format": 1,
"source_rom": str(source_rom.resolve()),
"source_rom_sha256": sha256(source),
"derived_rom": derived_rom.name,
"derived_rom_sha256": sha256(patched),
"root_certificate_offset": ROOT_CERT_OFFSET,
"root_certificate_slot_size": ROOT_CERT_SLOT_SIZE,
"root_certificate_der_size": len(root_der),
"root_certificate_sha256": sha256(root_der),
"certificate_chain_der_size": len(intermediate_der) + len(leaf_der),
"ticket_certificate_chain_der_size": (
len(intermediate_der) + len(ticket_leaf_der)
),
"gid_key_bits": 256,
"gid_key_sha256": sha256(gid_key),
}
(output / "identity.json").write_text(
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
return manifest
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source_rom", type=Path)
parser.add_argument("output_directory", type=Path)
args = parser.parse_args()
script_directory = Path(__file__).resolve().parent
config = script_directory / "a6-lab-openssl.cnf"
if not config.is_file():
raise LabIdentityError(f"OpenSSL configuration not found: {config}")
if not args.source_rom.is_file():
raise LabIdentityError(f"SecureROM not found: {args.source_rom}")
if shutil.which("openssl") is None:
raise LabIdentityError("openssl is required")
manifest = create_identity(
args.source_rom.resolve(), args.output_directory.resolve(), config
)
print(f"Lab identity created in {args.output_directory.resolve()}")
print(
"Derived SecureROM SHA-256: " + manifest["derived_rom_sha256"]
)
print(
"GID key: 256-bit private lab value (SHA-256 fingerprint "
+ manifest["gid_key_sha256"] + ")"
)
if __name__ == "__main__":
try:
main()
except (OSError, LabIdentityError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)