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.
289 lines
9.5 KiB
Python
Executable File
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)
|