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:
Executable
+148
@@ -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)
|
||||
Executable
+263
@@ -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)
|
||||
Executable
+288
@@ -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)
|
||||
Executable
+354
@@ -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)
|
||||
@@ -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
|
||||
Executable
+112
@@ -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)
|
||||
Executable
+66
@@ -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"
|
||||
Executable
+136
@@ -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"
|
||||
Executable
+199
@@ -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)
|
||||
Executable
+33
@@ -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" "$@"
|
||||
Executable
+20
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user