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.
This commit is contained in:
2026-09-01 09:51:49 -07:00
parent 47977dd34a
commit 5d9a60a926
45 changed files with 6002 additions and 265 deletions
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Validate and display the bounded structure of an Apple Image3 file."""
import argparse
import json
from pathlib import Path
import struct
import sys
IMG3_HEADER = struct.Struct("<4s4I")
TAG_HEADER = struct.Struct("<4s2I")
class Img3Error(ValueError):
pass
def fourcc(raw):
return raw[::-1].decode("ascii", errors="replace")
def scalar(tag, payload):
if tag == "TYPE" and len(payload) >= 4:
return fourcc(payload[:4])
if tag == "VERS" and len(payload) > 4:
return payload[4:].split(b"\0", 1)[0].decode("ascii", errors="replace")
if tag in {"CHIP", "BORD", "SEPO", "PROD", "SDOM"} and len(payload) == 4:
return f"0x{struct.unpack('<I', payload)[0]:08x}"
if tag == "ECID" and len(payload) == 8:
return f"0x{struct.unpack('<Q', payload)[0]:016x}"
if tag == "KBAG" and len(payload) >= 8:
key_modifier, key_bits = struct.unpack_from("<II", payload)
expected = 8 + 16 + key_bits // 8
suffix = "" if len(payload) == expected else f" payload={len(payload)}"
return (
f"key_modifier={key_modifier} key_bits={key_bits}" + suffix
)
return None
def parse_img3(data):
if len(data) < IMG3_HEADER.size:
raise Img3Error("file is shorter than the 20-byte Image3 header")
magic, full_size, data_size, shsh_offset, image_type = (
IMG3_HEADER.unpack_from(data)
)
if fourcc(magic) != "Img3":
raise Img3Error(f"bad magic {magic.hex()} (expected Image3)")
if full_size < IMG3_HEADER.size or full_size > len(data):
raise Img3Error(
f"full_size {full_size} is outside the {len(data)}-byte input"
)
if data_size > full_size - IMG3_HEADER.size:
raise Img3Error("data_size extends past full_size")
if shsh_offset > data_size:
raise Img3Error("shsh_offset extends past data_size")
tags = []
offset = IMG3_HEADER.size
end = IMG3_HEADER.size + data_size
while offset < end:
if end - offset < TAG_HEADER.size:
raise Img3Error(f"truncated tag header at 0x{offset:x}")
raw_tag, total_size, payload_size = TAG_HEADER.unpack_from(data, offset)
tag = fourcc(raw_tag)
if total_size < TAG_HEADER.size:
raise Img3Error(f"{tag} at 0x{offset:x} has an invalid total size")
if payload_size > total_size - TAG_HEADER.size:
raise Img3Error(
f"{tag} at 0x{offset:x} has an invalid payload size"
)
if total_size > end - offset:
raise Img3Error(f"{tag} at 0x{offset:x} extends past data_size")
payload_start = offset + TAG_HEADER.size
payload = data[payload_start:payload_start + payload_size]
tags.append({
"tag": tag,
"offset": offset,
"payload_offset": payload_start,
"total_size": total_size,
"payload_size": payload_size,
"padding_size": total_size - TAG_HEADER.size - payload_size,
"value": scalar(tag, payload),
})
offset += total_size
if offset != end:
raise Img3Error("tag sizes do not exactly cover data_size")
return {
"magic": "Img3",
"file_size": len(data),
"full_size": full_size,
"data_size": data_size,
"shsh_offset": shsh_offset,
"image_type": fourcc(struct.pack("<I", image_type)),
"trailing_size": len(data) - full_size,
"tags": tags,
}
def print_human(path, image):
print(f"{path}: {image['magic']} type={image['image_type']}")
print(
f" file={image['file_size']} full={image['full_size']} "
f"data={image['data_size']} shsh_offset=0x{image['shsh_offset']:x} "
f"trailing={image['trailing_size']}"
)
for tag in image["tags"]:
value = f" value={tag['value']}" if tag["value"] is not None else ""
print(
f" 0x{tag['offset']:08x} {tag['tag']:<4} "
f"total={tag['total_size']:<8} data={tag['payload_size']:<8} "
f"padding={tag['padding_size']}{value}"
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("image", type=Path)
parser.add_argument("--json", action="store_true", dest="as_json")
parser.add_argument("--extract-data", type=Path, metavar="PATH")
args = parser.parse_args()
data = args.image.read_bytes()
image = parse_img3(data)
if args.extract_data:
data_tags = [tag for tag in image["tags"] if tag["tag"] == "DATA"]
if len(data_tags) != 1:
raise Img3Error(f"expected one DATA tag, found {len(data_tags)}")
tag = data_tags[0]
start = tag["payload_offset"]
args.extract_data.write_bytes(data[start:start + tag["payload_size"]])
print(f"Extracted {tag['payload_size']} bytes to {args.extract_data}")
if args.as_json:
print(json.dumps(image, indent=2))
else:
print_human(args.image, image)
if __name__ == "__main__":
try:
main()
except (OSError, Img3Error) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Create a lab-signed A6 APTicket and prefix it to a ticketed iBEC IMG3."""
import argparse
import hashlib
import json
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
IMG3_HEADER = struct.Struct("<4s4I")
IMG3_MAGIC = b"3gmI"
SHA1_WITH_RSA = bytes.fromhex("300b06092a864886f70d010105")
SIGNATURE_SIZE = 128
DEFAULT_ECID = 0x200000
DEFAULT_CHIP_ID = 0x8950
DEFAULT_BOARD_ID = 0
DEFAULT_PRODUCTION_MODE = 1
DEFAULT_SECURITY_DOMAIN = 3
DEFAULT_BUILD_IDENTITY = "iBoot-3406.60.10~70"
# iBoot 3406 expects the complete IMG3-era manifest shape even though the
# boot-only lab path validates just the device binding, nonce, and iBEC digest.
# Values for components that are not sent by this emulator are deterministic
# lab placeholders. They do not come from an Apple-issued ticket.
MANIFEST_DIGEST_TAGS = (
7, 8, 9, 10, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28,
75, 78, 79, 80, 228, 229,
)
MANIFEST_FLAG_TAGS = (
48, 49, 50, 51, 54, 55, 56, 57, 59, 60, 61, 62,
84, 85, 86, 231, 232, 233,
)
MANIFEST_BUILD_TAGS = (6, 20, 22)
class TicketBuildError(RuntimeError):
pass
def boot_nonce(value):
try:
decoded = bytes.fromhex(value.removeprefix("0x"))
except ValueError as error:
raise argparse.ArgumentTypeError(
"boot nonce must be hexadecimal"
) from error
if len(decoded) != 8:
raise argparse.ArgumentTypeError(
"boot nonce must contain exactly 8 bytes"
)
return decoded
def encode_length(length):
if length < 0:
raise TicketBuildError("negative DER length")
if length < 0x80:
return bytes((length,))
encoded = length.to_bytes((length.bit_length() + 7) // 8, "big")
return bytes((0x80 | len(encoded),)) + encoded
def encode_tlv(identifier, payload):
return bytes((identifier,)) + encode_length(len(payload)) + payload
def encode_context(tag, payload, constructed=False):
identifier = 0xA0 if constructed else 0x80
if tag < 31:
return encode_tlv(identifier | tag, payload)
encoded_tag = bytearray((tag & 0x7F,))
tag >>= 7
while tag:
encoded_tag.insert(0, 0x80 | (tag & 0x7F))
tag >>= 7
prefix = bytes((identifier | 0x1F,)) + bytes(encoded_tag)
return prefix + encode_length(len(payload)) + payload
def sign_sha1(payload, key):
try:
result = subprocess.run(
["openssl", "dgst", "-sha1", "-sign", str(key)],
input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
)
except FileNotFoundError as error:
raise TicketBuildError("openssl is required") from error
except subprocess.CalledProcessError as error:
detail = error.stderr.decode("utf-8", errors="replace").strip()
raise TicketBuildError(f"openssl failed: {detail}") from error
return result.stdout
def component_digest(component):
if len(component) < IMG3_HEADER.size:
raise TicketBuildError("iBEC is shorter than its IMG3 header")
magic, full_size, data_size, shsh_offset, _ = IMG3_HEADER.unpack_from(
component
)
if magic != IMG3_MAGIC or full_size != len(component):
raise TicketBuildError("iBEC is not a bounded IMG3 container")
if data_size != len(component) - IMG3_HEADER.size:
raise TicketBuildError("iBEC IMG3 data_size is inconsistent")
if shsh_offset != data_size:
raise TicketBuildError("iBEC must use external-ticket IMG3 layout")
return hashlib.sha1(component[12:]).digest()
def placeholder_digest(tag):
label = f"QEMU A6 lab placeholder manifest tag {tag}".encode("ascii")
return hashlib.sha1(label).digest()
def build_manifest(args, digest):
fields = {tag: placeholder_digest(tag) for tag in MANIFEST_DIGEST_TAGS}
fields.update(
{tag: (1).to_bytes(4, "little") for tag in MANIFEST_FLAG_TAGS}
)
fields.update(
{
tag: args.build_identity.encode("ascii")
for tag in MANIFEST_BUILD_TAGS
}
)
fields.update({
1: args.ecid.to_bytes(8, "little"),
2: args.chip_id.to_bytes(4, "little"),
3: args.board_id.to_bytes(4, "little"),
4: args.production_mode.to_bytes(4, "little"),
5: args.security_domain.to_bytes(4, "little"),
18: hashlib.sha1(args.boot_nonce).digest(),
230: digest,
})
return fields
def atomic_write(path, payload):
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
prefix=path.name + ".", dir=path.parent, delete=False
) as temporary:
temporary.write(payload)
temporary_path = Path(temporary.name)
temporary_path.replace(path)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--component", required=True, type=Path)
parser.add_argument("--identity", required=True, type=Path)
parser.add_argument("--ticket-output", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument(
"--ecid", type=lambda value: int(value, 0), default=DEFAULT_ECID
)
parser.add_argument(
"--chip-id", type=lambda value: int(value, 0), default=DEFAULT_CHIP_ID
)
parser.add_argument(
"--board-id", type=lambda value: int(value, 0), default=DEFAULT_BOARD_ID
)
parser.add_argument(
"--production-mode", type=lambda value: int(value, 0), choices=(0, 1),
default=DEFAULT_PRODUCTION_MODE,
)
parser.add_argument(
"--security-domain", type=lambda value: int(value, 0),
default=DEFAULT_SECURITY_DOMAIN,
)
parser.add_argument("--build-identity", default=DEFAULT_BUILD_IDENTITY)
parser.add_argument(
"--boot-nonce", type=boot_nonce, required=True,
help=(
"raw 8-byte iBSS boot nonce in hexadecimal; stored as the "
"original APTicket [18] SHA-1 nonce binding"
),
)
args = parser.parse_args()
required = (
(args.component, "ticketed iBEC"),
(args.identity / "ticket-leaf-key.pem", "ticket leaf private key"),
(args.identity / "ticket-cert-chain.der", "ticket certificate chain"),
)
for path, description in required:
if not path.is_file():
raise TicketBuildError(f"{description} not found: {path}")
for path in (args.ticket_output, args.output):
if path.exists():
raise TicketBuildError(f"refusing to overwrite output: {path}")
component = args.component.read_bytes()
digest = component_digest(component)
replacements = build_manifest(args, digest)
# DER SET members must be sorted by their complete encoded byte strings.
# No external or previously issued ticket is used as input.
encoded_fields = [
encode_context(tag, payload) for tag, payload in replacements.items()
]
fields = b"".join(sorted(encoded_fields))
manifest_der = encode_tlv(0x31, fields)
signature = sign_sha1(manifest_der, args.identity / "ticket-leaf-key.pem")
if len(signature) != SIGNATURE_SIZE:
raise TicketBuildError(
f"ticket leaf produced {len(signature)} signature bytes; "
"expected 128"
)
chain = (args.identity / "ticket-cert-chain.der").read_bytes()
ticket = encode_tlv(
0x30,
SHA1_WITH_RSA + encode_tlv(0x31, fields)
+ encode_tlv(0x04, signature)
+ encode_context(1, chain, constructed=True),
)
padded_size = (len(ticket) + 63) & ~63
combined = ticket + bytes((0xFF,)) * (padded_size - len(ticket)) + component
atomic_write(args.ticket_output, ticket)
atomic_write(args.output, combined)
metadata = {
"format": 1,
"ticket_size": len(ticket),
"ticket_padded_size": padded_size,
"component_size": len(component),
"combined_size": len(combined),
"ecid": f"0x{args.ecid:016x}",
"chip_id": f"0x{args.chip_id:04x}",
"board_id": args.board_id,
"production_mode": args.production_mode,
"security_domain": args.security_domain,
"build_identity": args.build_identity,
"boot_nonce": args.boot_nonce.hex(),
"boot_nonce_sha1": hashlib.sha1(args.boot_nonce).hexdigest(),
"manifest_tags": sorted(replacements),
"ibec_manifest_tag": 230,
"ibec_sha1_range": "0x0c..EOF",
"ibec_sha1": digest.hex(),
"ticket_sha256": hashlib.sha256(ticket).hexdigest(),
"combined_sha256": hashlib.sha256(combined).hexdigest(),
}
args.output.with_name(args.output.name + ".json").write_text(
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
)
print(
f"Created lab APTicket ({len(ticket)} bytes) and ticketed iBEC "
f"({len(combined)} bytes): {args.output}"
)
print(f"Manifest [230] iBEC SHA-1: {digest.hex()}")
if __name__ == "__main__":
try:
main()
except (OSError, TicketBuildError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+288
View File
@@ -0,0 +1,288 @@
#!/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)
+354
View File
@@ -0,0 +1,354 @@
#!/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("<I", value)))
if encrypted:
gid_key_path = identity / "gid-key.bin"
gid_key = gid_key_path.read_bytes()
if len(gid_key) != 32:
raise Img3BuildError("lab GID key must be exactly 32 bytes")
clear_keybag = content_iv + content_key
wrapped_keybag = aes256_cbc(clear_keybag, gid_key, bytes(16))
unwrapped_keybag = aes256_cbc(
wrapped_keybag, gid_key, bytes(16), decrypt=True
)
if unwrapped_keybag != clear_keybag:
raise Img3BuildError("internal KBAG AES round-trip failed")
elements.append(
make_tag("KBAG", struct.pack("<II", 1, 256) + wrapped_keybag)
)
before_shsh = b"".join(elements)
if ticketed:
data_size = len(before_shsh)
full_size = IMG3_HEADER.size + data_size
header = IMG3_HEADER.pack(
IMG3_MAGIC, full_size, data_size, data_size, image_type
)
result = header + before_shsh
if encrypted:
decrypted = aes256_cbc(
image_data, content_key, content_iv, decrypt=True
)
if decrypted != payload:
raise Img3BuildError("internal DATA AES round-trip failed")
metadata = {
"format": 1,
"image_type": display_tag(struct.pack("<I", image_type)),
"full_size": full_size,
"data_size": data_size,
"shsh_offset": data_size,
"authentication": "external-apticket",
"encrypted": encrypted,
"kbag_key_modifier": 1 if encrypted else None,
"kbag_key_bits": 256 if encrypted else None,
"apticket_component_range": "0x0c..EOF",
"apticket_component_sha1": hashlib.sha1(result[12:]).hexdigest(),
"img3_sha256": hashlib.sha256(result).hexdigest(),
}
return result, metadata
# Apple's A6 TSS response uses a 64-byte ECID element (8 data + 44 pad).
elements.append(make_tag("ECID", struct.pack("<Q", ecid), 44))
before_shsh = b"".join(elements)
cert_element_size = TAG_HEADER.size + len(chain)
data_size = (
len(before_shsh) + TAG_HEADER.size + SHSH_SIZE + cert_element_size
)
full_size = IMG3_HEADER.size + data_size
header = IMG3_HEADER.pack(
IMG3_MAGIC, full_size, data_size, len(before_shsh), image_type
)
signed_bytes = header[12:] + before_shsh
signature = sign_sha1(signed_bytes, leaf_key)
if len(signature) != SHSH_SIZE:
raise Img3BuildError(
f"IMG3 leaf produced a {len(signature)}-byte signature; "
"expected 128"
)
result = (
header + before_shsh + make_tag("SHSH", signature)
+ make_tag("CERT", chain)
)
if len(result) != full_size:
raise Img3BuildError("internal IMG3 size mismatch")
if encrypted:
decrypted = aes256_cbc(
image_data, content_key, content_iv, decrypt=True
)
if decrypted != payload:
raise Img3BuildError("internal DATA AES round-trip failed")
metadata = {
"format": 1,
"image_type": display_tag(struct.pack("<I", image_type)),
"full_size": full_size,
"data_size": data_size,
"shsh_offset": len(before_shsh),
"ecid": f"0x{ecid:016x}",
"security_domain": security_domain,
"production_mode": production_mode,
"board_id": board_id,
"chip_epoch": chip_epoch,
"authentication": "embedded-shsh",
"encrypted": encrypted,
"kbag_key_modifier": 1 if encrypted else None,
"kbag_key_bits": 256 if encrypted else None,
"signed_range": "0x0c..SHSH",
"signed_sha1": hashlib.sha1(signed_bytes).hexdigest(),
"img3_sha256": hashlib.sha256(result).hexdigest(),
}
return result, metadata
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--template", required=True, type=Path)
parser.add_argument("--payload", required=True, type=Path)
parser.add_argument("--identity", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument(
"--ecid", type=lambda value: int(value, 0), default=DEFAULT_ECID
)
parser.add_argument(
"--security-domain", type=lambda value: int(value, 0),
default=DEFAULT_SECURITY_DOMAIN,
)
parser.add_argument(
"--production-mode", type=lambda value: int(value, 0), choices=(0, 1),
default=DEFAULT_PRODUCTION_MODE,
)
parser.add_argument(
"--board-id", type=lambda value: int(value, 0),
default=DEFAULT_BOARD_ID,
)
parser.add_argument(
"--chip-epoch", type=lambda value: int(value, 0),
default=DEFAULT_CHIP_EPOCH,
)
parser.add_argument("--encrypt", action="store_true")
parser.add_argument(
"--ticketed", action="store_true",
help="omit embedded ECID/SHSH/CERT for an external APTicket (iBEC)",
)
args = parser.parse_args()
for path, description in (
(args.template, "IMG3 template"),
(args.payload, "payload"),
):
if not path.is_file():
raise Img3BuildError(f"{description} not found: {path}")
if not args.ticketed and not (args.identity / "cert-chain.der").is_file():
raise Img3BuildError(
"lab certificate chain not found: "
f"{args.identity / 'cert-chain.der'}"
)
if args.output.exists():
raise Img3BuildError(f"refusing to overwrite output: {args.output}")
image, metadata = build_image(
args.template.read_bytes(), args.payload.read_bytes(),
args.identity.resolve(), args.ecid, args.encrypt, args.ticketed,
args.security_domain, args.production_mode, args.board_id,
args.chip_epoch,
)
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(image)
temporary_path = Path(temporary.name)
temporary_path.replace(args.output)
manifest_path = args.output.with_name(args.output.name + ".json")
manifest_path.write_text(
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
)
print(
f"Created {metadata['image_type']} IMG3: {args.output} "
f"({metadata['full_size']} bytes)"
)
print(f"Encrypted DATA/KBAG: {'yes' if args.encrypt else 'no'}")
if args.ticketed:
print(
"APTicket component SHA-1: "
f"{metadata['apticket_component_sha1']}"
)
else:
print(f"Signed SHA-1: {metadata['signed_sha1']}")
if __name__ == "__main__":
try:
main()
except (OSError, Img3BuildError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+52
View File
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: GPL-2.0-or-later
[req]
distinguished_name = distinguished_name
prompt = no
# The A6 SecureROM compares the complete ASN.1 Common Name object, including
# its PrintableString tag, for the public Apple secure-boot authority name.
string_mask = default
[distinguished_name]
C = ZZ
O = QEMU A6 Lab
OU = Secure Boot Research
CN = QEMU A6 Lab
[v3_root]
keyUsage = critical, keyCertSign, cRLSign
basicConstraints = critical, CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
# Standard public Apple Root CA policy metadata. Reusing its DER shape keeps
# the generated lab root at the exact 1215-byte A6 trust-anchor size; the
# certificate public key and signature are still generated locally.
2.5.29.32 = DER:30:82:01:04:30:82:01:00:06:09:2A:86:48:86:F7:63:64:05:01:30:81:F2:30:2A:06:08:2B:06:01:05:05:07:02:01:16:1E:68:74:74:70:73:3A:2F:2F:77:77:77:2E:61:70:70:6C:65:2E:63:6F:6D:2F:61:70:70:6C:65:63:61:2F:30:81:C3:06:08:2B:06:01:05:05:07:02:02:30:81:B6:1A:81:B3:52:65:6C:69:61:6E:63:65:20:6F:6E:20:74:68:69:73:20:63:65:72:74:69:66:69:63:61:74:65:20:62:79:20:61:6E:79:20:70:61:72:74:79:20:61:73:73:75:6D:65:73:20:61:63:63:65:70:74:61:6E:63:65:20:6F:66:20:74:68:65:20:74:68:65:6E:20:61:70:70:6C:69:63:61:62:6C:65:20:73:74:61:6E:64:61:72:64:20:74:65:72:6D:73:20:61:6E:64:20:63:6F:6E:64:69:74:69:6F:6E:73:20:6F:66:20:75:73:65:2C:20:63:65:72:74:69:66:69:63:61:74:65:20:70:6F:6C:69:63:79:20:61:6E:64:20:63:65:72:74:69:66:69:63:61:74:69:6F:6E:20:70:72:61:63:74:69:63:65:20:73:74:61:74:65:6D:65:6E:74:73:2E
[v3_intermediate]
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
[v3_img3_leaf]
basicConstraints = critical, CA:false
keyUsage = digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
# Apple IMG3 certificate constraint object for A6 production/Darwin images.
# This is public metadata, not an Apple key. The nested DER object constrains
# CEPO=0x10, SDOM=3, PROD=1 and CHIP=0x8950, matching the emulated
# 0x3f500000 fuse word (0x200d).
1.2.840.113635.100.6.1.1 = critical, DER:04:81:84:33:67:6d:49:84:00:00:00:70:00:00:00:00:00:00:00:74:72:65:63:4f:50:45:43:1c:00:00:00:04:00:00:00:10:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:4d:4f:44:53:1c:00:00:00:04:00:00:00:03:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:44:4f:52:50:1c:00:00:00:04:00:00:00:01:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:50:49:48:43:1c:00:00:00:04:00:00:00:50:89:00:00:00:00:00:00:00:00:00:00:00:00:00:00
[v3_ticket_leaf]
basicConstraints = critical, CA:false
keyUsage = digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
# A6 APTicket signing constraint. The original iBSS decoder requires the
# public ticket OID and constrains CHIP=0x8950, PROD=1 and SDOM=3. These are
# values of the emulated n41ap, while the certificate and key remain private
# to this lab identity.
1.2.840.113635.100.6.1.11 = critical, DER:30:1e:a1:1c:31:1a:82:04:50:89:00:00:84:04:01:00:00:00:85:04:03:00:00:00:9f:81:6b:04:10:00:00:00
+112
View File
@@ -0,0 +1,112 @@
#!/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)
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
usage()
{
echo "usage: $0 SECUREROM OUTPUT_DIRECTORY [BOOT_NONCE_HEX]" >&2
exit 2
}
[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage
source_rom=$1
output_dir=$2
boot_nonce=${3:-${A6_BOOT_NONCE:-8d82693c897d1b9d}}
ibss_payload=${A6_IBSS_PAYLOAD:-"$source_dir/firmware/iBSS.iphone5.RELEASE.bin"}
ibec_payload=${A6_IBEC_PAYLOAD:-"$source_dir/firmware/iBEC.iphone5.RELEASE.bin"}
for input in \
"$source_rom" \
"$source_dir/firmware/iBSS.iphone5.RELEASE.dfu" \
"$source_dir/firmware/iBEC.iphone5.RELEASE.dfu" \
"$ibss_payload" \
"$ibec_payload"; do
if [ ! -f "$input" ]; then
echo "required input not found: $input" >&2
exit 1
fi
done
python3 "$script_dir/a6-lab-identity.py" "$source_rom" "$output_dir"
output_dir=$(CDPATH= cd -- "$output_dir" && pwd)
python3 "$script_dir/a6-lab-patch-iboot-root.py" \
--source-rom "$source_rom" --identity "$output_dir" \
--input "$ibss_payload" \
--output "$output_dir/iBSS.lab-root.bin"
python3 "$script_dir/a6-lab-patch-iboot-root.py" \
--source-rom "$source_rom" --identity "$output_dir" \
--input "$ibec_payload" \
--output "$output_dir/iBEC.lab-root.bin"
python3 "$script_dir/a6-lab-img3.py" \
--template "$source_dir/firmware/iBSS.iphone5.RELEASE.dfu" \
--payload "$output_dir/iBSS.lab-root.bin" \
--identity "$output_dir" --encrypt \
--output "$output_dir/iBSS.chain-encrypted.dfu"
python3 "$script_dir/a6-lab-img3.py" \
--template "$source_dir/firmware/iBEC.iphone5.RELEASE.dfu" \
--payload "$output_dir/iBEC.lab-root.bin" \
--identity "$output_dir" --encrypt --ticketed \
--output "$output_dir/iBEC.ticketed-encrypted.img3"
python3 "$script_dir/a6-lab-apticket.py" \
--component "$output_dir/iBEC.ticketed-encrypted.img3" \
--identity "$output_dir" \
--boot-nonce "$boot_nonce" \
--ticket-output "$output_dir/apticket-nonce.der" \
--output "$output_dir/iBEC.chain-nonce-encrypted.dfu"
echo
echo "A6 lab chain created in $output_dir"
echo "Run: A6_LAB_DIR='$output_dir' ./Run-iPhone5-macOS.sh"
+136
View File
@@ -0,0 +1,136 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
build_root=${A6_RECOVERY_BUILD_DIR:-"$source_dir/build/macos-recovery"}
prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"}
downloads="$build_root/downloads"
libirecovery_commit=95dec3aa25b1e30654ca107eb971971f6a216520
libirecovery_sha256=fcd91f2d5c6c3d70bba5b6f790ae37e
libirecovery_sha256=${libirecovery_sha256}dcc2ff9419ac56de67770bb5f1cc2eca9
idevicerestore_commit=540c352c4c44896f7415abef87a166e8bbaea9b0
idevicerestore_sha256=d971449c0838fe6733e6cd18268ffa2e5
idevicerestore_sha256=${idevicerestore_sha256}499b8d9034e10cf2984c2c5835913b3
libirecovery_archive="$downloads/libirecovery-$libirecovery_commit.tar.gz"
idevicerestore_archive="$downloads/idevicerestore-$idevicerestore_commit.tar.gz"
github_base=https://github.com/libimobiledevice
libirecovery_url="$github_base/libirecovery/archive/$libirecovery_commit.tar.gz"
idevicerestore_path="idevicerestore/archive/$idevicerestore_commit.tar.gz"
idevicerestore_url="$github_base/$idevicerestore_path"
fail()
{
echo "error: $*" >&2
exit 1
}
for command_name in curl shasum tar patch autoreconf pkg-config make; do
command -v "$command_name" >/dev/null 2>&1 ||
fail "required command not found: $command_name"
done
if command -v brew >/dev/null 2>&1; then
brew_prefix=$(brew --prefix)
else
brew_prefix=/usr/local
fi
pkg_config_path="$prefix/lib/pkgconfig:$brew_prefix/lib/pkgconfig"
pkg_config_path="$pkg_config_path:$brew_prefix/opt/libusb/lib/pkgconfig"
if [ -n "${PKG_CONFIG_PATH:-}" ]; then
pkg_config_path="$pkg_config_path:$PKG_CONFIG_PATH"
fi
export PKG_CONFIG_PATH=$pkg_config_path
for package in \
libimobiledevice-glue-1.0 \
libimobiledevice-1.0 \
libusbmuxd-2.0 \
libplist-2.0 \
libtatsu-1.0 \
libzip \
libcurl \
zlib; do
pkg-config --exists "$package" || fail \
"missing $package dependency; run: brew install libimobiledevice libusb libzip"
done
mkdir -p "$downloads" "$prefix"
fetch_verified()
{
archive=$1
url=$2
expected=$3
temporary="$archive.tmp"
if [ -f "$archive" ]; then
actual=$(shasum -a 256 "$archive" | awk '{print $1}')
if [ "$actual" = "$expected" ]; then
return
fi
fi
echo "Downloading: $url"
curl -fsSL "$url" -o "$temporary"
actual=$(shasum -a 256 "$temporary" | awk '{print $1}')
[ "$actual" = "$expected" ] ||
fail "unexpected SHA-256 for $url: $actual"
mv "$temporary" "$archive"
}
fetch_verified "$libirecovery_archive" "$libirecovery_url" \
"$libirecovery_sha256"
fetch_verified "$idevicerestore_archive" "$idevicerestore_url" \
"$idevicerestore_sha256"
work_dir=$(mktemp -d "$build_root/source.XXXXXX")
case $work_dir in
"$build_root"/source.*) ;;
*) fail "unexpected temporary directory: $work_dir" ;;
esac
trap 'rm -rf "$work_dir"' EXIT HUP INT TERM
tar -xzf "$libirecovery_archive" -C "$work_dir"
tar -xzf "$idevicerestore_archive" -C "$work_dir"
libirecovery_source="$work_dir/libirecovery-$libirecovery_commit"
idevicerestore_source="$work_dir/idevicerestore-$idevicerestore_commit"
echo "Applying the QEMU backend to libirecovery"
patch -d "$libirecovery_source" -p1 \
-i "$script_dir/patches/libirecovery-qemu.patch"
printf '%s\n' '1.3.1-qemu-a6' > "$libirecovery_source/.tarball-version"
(
cd "$libirecovery_source"
autoreconf -fiv
./configure --prefix="$prefix"
make -j4
make install
)
printf '%s\n' '1.0.0-qemu-a6' > "$idevicerestore_source/.tarball-version"
echo "Applying boot-only mode without filesystem extraction"
patch -d "$idevicerestore_source" -p1 \
-i "$script_dir/patches/idevicerestore-no-restore-fs.patch"
echo "Applying QEMU boot support for a local lab identity"
patch -d "$idevicerestore_source" -p1 \
-i "$script_dir/patches/idevicerestore-qemu-lab-boot.patch"
(
cd "$idevicerestore_source"
autoreconf -fiv
./configure --prefix="$prefix"
make -j4
make install
)
echo
echo "Tools installed in $prefix/bin"
echo "Start QEMU with ./Run-iPhone5-macOS.sh, then run:"
echo " scripts/irecovery-qemu -q"
echo " scripts/idevicerestore-qemu -d -y /path/to/Restore.ipsw"
+199
View File
@@ -0,0 +1,199 @@
#!/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)
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"}
endpoint=${QEMU_USB_ENDPOINT:-127.0.0.1:26050}
lab_dir=${A6_LAB_DIR:-}
binary="$prefix/bin/idevicerestore"
if [ ! -x "$binary" ]; then
echo "QEMU-enabled idevicerestore not found: $binary" >&2
echo "Run scripts/build-macos-recovery-tools.sh first." >&2
exit 1
fi
export LIBIRECOVERY_QEMU=$endpoint
export DYLD_LIBRARY_PATH="$prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
if [ -n "$lab_dir" ]; then
lab_dir=$(CDPATH= cd -- "$lab_dir" && pwd)
for image in iBSS.chain-encrypted.dfu iBEC.chain-nonce-encrypted.dfu; do
if [ ! -f "$lab_dir/$image" ]; then
echo "Lab image not found: $lab_dir/$image" >&2
exit 1
fi
done
export IDEVICERESTORE_QEMU_LAB_DIR=$lab_dir
export IDEVICERESTORE_QEMU_LAB_BOOT_ONLY=1
exec "$binary" -z "$@"
fi
exec "$binary" "$@"
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"}
endpoint=${QEMU_USB_ENDPOINT:-127.0.0.1:26050}
binary="$prefix/bin/irecovery"
if [ ! -x "$binary" ]; then
echo "QEMU-enabled irecovery not found: $binary" >&2
echo "Run scripts/build-macos-recovery-tools.sh first." >&2
exit 1
fi
export LIBIRECOVERY_QEMU=$endpoint
export DYLD_LIBRARY_PATH="$prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
exec "$binary" "$@"
@@ -0,0 +1,12 @@
--- a/src/idevicerestore.c
+++ b/src/idevicerestore.c
@@ -1136,7 +1136,8 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
}
}
- if (needs_os_extraction && !(client->flags & FLAG_SHSHONLY)) {
+ if (needs_os_extraction && !(client->flags & FLAG_SHSHONLY) &&
+ !(client->flags & FLAG_NO_RESTORE)) {
char* tmpf = NULL;
struct stat st;
if (client->cache_dir) {
@@ -0,0 +1,116 @@
--- a/src/dfu.c
+++ b/src/dfu.c
@@ -127,6 +127,9 @@
int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_identity, const char* component)
{
char* path = NULL;
+ const char* qemu_lab_dir = getenv("IDEVICERESTORE_QEMU_LAB_DIR");
+ int qemu_lab_component = qemu_lab_dir && qemu_lab_dir[0] &&
+ (!strcmp(component, "iBSS") || !strcmp(component, "iBEC"));
// Use a specific TSS ticket for the Ap,LocalPolicy component
plist_t tss = client->tss;
@@ -137,7 +140,26 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide
void* component_data = NULL;
size_t component_size = 0;
- if (strcmp(component, "Ap,LocalPolicy") == 0) {
+ if (qemu_lab_component) {
+ const char* filename = !strcmp(component, "iBSS") ?
+ "iBSS.chain-encrypted.dfu" :
+ "iBEC.chain-nonce-encrypted.dfu";
+ size_t path_size = strlen(qemu_lab_dir) + strlen(filename) + 2;
+ path = malloc(path_size);
+ if (!path) {
+ logger(LL_ERROR, "Out of memory\n");
+ return -1;
+ }
+ snprintf(path, path_size, "%s/%s", qemu_lab_dir, filename);
+ if (read_file(path, &component_data, &component_size) < 0) {
+ logger(LL_ERROR, "Unable to read QEMU lab %s from %s\n", component, path);
+ free(path);
+ return -1;
+ }
+ logger(LL_INFO, "Using QEMU lab %s from %s\n", component, path);
+ free(path);
+ path = NULL;
+ } else if (strcmp(component, "Ap,LocalPolicy") == 0) {
// If Ap,LocalPolicy => Inject an empty policy
component_data = malloc(sizeof(lpol_file));
component_size = sizeof(lpol_file);
@@ -168,7 +190,11 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide
void* data = NULL;
size_t size = 0;
- if (personalize_component(client, component, component_data, component_size, tss, &data, &size) < 0) {
+ if (qemu_lab_component) {
+ data = component_data;
+ size = component_size;
+ component_data = NULL;
+ } else if (personalize_component(client, component, component_data, component_size, tss, &data, &size) < 0) {
logger(LL_ERROR, "Unable to get personalized component: %s\n", component);
free(component_data);
return -1;
@@ -176,7 +202,7 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide
free(component_data);
component_data = NULL;
- if (!client->image4supported && client->build_major > 8 && !(client->flags & FLAG_CUSTOM) && !strcmp(component, "iBEC")) {
+ if (!qemu_lab_component && !client->image4supported && client->build_major > 8 && !(client->flags & FLAG_CUSTOM) && !strcmp(component, "iBEC")) {
unsigned char* ticket = NULL;
unsigned int tsize = 0;
if (tss_response_get_ap_ticket(client->tss, &ticket, &tsize) < 0) {
@@ -517,7 +543,8 @@ int dfu_enter_recovery(struct idevicerestore_client_t* client, plist_t build_ide
logger(LL_INFO, "Nonce: ");
logger_dump_hex(LL_INFO, client->nonce, client->nonce_size);
- if (nonce_changed && !(client->flags & FLAG_CUSTOM)) {
+ if (nonce_changed && !(client->flags & FLAG_CUSTOM) &&
+ !getenv("IDEVICERESTORE_QEMU_LAB_DIR")) {
// Welcome iOS5. We have to re-request the TSS with our nonce.
plist_free(client->tss);
if (get_tss_response(client, build_identity, &client->tss) < 0) {
--- a/src/idevicerestore.c
+++ b/src/idevicerestore.c
@@ -682,6 +682,11 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
tss_enabled = 0;
logger(LL_INFO, "Custom firmware requested; TSS has been disabled.\n");
}
+ if (getenv("IDEVICERESTORE_QEMU_LAB_DIR")) {
+ /* The QEMU wrapper supplies locally personalized iBSS/iBEC images. */
+ tss_enabled = 0;
+ logger(LL_INFO, "QEMU lab boot requested; external TSS has been disabled.\n");
+ }
if (client->mode == MODE_RESTORE) {
if (!(client->flags & FLAG_ALLOW_RESTORE_MODE)) {
@@ -1111,11 +1116,16 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
/* check if all components we need are actually there */
logger(LL_INFO, "Checking IPSW for required components...\n");
- if (build_identity_check_components_in_ipsw(build_identity, client->ipsw) < 0) {
+ if (!getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY") &&
+ build_identity_check_components_in_ipsw(build_identity, client->ipsw) < 0) {
logger(LL_ERROR, "Could not find all required components in IPSW %s\n", client->ipsw->path);
return -1;
}
- logger(LL_INFO, "All required components found in IPSW\n");
+ if (getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY")) {
+ logger(LL_INFO, "QEMU lab boot only needs BuildManifest plus local iBSS/iBEC.\n");
+ } else {
+ logger(LL_INFO, "All required components found in IPSW\n");
+ }
/* Get OS (filesystem) name from build identity */
char* os_path = NULL;
@@ -1466,6 +1476,10 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
mutex_unlock(&client->device_event_mutex);
}
idevicerestore_progress(client, RESTORE_STEP_PREPARE, 0.5);
+ if (getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY")) {
+ logger(LL_INFO, "QEMU lab iBEC is running in Recovery mode.\n");
+ return 0;
+ }
if (client->flags & FLAG_QUIT) {
return -1;
}
+700
View File
@@ -0,0 +1,700 @@
--- a/src/libirecovery.c
+++ b/src/libirecovery.c
@@ -31,6 +31,12 @@
#include <ctype.h>
#include <unistd.h>
#include <sys/stat.h>
+#ifndef _WIN32
+#include <errno.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#endif
#include <libimobiledevice-glue/collection.h>
#include <libimobiledevice-glue/thread.h>
@@ -119,6 +125,11 @@
int usb_alt_interface;
unsigned int mode;
int isKIS;
+ int qemu_transport;
+ int qemu_fd;
+ uint32_t qemu_request_id;
+ uint32_t qemu_generation;
+ uint32_t qemu_poll_timeout_ms;
struct irecv_device_info device_info;
#ifndef USE_DUMMY
#ifndef _WIN32
@@ -163,6 +173,214 @@
#define debug(...) if (libirecovery_debug) fprintf(stderr, __VA_ARGS__)
static int libirecovery_debug = 0;
+
+#ifndef _WIN32
+#define QA6_USB_MAGIC 0x55364151u
+#define QA6_USB_PROTOCOL_VERSION 1u
+#define QA6_USB_MESSAGE_REQUEST 1u
+#define QA6_USB_MESSAGE_RESPONSE 2u
+#define QA6_USB_MESSAGE_INFO_REQUEST 3u
+#define QA6_USB_MESSAGE_INFO_RESPONSE 4u
+#define QA6_USB_MESSAGE_BULK_REQUEST 5u
+#define QA6_USB_MESSAGE_BULK_RESPONSE 6u
+#define QA6_USB_MESSAGE_RESET_REQUEST 7u
+#define QA6_USB_MESSAGE_RESET_RESPONSE 8u
+#define QA6_USB_STATUS_SUCCESS 0
+#define QA6_USB_STATUS_STALL -1
+#define QA6_USB_STATUS_DISCONNECTED -2
+#define QA6_USB_STATUS_PROTOCOL -3
+#define QA6_USB_STATUS_UNSUPPORTED -4
+#define QA6_USB_MAX_PAYLOAD 65535u
+#define QA6_USB_SERIAL_MAX 256u
+#define QA6_USB_DFU_STATE_ERROR 10u
+#define QA6_USB_DFU_STATE_WAIT_RESET 8u
+
+#pragma pack(push, 1)
+typedef struct qemu_a6_usb_frame_header {
+ uint32_t magic;
+ uint16_t version;
+ uint16_t type;
+ uint32_t request_id;
+ int32_t status;
+ uint32_t payload_length;
+ uint32_t transfer_length;
+ uint8_t setup[8];
+} qemu_a6_usb_frame_header;
+
+typedef struct qemu_a6_usb_device_info {
+ uint32_t generation;
+ uint16_t vendor_id;
+ uint16_t product_id;
+ uint8_t device_class;
+ uint8_t device_subclass;
+ uint8_t device_protocol;
+ uint8_t is_dfu;
+ char serial[QA6_USB_SERIAL_MAX];
+} qemu_a6_usb_device_info;
+#pragma pack(pop)
+
+static const char *qemu_a6_endpoint(void)
+{
+ const char *endpoint = getenv("LIBIRECOVERY_QEMU");
+
+ return endpoint && endpoint[0] ? endpoint : NULL;
+}
+
+static int qemu_a6_send_all(int fd, const void *data, size_t length)
+{
+ const uint8_t *cursor = data;
+
+ while (length) {
+ ssize_t sent = send(fd, cursor, length, 0);
+ if (sent < 0 && errno == EINTR) {
+ continue;
+ }
+ if (sent <= 0) {
+ return -1;
+ }
+ cursor += sent;
+ length -= sent;
+ }
+ return 0;
+}
+
+static int qemu_a6_recv_all(int fd, void *data, size_t length)
+{
+ uint8_t *cursor = data;
+
+ while (length) {
+ ssize_t received = recv(fd, cursor, length, 0);
+ if (received < 0 && errno == EINTR) {
+ continue;
+ }
+ if (received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
+ return -2;
+ }
+ if (received <= 0) {
+ return -1;
+ }
+ cursor += received;
+ length -= received;
+ }
+ return 0;
+}
+
+static int qemu_a6_connect(void)
+{
+ const char *endpoint = qemu_a6_endpoint();
+ char host[256];
+ char port[16];
+ const char *separator;
+ struct addrinfo hints;
+ struct addrinfo *addresses = NULL;
+ struct addrinfo *address;
+ struct timeval timeout = { 1, 0 };
+ int fd = -1;
+
+ if (!endpoint || !(separator = strrchr(endpoint, ':')) ||
+ separator == endpoint || !separator[1] ||
+ (size_t)(separator - endpoint) >= sizeof(host) ||
+ strlen(separator + 1) >= sizeof(port)) {
+ return -1;
+ }
+ memcpy(host, endpoint, separator - endpoint);
+ host[separator - endpoint] = '\0';
+ strcpy(port, separator + 1);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ if (getaddrinfo(host, port, &hints, &addresses) != 0) {
+ return -1;
+ }
+ for (address = addresses; address; address = address->ai_next) {
+ fd = socket(address->ai_family, address->ai_socktype,
+ address->ai_protocol);
+ if (fd < 0) {
+ continue;
+ }
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+ if (connect(fd, address->ai_addr, address->ai_addrlen) == 0) {
+ break;
+ }
+ close(fd);
+ fd = -1;
+ }
+ freeaddrinfo(addresses);
+ return fd;
+}
+
+static int qemu_a6_exchange(int fd, qemu_a6_usb_frame_header *request,
+ const void *out_data,
+ qemu_a6_usb_frame_header *response,
+ void *in_data, size_t in_capacity)
+{
+ int receive_result;
+
+ if (qemu_a6_send_all(fd, request, sizeof(*request)) < 0 ||
+ (request->payload_length &&
+ qemu_a6_send_all(fd, out_data, request->payload_length) < 0)) {
+ return IRECV_E_NO_DEVICE;
+ }
+ receive_result = qemu_a6_recv_all(fd, response, sizeof(*response));
+ if (receive_result == -2) {
+ return IRECV_E_TIMEOUT;
+ }
+ if (receive_result < 0) {
+ return IRECV_E_NO_DEVICE;
+ }
+ if (response->magic != QA6_USB_MAGIC ||
+ response->version != QA6_USB_PROTOCOL_VERSION ||
+ response->request_id != request->request_id ||
+ response->payload_length > QA6_USB_MAX_PAYLOAD ||
+ response->payload_length > in_capacity) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ if (response->payload_length &&
+ qemu_a6_recv_all(fd, in_data, response->payload_length) < 0) {
+ return IRECV_E_NO_DEVICE;
+ }
+ switch (response->status) {
+ case QA6_USB_STATUS_SUCCESS:
+ return IRECV_E_SUCCESS;
+ case QA6_USB_STATUS_STALL:
+ return IRECV_E_PIPE;
+ case QA6_USB_STATUS_DISCONNECTED:
+ return IRECV_E_NO_DEVICE;
+ case QA6_USB_STATUS_UNSUPPORTED:
+ return IRECV_E_UNSUPPORTED;
+ case QA6_USB_STATUS_PROTOCOL:
+ default:
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+}
+
+static int qemu_a6_read_info_fd(int fd, qemu_a6_usb_device_info *info)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ int error;
+
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_INFO_REQUEST;
+ request.request_id = 1;
+ error = qemu_a6_exchange(fd, &request, NULL, &response,
+ info, sizeof(*info));
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_INFO_RESPONSE ||
+ response.payload_length != sizeof(*info) ||
+ response.transfer_length != sizeof(*info)) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ info->serial[sizeof(info->serial) - 1] = '\0';
+ return IRECV_E_SUCCESS;
+}
+#endif
#ifndef USE_DUMMY
#ifndef _WIN32
#ifndef HAVE_IOKIT
@@ -930,6 +1149,199 @@
}
}
+#ifndef _WIN32
+static irecv_error_t qemu_a6_open_with_ecid(irecv_client_t *pclient,
+ uint64_t ecid)
+{
+ qemu_a6_usb_device_info info;
+ irecv_client_t client;
+ int fd;
+ int error;
+
+ *pclient = NULL;
+ fd = qemu_a6_connect();
+ if (fd < 0) {
+ return IRECV_E_UNABLE_TO_CONNECT;
+ }
+ memset(&info, 0, sizeof(info));
+ error = qemu_a6_read_info_fd(fd, &info);
+ if (error != IRECV_E_SUCCESS) {
+ close(fd);
+ return error;
+ }
+ if (info.vendor_id != APPLE_VENDOR_ID ||
+ (info.product_id != IRECV_K_DFU_MODE &&
+ info.product_id != IRECV_K_WTF_MODE &&
+ info.product_id != IRECV_K_PORT_DFU_MODE &&
+ (info.product_id < IRECV_K_RECOVERY_MODE_1 ||
+ info.product_id > IRECV_K_RECOVERY_MODE_4))) {
+ close(fd);
+ return IRECV_E_NO_DEVICE;
+ }
+
+ client = calloc(1, sizeof(*client));
+ if (!client) {
+ close(fd);
+ return IRECV_E_OUT_OF_MEMORY;
+ }
+ client->qemu_transport = 1;
+ client->qemu_fd = fd;
+ client->qemu_request_id = 1;
+ client->qemu_generation = info.generation;
+ client->mode = info.product_id;
+ client->usb_interface = 0;
+ irecv_load_device_info_from_iboot_string(client, info.serial);
+ if (ecid && ecid != IRECV_K_WTF_MODE &&
+ client->device_info.ecid != ecid) {
+ irecv_close(client);
+ return IRECV_E_NO_DEVICE;
+ }
+ debug("opening QEMU A6 device %04x:%04x generation %u...\n",
+ info.vendor_id, info.product_id, info.generation);
+ *pclient = client;
+ return IRECV_E_SUCCESS;
+}
+
+static int qemu_a6_control_transfer(irecv_client_t client,
+ uint8_t bm_request_type,
+ uint8_t b_request, uint16_t w_value,
+ uint16_t w_index, unsigned char *data,
+ uint16_t w_length)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ int direction_in = (bm_request_type & 0x80) != 0;
+ int error;
+
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_REQUEST;
+ request.request_id = ++client->qemu_request_id;
+ request.payload_length = direction_in ? 0 : w_length;
+ request.transfer_length = w_length;
+ request.setup[0] = bm_request_type;
+ request.setup[1] = b_request;
+ request.setup[2] = w_value & 0xff;
+ request.setup[3] = w_value >> 8;
+ request.setup[4] = w_index & 0xff;
+ request.setup[5] = w_index >> 8;
+ request.setup[6] = w_length & 0xff;
+ request.setup[7] = w_length >> 8;
+ error = qemu_a6_exchange(client->qemu_fd, &request,
+ direction_in ? NULL : data, &response,
+ direction_in ? data : NULL,
+ direction_in ? w_length : 0);
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_RESPONSE ||
+ response.transfer_length > w_length) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ if (bm_request_type == 0xa1 && b_request == 3 && data &&
+ response.transfer_length >= 4) {
+ client->qemu_poll_timeout_ms = data[1] |
+ (data[2] << 8) | (data[3] << 16);
+ }
+ return response.transfer_length;
+}
+
+static int qemu_a6_bulk_transfer(irecv_client_t client,
+ unsigned char endpoint,
+ unsigned char *data, int length,
+ int *transferred, unsigned int timeout)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ int direction_in = (endpoint & 0x80) != 0;
+ int error;
+
+ if (length < 0 || (unsigned int)length > QA6_USB_MAX_PAYLOAD) {
+ return IRECV_E_INVALID_INPUT;
+ }
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_BULK_REQUEST;
+ request.request_id = ++client->qemu_request_id;
+ request.payload_length = direction_in ? 0 : length;
+ request.transfer_length = length;
+ request.setup[0] = endpoint;
+ request.setup[1] = timeout & 0xff;
+ request.setup[2] = (timeout >> 8) & 0xff;
+ request.setup[3] = (timeout >> 16) & 0xff;
+ request.setup[4] = (timeout >> 24) & 0xff;
+ error = qemu_a6_exchange(client->qemu_fd, &request,
+ direction_in ? NULL : data, &response,
+ direction_in ? data : NULL,
+ direction_in ? length : 0);
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_BULK_RESPONSE ||
+ response.transfer_length > (unsigned int)length) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ *transferred = response.transfer_length;
+ return IRECV_E_SUCCESS;
+}
+
+static irecv_error_t qemu_a6_reset(irecv_client_t client)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ uint8_t dfu_status[6];
+ int retry = 0;
+ int error;
+ while (client->qemu_poll_timeout_ms && retry++ < 5) {
+ uint32_t delay_ms = client->qemu_poll_timeout_ms > 10000 ?
+ 10000 : client->qemu_poll_timeout_ms;
+ debug("honoring QEMU DFU poll timeout: %u ms\n", delay_ms);
+ usleep((useconds_t)delay_ms * 1000);
+ client->qemu_poll_timeout_ms = 0;
+ memset(dfu_status, 0, sizeof(dfu_status));
+ error = qemu_a6_control_transfer(client, 0xa1, 3, 0, 0,
+ dfu_status, sizeof(dfu_status));
+ if (error != sizeof(dfu_status)) {
+ return error < 0 ? error : IRECV_E_USB_STATUS;
+ }
+ debug("QEMU DFU manifestation state=%u status=%u\n",
+ dfu_status[4], dfu_status[0]);
+ if (dfu_status[0] || dfu_status[4] == QA6_USB_DFU_STATE_ERROR) {
+ return IRECV_E_USB_UPLOAD;
+ }
+ if (dfu_status[4] == QA6_USB_DFU_STATE_WAIT_RESET) {
+ client->qemu_poll_timeout_ms = 0;
+ break;
+ }
+ }
+ if (client->qemu_poll_timeout_ms) {
+ return IRECV_E_TIMEOUT;
+ }
+ if (getenv("LIBIRECOVERY_QEMU_NO_RESET")) {
+ debug("leaving QEMU in DFU-WAIT-RESET for debugging\n");
+ return IRECV_E_SUCCESS;
+ }
+
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_RESET_REQUEST;
+ request.request_id = ++client->qemu_request_id;
+ error = qemu_a6_exchange(client->qemu_fd, &request, NULL, &response,
+ NULL, 0);
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_RESET_RESPONSE ||
+ response.payload_length || response.transfer_length) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ return IRECV_E_SUCCESS;
+}
+#endif
+
static void irecv_copy_nonce_with_tag_from_buffer(const char* tag, unsigned char** nonce, unsigned int* nonce_size, const char *buf)
{
int taglen = strlen(tag);
@@ -1006,6 +1350,11 @@
*nonce = NULL;
*nonce_size = 0;
+ if (client->qemu_transport) {
+ irecv_copy_nonce_with_tag_from_buffer(
+ tag, nonce, nonce_size, client->device_info.serial_string);
+ return;
+ }
memset(buf, 0, sizeof(buf));
len = irecv_get_string_descriptor_ascii(client, 1, (unsigned char*)buf, sizeof(buf)-1);
@@ -1406,7 +1755,8 @@
static int check_context(irecv_client_t client)
{
- if (client == NULL || client->handle == NULL) {
+ if (client == NULL ||
+ (!client->qemu_transport && client->handle == NULL)) {
return IRECV_E_NO_DEVICE;
}
@@ -1456,6 +1806,12 @@
return IRECV_E_UNSUPPORTED;
#else
#ifndef _WIN32
+ if (client->qemu_transport) {
+ return qemu_a6_control_transfer(client, bm_request_type, b_request,
+ w_value, w_index, data, w_length);
+ }
+#endif
+#ifndef _WIN32
#ifdef HAVE_IOKIT
return iokit_usb_control_transfer(client, bm_request_type, b_request, w_value, w_index, data, w_length, timeout);
#else
@@ -1813,6 +2169,10 @@
int ret;
#ifndef _WIN32
+ if (client->qemu_transport) {
+ return qemu_a6_bulk_transfer(client, endpoint, data, length,
+ transferred, timeout);
+ }
#ifdef HAVE_IOKIT
return iokit_usb_bulk_transfer(client, endpoint, data, length, transferred, timeout);
#else
@@ -2144,11 +2504,15 @@
irecv_set_debug_level(libirecovery_debug);
}
#ifndef _WIN32
+ if (qemu_a6_endpoint()) {
+ error = qemu_a6_open_with_ecid(pclient, ecid);
+ } else {
#ifdef HAVE_IOKIT
- error = iokit_open_with_ecid(pclient, ecid);
+ error = iokit_open_with_ecid(pclient, ecid);
#else
- error = libusb_open_with_ecid(pclient, ecid);
+ error = libusb_open_with_ecid(pclient, ecid);
#endif
+ }
#else
error = win32_open_with_ecid(pclient, ecid);
#endif
@@ -2166,12 +2530,14 @@
}
#ifdef HAVE_IOKIT
- error = (*client->handle)->CreateDeviceAsyncEventSource(client->handle, &client->async_event_source);
- if (error != IRECV_E_SUCCESS) {
- free(client);
- return error;
+ if (!client->qemu_transport) {
+ error = (*client->handle)->CreateDeviceAsyncEventSource(client->handle, &client->async_event_source);
+ if (error != IRECV_E_SUCCESS) {
+ free(client);
+ return error;
+ }
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), client->async_event_source, kCFRunLoopDefaultMode);
}
- CFRunLoopAddSource(CFRunLoopGetCurrent(), client->async_event_source, kCFRunLoopDefaultMode);
#endif
if (client->mode == IRECV_K_DFU_MODE || client->mode == IRECV_K_PORT_DFU_MODE || client->mode == IRECV_K_WTF_MODE || client->mode == KIS_PRODUCT_ID) {
@@ -2237,6 +2603,10 @@
#ifndef _WIN32
debug("Setting to configuration %d\n", configuration);
+ if (client->qemu_transport) {
+ client->usb_config = configuration;
+ return IRECV_E_SUCCESS;
+ }
#ifdef HAVE_IOKIT
IOReturn result;
@@ -2355,6 +2725,17 @@
debug("Setting to interface %d:%d\n", usb_interface, usb_alt_interface);
#ifndef _WIN32
+ if (client->qemu_transport) {
+ if (usb_interface == 1 &&
+ qemu_a6_control_transfer(client, 0x01, 0x0B,
+ usb_alt_interface, usb_interface,
+ NULL, 0) < 0) {
+ return IRECV_E_USB_INTERFACE;
+ }
+ client->usb_interface = usb_interface;
+ client->usb_alt_interface = usb_alt_interface;
+ return IRECV_E_SUCCESS;
+ }
#ifdef HAVE_IOKIT
if (iokit_usb_set_interface(client, usb_interface, usb_alt_interface) < 0) {
return IRECV_E_USB_INTERFACE;
@@ -2393,6 +2768,9 @@
return IRECV_E_NO_DEVICE;
#ifndef _WIN32
+ if (client->qemu_transport) {
+ return qemu_a6_reset(client);
+ }
#ifdef HAVE_IOKIT
IOReturn result;
@@ -2875,6 +3253,44 @@
}
#ifndef _WIN32
+static struct irecv_usb_device_info *qemu_a6_handle_device_add(
+ const qemu_a6_usb_device_info *info)
+{
+ struct irecv_client_private client_loc;
+ struct irecv_usb_device_info *usb_dev_info;
+ irecv_device_event_t dev_event;
+
+ memset(&client_loc, 0, sizeof(client_loc));
+ client_loc.mode = info->product_id;
+ irecv_load_device_info_from_iboot_string(&client_loc, info->serial);
+ usb_dev_info = calloc(1, sizeof(*usb_dev_info));
+ if (!usb_dev_info) {
+ free(client_loc.device_info.srnm);
+ free(client_loc.device_info.imei);
+ free(client_loc.device_info.srtg);
+ free(client_loc.device_info.serial_string);
+ return NULL;
+ }
+ memcpy(&usb_dev_info->device_info, &client_loc.device_info,
+ sizeof(client_loc.device_info));
+ usb_dev_info->location = info->generation;
+ usb_dev_info->alive = 1;
+ usb_dev_info->mode = client_loc.mode;
+ collection_add(&devices, usb_dev_info);
+
+ dev_event.type = IRECV_DEVICE_ADD;
+ dev_event.mode = client_loc.mode;
+ dev_event.device_info = &usb_dev_info->device_info;
+ mutex_lock(&listener_mutex);
+ FOREACH(struct irecv_device_event_context* context, &listeners) {
+ context->callback(&dev_event, context->user_data);
+ } ENDFOREACH
+ mutex_unlock(&listener_mutex);
+ return usb_dev_info;
+}
+#endif
+
+#ifndef _WIN32
#ifdef HAVE_IOKIT
static void iokit_device_added(void *refcon, io_iterator_t iterator)
{
@@ -2977,10 +3393,70 @@
cond_t startup_cond;
mutex_t startup_mutex;
};
+
+#ifndef _WIN32
+static void *qemu_a6_event_handler(struct _irecv_event_handler_info *startup)
+{
+ struct irecv_usb_device_info *current = NULL;
+
+ mutex_lock(&startup->startup_mutex);
+ cond_signal(&startup->startup_cond);
+ mutex_unlock(&startup->startup_mutex);
+
+ for (;;) {
+ qemu_a6_usb_device_info info;
+ int listeners_active;
+ int fd;
+ int error = IRECV_E_UNABLE_TO_CONNECT;
+
+ mutex_lock(&listener_mutex);
+ listeners_active = collection_count(&listeners) != 0;
+ mutex_unlock(&listener_mutex);
+ if (!listeners_active) {
+ break;
+ }
+
+ fd = qemu_a6_connect();
+ if (fd >= 0) {
+ memset(&info, 0, sizeof(info));
+ error = qemu_a6_read_info_fd(fd, &info);
+ close(fd);
+ }
+ if (error == IRECV_E_SUCCESS) {
+ if (current &&
+ (current->location != info.generation ||
+ current->mode != info.product_id)) {
+ /*
+ * Publish removal and arrival on separate polling turns.
+ * idevicerestore deliberately waits for MODE_UNKNOWN before
+ * it accepts the next DFU/recovery attachment.
+ */
+ _irecv_handle_device_remove(current);
+ current = NULL;
+ } else if (!current) {
+ current = qemu_a6_handle_device_add(&info);
+ }
+ } else if (error == IRECV_E_NO_DEVICE && current) {
+ _irecv_handle_device_remove(current);
+ current = NULL;
+ }
+ usleep(250000);
+ }
+ if (current) {
+ _irecv_handle_device_remove(current);
+ }
+ return NULL;
+}
+#endif
static void *_irecv_event_handler(void* data)
{
struct _irecv_event_handler_info* info = (struct _irecv_event_handler_info*)data;
+#ifndef _WIN32
+ if (qemu_a6_endpoint()) {
+ return qemu_a6_event_handler(info);
+ }
+#endif
#ifdef _WIN32
struct collection newDevices;
const GUID *guids[] = { &GUID_DEVINTERFACE_KIS, &GUID_DEVINTERFACE_PORTDFU, &GUID_DEVINTERFACE_DFU, &GUID_DEVINTERFACE_IBOOT, NULL };
@@ -3381,6 +3858,10 @@
client->disconnected_callback(client, &event);
}
#ifndef _WIN32
+ if (client->qemu_transport) {
+ close(client->qemu_fd);
+ client->qemu_fd = -1;
+ } else {
#ifdef HAVE_IOKIT
if (client->usbInterface) {
(*client->usbInterface)->USBInterfaceClose(client->usbInterface);
@@ -3405,6 +3886,7 @@
client->handle = NULL;
}
#endif
+ }
#else
CloseHandle(client->handle);
#endif