hw/arm: add authenticated A6 IMG3 boot lab

Model the A6 crypto, interrupt, USB, and platform blocks needed to boot SecureROM through iBSS into iBEC Recovery.

Add local lab identity, IMG3, and APTicket tooling, patched macOS recovery utilities, UART and GDB access, and English end-user documentation.
This commit is contained in:
2026-09-01 09:51:49 -07:00
parent 47977dd34a
commit 5d9a60a926
45 changed files with 6002 additions and 265 deletions
+9
View File
@@ -2,6 +2,11 @@
/build/
/target/
/firmware/
/.DS_Store
/.irecovery
/iBSS
/iBEC
/restore_*.log
/windows/QemuA6Ude/build/
/windows/QemuA6Ude/cert/
/windows/QemuA6Ude/package/
@@ -10,6 +15,7 @@
/.cache/
/.vscode/
*.pyc
__pycache__/
.sdk
.stgit-*
.git-submodule-status
@@ -26,4 +32,7 @@ GTAGS
*.depend_raw
*.swp
*.patch
!scripts/patches/libirecovery-qemu.patch
!scripts/patches/idevicerestore-no-restore-fs.patch
!scripts/patches/idevicerestore-qemu-lab-boot.patch
*.gcov
+341 -86
View File
@@ -1,111 +1,366 @@
# iPhone 5 / Apple A6 sur QEMU Windows natif
# Apple A6 / iPhone 5 secure-boot lab
## État actuel
This repository contains an experimental QEMU `iphone5` machine for studying
the A6 IMG3 boot path in an isolated environment. It runs the guest
SecureROM, iBSS, and iBEC code with a locally generated root of trust and a
locally generated AES-256 GID key. The verified endpoint is iBEC Recovery
mode; booting a ramdisk or iOS is not implemented.
- QEMU `arm-softmmu` est compilé en PE Win64 natif avec MSYS2 MinGW64. WSL nest pas utilisé.
- La machine `iphone5` exécute la SecureROM A6 `iBoot-1145.3` de 65 536 octets.
- Les fusibles exposent `CPID:8950`, `CPRV:20` et `CPFM:03`.
- LUART A6 est relié à la console QEMU (`Ctrl+A`, puis `C` pour le moniteur ; `Ctrl+A`, puis `X` pour quitter).
- Le contrôleur USB OTG, son EP0, les DMA et lIRQ 11 permettent à la SecureROM de terminer son énumération DFU.
- Le descripteur produit par cette SecureROM annonce `VID=05AC, PID=1229`. La façade Windows le normalise vers le PID DFU public `1227`, attendu par `libirecovery`.
- Le pilote KMDF/UDE `QemuA6Ude.sys` crée un véritable enfant PnP `USB\VID_05AC&PID_1227` sous Windows.
- `QemuA6UsbBridge.exe` relaie les contrôles DFU entre le pilote et QEMU sur `127.0.0.1:26050`.
- Un test externe `DFU_GETSTATE` a traversé le socket, le modèle OTG et la SecureROM, puis a renvoyé `dfuIDLE` (`2`).
- `idevicerestore.exe` et `libirecovery` restent dorigine et ne sont pas patchés.
No Apple private key or GID key is included or required. Firmware files and
generated lab identities are intentionally ignored by Git.
Le paquet pilote signé en mode test est généré localement dans
`windows\QemuA6Ude\package` ; les clés, certificats et binaires générés ne sont
pas versionnés.
## Verified flow
## Construction Windows native
The following path has been tested on macOS:
QEMU se configure depuis un terminal MSYS2 MinGW64 :
1. The 64 KiB A6 SecureROM starts in DFU mode.
2. Patched `idevicerestore` selects the `n41ap` build identity from
`BuildManifest.plist` and sends the locally signed and encrypted iBSS.
3. Guest SecureROM parses the IMG3, validates its RSA chain and signature,
asks the emulated AES engine to unwrap the KBAG with the local GID slot,
and decrypts `DATA`.
4. iBSS parses and validates the local APTicket, including its device fields,
boot-nonce binding, certificate chain, signature, and iBEC digest.
5. iBSS unwraps and decrypts iBEC. QEMU performs a warm authenticated handoff
of the resulting image to its linked address at `0xbff00000`.
6. iBEC prints its real `iBootStage2` banner on UART and enumerates as
Recovery (`05ac:1281`). `irecovery -q` reports `MODE: Recovery` and
`IBFL: 0x1a`.
This is not a cycle-accurate reproduction of Apple silicon. The original
guest IMG3 and APTicket parsers execute, but the root and GID material are
lab-owned, crypto is provided by QEMU MMIO models, debug UART activation is
optionally assisted after authentication, and the final iBEC relocation is a
QEMU handoff. Timing, analog behavior, fault behavior, and unimplemented
peripherals must not be treated as hardware-equivalent.
## Host requirements
On macOS, install the QEMU build dependencies and the recovery-tool
dependencies. A typical Homebrew setup is:
```sh
./configure --target-list=arm-softmmu
ninja -C build qemu-system-arm.exe
brew install ninja pkg-config glib pixman libslirp \
autoconf automake libtool libimobiledevice libusb libzip
```
Le pilote UDE nécessite Visual Studio 2022 Build Tools et le WDK avec UDE. Si
le WDK n'est pas installé à son emplacement standard, définir
`QA6_WDK_ROOT` vers sa racine (celle qui contient `Include`, `bin` et `tools`).
Depuis PowerShell :
Python 3 and OpenSSL are required by the image-generation scripts. You must
also provide your own 65,536-byte A6 SecureROM dump. The repository does not
distribute it.
```powershell
.\windows\QemuA6Ude\prepare-package.ps1
.\windows\QemuA6UsbBridge\build-bridge.ps1 -Configuration Release
```
## Build QEMU
## Première installation du pilote
La signature dun pilote noyau local exige le mode de signature test Windows. Cette opération modifie le BCD et demande un redémarrage. Depuis un PowerShell administrateur :
```powershell
.\windows\QemuA6Ude\enable-test-mode.ps1
Restart-Computer
```
Après le redémarrage, toujours en administrateur :
```powershell
.\windows\QemuA6Ude\install-driver.ps1
```
Si Secure Boot bloque `testsigning`, il faut le désactiver dans lUEFI. Le script dinstallation importe uniquement le certificat public de test puis crée le contrôleur racine `Root\QemuA6Ude` avec `devcon`.
## Lancement normal
Depuis PowerShell à la racine :
```powershell
.\Run-iPhone5.ps1 -SkipBuild
```
Le mode USB Windows est actif par défaut. Le lanceur démarre le pont en arrière-plan, garde lUART dans la console et écrit les échanges USB dans `build\a6-usb-bridge.log`.
Vérification dans un second PowerShell :
```powershell
pnputil /enum-devices /connected /class USB
& 'C:\msys64\mingw64\bin\idevicerestore.exe' -d 'iPhone_4.0_32bit_10.3.4_14G61_Restore.ipsw'
```
Pour conserver lancien injecteur DFU interne, fournir directement une image :
```powershell
.\Run-iPhone5.ps1 -DfuImagePath '.\build\firmware\iBSS.iphone5.RELEASE.dfu' -Trace
```
Pour démarrer sans façade USB Windows :
```powershell
.\Run-iPhone5.ps1 -NoWindowsUsb
```
## Prochain jalon
Une fois l’énumération PnP confirmée, lIPSW iOS 10.3.4 sera envoyé par l`idevicerestore` original. La SecureROM vérifiera liBSS ; après son exécution, il faudra modéliser les périphériques supplémentaires utilisés par iBSS/iBEC et gérer sa réénumération USB Recovery pour atteindre iBoot.
## Reprise sur macOS
Le fork QEMU et linjecteur DFU interne sont portables. Sur macOS, installez les
dépendances QEMU, configurez `arm-softmmu`, puis compilez :
From the repository root:
```sh
./configure --target-list=arm-softmmu
ninja -C build qemu-system-arm
```
Placez ensuite le dump dans `firmware/s5l8950x-secure-rom.bin`, puis lancez :
The launcher expects `build/qemu-system-arm`. Set `QEMU_BIN` if the binary is
elsewhere.
## Fetch the public bootloader files
The helper reads the iOS 10.3.4 IPSW ZIP directory with HTTP Range requests
and downloads only `BuildManifest.plist`, iBSS, and iBEC:
```sh
./Run-iPhone5-macOS.sh
scripts/fetch-a6-bootloaders.py --board n41
```
Pour charger liBSS extrait de lIPSW sans dépendre dun périphérique USB hôte :
The following files should then exist locally:
```text
firmware/BuildManifest.plist
firmware/iBSS.iphone5.RELEASE.dfu
firmware/iBSS.iphone5.RELEASE.bin
firmware/iBEC.iphone5.RELEASE.dfu
firmware/iBEC.iphone5.RELEASE.bin
```
The fetcher validates HTTP ranges, uncompressed sizes, and CRC-32 values. It
does not personalize, decrypt, sign, or modify the downloaded images.
## Build the macOS recovery tools
macOS does not expose the emulator as a system-wide USB device. Instead, the
QEMU USB model is connected over a localhost transport implemented in the
patched `libirecovery`. The normal IOKit backend remains unchanged when the
transport environment variable is absent.
Build the pinned and checksum-verified recovery tools locally:
```sh
DFU_IMAGE="$PWD/firmware/iBSS.n41.RELEASE.dfu" ./Run-iPhone5-macOS.sh
scripts/build-macos-recovery-tools.sh
```
La façade UDE est propre à Windows. Faire apparaître le téléphone comme un vrai périphérique USB sur macOS demanderait une extension DriverKit signée ; ce nest donc pas un raccourci par rapport au chemin UDE déjà construit.
They are installed under `build/limd-prefix`; nothing is installed into
`/usr/local`. Always use the repository wrappers:
```sh
scripts/irecovery-qemu
scripts/idevicerestore-qemu
```
The QEMU endpoint defaults to `127.0.0.1:26050`. Change both sides with
`USB_BRIDGE_PORT` and `QEMU_USB_ENDPOINT` when running concurrent instances.
## Generate a new lab identity and boot chain
Create an empty output directory containing a new RSA hierarchy, a random
AES-256 GID key, a derived SecureROM, signed IMG3 files, encrypted payloads,
and a locally signed APTicket:
```sh
scripts/build-a6-lab-chain.sh \
"/absolute/path/to/SecureROM for s5l8950xsi, iBoot-1145.3" \
build/a6-lab
```
The output directory must be new or empty. Private PEM files and
`gid-key.bin` are created with owner-only permissions. Do not commit or share
that directory. The default ticket nonce is the value produced by the
current deterministic A6 lab boot (`8d82693c897d1b9d`). The optional third
argument exists for experiments in which the guest nonce is changed.
Start QEMU in the first terminal:
```sh
A6_LAB_DIR="$PWD/build/a6-lab" GDB_PORT=12345 \
./Run-iPhone5-macOS.sh
```
Boot iBSS and iBEC in a second terminal:
```sh
A6_LAB_DIR="$PWD/build/a6-lab" \
scripts/idevicerestore-qemu -d -y -P firmware
```
This wrapper automatically enables the explicit QEMU lab boot-only path. It
does not contact a TSS server and stops after iBEC reaches Recovery; it does
not restore a filesystem. Confirm the result with:
```sh
scripts/irecovery-qemu -q
```
Expected identifying fields include:
```text
CPID: 0x8950
ECID: 0x0000000000200000
IBFL: 0x1a
MODE: Recovery
PRODUCT: iPhone5,1
MODEL: n41ap
```
## UART console
UART0 is mapped at `0x32b00000` and connected to QEMU's standard console by
`-serial mon:stdio`. Run the launcher directly from the terminal in which you
want to see serial output. No extra serial client is needed.
Useful console controls are:
- `Ctrl+A`, then `C`: switch between UART and the QEMU monitor.
- `Ctrl+A`, then `X`: quit QEMU.
Release iBSS/iBEC images do not always enable serial output themselves. The
launcher therefore sets their `debug-uarts` boot argument to `3` after IMG3
authentication by default. Disable this assistance when testing native
firmware behavior:
```sh
DEBUG_UART=0 A6_LAB_DIR="$PWD/build/a6-lab" \
./Run-iPhone5-macOS.sh
```
With the default setting, a successful boot prints the authentic guest
`iBootStage2` banner followed by `Entering recovery mode, starting command
prompt`. Repeated NAND reset errors are currently expected because NAND is
not modeled; they do not prevent the Recovery endpoint from appearing.
## Live debugging with LLDB, GDB, or IDA
Set `GDB_PORT` to expose QEMU's remote GDB stub on localhost. Add `PAUSED=1`
to stop before the SecureROM's first instruction:
```sh
A6_LAB_DIR="$PWD/build/a6-lab" GDB_PORT=12345 PAUSED=1 \
./Run-iPhone5-macOS.sh
```
Attach LLDB:
```sh
lldb -o 'gdb-remote 127.0.0.1:12345'
```
Without `PAUSED=1`, the same command can attach to a running boot. Only one
debugger may control the GDB stub at a time.
Useful load addresses are:
| Stage | Address |
| --- | ---: |
| SecureROM | `0x00000000` |
| iBSS | `0x10000000` |
| iBEC | `0xbff00000` |
| UART0 MMIO | `0x32b00000` |
For IDA, load `build/a6-lab/iBEC.lab-root.bin` as a raw little-endian ARM
binary with image base `0xbff00000`. Select `Remote GDB debugger`, connect to
`127.0.0.1:12345`, and configure the same load address in the debugger's
memory map. The live PC should enter the `0xbff00000` region after the iBEC
handoff.
For a static-only iBSS session that intentionally bypasses IMG3 validation:
```sh
build/qemu-system-arm \
-machine iphone5 \
-bios firmware/s5l8950x-secure-rom.bin \
-device loader,file=firmware/iBSS.iphone5.RELEASE.bin,addr=0x10000000,cpu-num=0,force-raw=on \
-display none -serial mon:stdio \
-S -gdb tcp:127.0.0.1:12345
```
That loader command is useful for early breakpoints, but it bypasses the
SecureROM parser, signature checks, AES work, and normal handoff state. It is
not evidence of an authenticated boot.
## Build signed and encrypted custom images
The simplest supported workflow is to provide raw replacement `DATA`
payloads and rebuild an entirely new identity directory. Custom payloads must
retain exactly one copy of the original embedded 1,215-byte trust-anchor slot
so the builder can replace it with the generated lab root.
```sh
A6_IBSS_PAYLOAD="$PWD/payloads/iBSS.custom.bin" \
A6_IBEC_PAYLOAD="$PWD/payloads/iBEC.custom.bin" \
scripts/build-a6-lab-chain.sh \
"/absolute/path/to/SecureROM for s5l8950xsi, iBoot-1145.3" \
build/a6-custom
```
Boot that directory by setting `A6_LAB_DIR="$PWD/build/a6-custom"` in both
the launcher and `idevicerestore` commands shown above.
For individual IMG3 experiments, first inspect a source template:
```sh
scripts/a6-img3-info.py firmware/iBSS.iphone5.RELEASE.dfu
scripts/a6-img3-info.py firmware/iBSS.iphone5.RELEASE.dfu \
--extract-data build/iBSS.extracted.bin
```
Patch an iBoot payload to trust the existing lab root, then create an iBSS
with an embedded SHSH/CERT signature and GID-wrapped KBAG:
```sh
scripts/a6-lab-patch-iboot-root.py \
--source-rom "/absolute/path/to/original/SecureROM" \
--identity build/a6-lab \
--input payloads/iBSS.custom.bin \
--output build/iBSS.custom.lab-root.bin
scripts/a6-lab-img3.py \
--template firmware/iBSS.iphone5.RELEASE.dfu \
--payload build/iBSS.custom.lab-root.bin \
--identity build/a6-lab \
--encrypt \
--output build/iBSS.custom.signed-encrypted.dfu
```
An iBEC uses the external APTicket layout. Build its encrypted IMG3 first,
then bind the APTicket to that exact component and the guest boot nonce:
```sh
scripts/a6-lab-patch-iboot-root.py \
--source-rom "/absolute/path/to/original/SecureROM" \
--identity build/a6-lab \
--input payloads/iBEC.custom.bin \
--output build/iBEC.custom.lab-root.bin
scripts/a6-lab-img3.py \
--template firmware/iBEC.iphone5.RELEASE.dfu \
--payload build/iBEC.custom.lab-root.bin \
--identity build/a6-lab \
--encrypt --ticketed \
--output build/iBEC.custom.ticketed-encrypted.img3
scripts/a6-lab-apticket.py \
--component build/iBEC.custom.ticketed-encrypted.img3 \
--identity build/a6-lab \
--boot-nonce 8d82693c897d1b9d \
--ticket-output build/iBEC.custom.apticket.der \
--output build/iBEC.custom.signed-encrypted.dfu
```
The tools refuse to overwrite outputs and write JSON sidecars with hashes,
sizes, signed ranges, encryption state, device fields, and manifest tags.
`a6-lab-img3.py` uses a random content key and IV for every encrypted image;
the content key is wrapped by AES-256-CBC with the generated lab GID key.
## Environment variables
| Variable | Purpose | Default |
| --- | --- | --- |
| `A6_LAB_DIR` | Generated identity and boot-chain directory | unset |
| `A6_IBSS_PAYLOAD` | Raw custom iBSS payload for the chain builder | stock extracted iBSS |
| `A6_IBEC_PAYLOAD` | Raw custom iBEC payload for the chain builder | stock extracted iBEC |
| `A6_BOOT_NONCE` | Eight-byte ticket nonce in hexadecimal | `8d82693c897d1b9d` |
| `QEMU_BIN` | QEMU executable used by the launcher | `build/qemu-system-arm` |
| `USB_BRIDGE_PORT` | QEMU localhost recovery transport; `0` disables it | `26050` |
| `QEMU_USB_ENDPOINT` | Endpoint used by the host recovery wrappers | `127.0.0.1:26050` |
| `GDB_PORT` | Remote GDB listener | unset |
| `PAUSED` | Start with the CPU stopped | `0` |
| `DEBUG_UART` | Enable post-authentication debug UART assistance | `1` |
| `NATIVE_IMG3` | Enable authenticated native IMG3 handoff helpers | `1` with `A6_LAB_DIR` |
| `DFU_IMAGE` | Optional internal one-shot DFU image | unset |
## Windows host path
The Windows path uses a UDE test driver and a native TCP bridge to present
the virtual Apple DFU device to unmodified Windows recovery tools. Build QEMU
from MSYS2 MinGW64, then prepare the driver and bridge from PowerShell:
```powershell
.\windows\QemuA6Ude\prepare-package.ps1
.\windows\QemuA6UsbBridge\build-bridge.ps1 -Configuration Release
```
The first installation requires an administrator shell, test-signing mode,
and a restart:
```powershell
.\windows\QemuA6Ude\enable-test-mode.ps1
Restart-Computer
.\windows\QemuA6Ude\install-driver.ps1
```
Launch with:
```powershell
.\Run-iPhone5.ps1 -SkipBuild -GdbPort 12345
```
The local-key IMG3 generation and macOS `idevicerestore` boot-only wrapper
are currently the verified end-to-end path. The Windows UDE bridge has been
validated for DFU control traffic but not for the complete local iBSS/iBEC
workflow.
## Current limitations
- The verified endpoint is iBEC Recovery, not a ramdisk or iOS userspace.
- NAND is not implemented, so the guest emits repeated NAND errors.
- UART transmit and MMIO access are verified. UART receive is modeled, but a
complete interactive iBoot command session over serial is not guaranteed.
- The localhost macOS backend is visible only to the patched libirecovery
tools; it does not appear in Finder or System Information.
- The implementation is intended for deterministic parser and boot-flow
research, not for hardware timing or fault-injection equivalence.
+4
View File
@@ -34,6 +34,10 @@ version 2. For full licensing details, consult the LICENSE file.
Documentation
=============
The experimental Apple A6/iPhone 5 machine, its authenticated IMG3 lab boot
flow, UART access, and live-debugging workflow are documented in
`A6-STATUS.md <A6-STATUS.md>`_.
Documentation can be found hosted online at
`<https://www.qemu.org/documentation/>`_. The documentation for the
current development version that is available at
+133 -10
View File
@@ -3,18 +3,78 @@ set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
qemu_bin=${QEMU_BIN:-"$script_dir/build/qemu-system-arm"}
rom_path=${ROM_PATH:-"$script_dir/firmware/s5l8950x-secure-rom.bin"}
a6_lab_dir=${A6_LAB_DIR:-}
if [ -n "$a6_lab_dir" ]; then
a6_lab_dir=$(CDPATH= cd -- "$a6_lab_dir" && pwd)
rom_path=${ROM_PATH:-"$a6_lab_dir/s5l8950x-secure-rom-lab.bin"}
gid_key_path=${GID_KEY_PATH:-"$a6_lab_dir/gid-key.bin"}
native_img3=${NATIVE_IMG3:-1}
else
rom_path=${ROM_PATH:-"$script_dir/firmware/s5l8950x-secure-rom.bin"}
gid_key_path=${GID_KEY_PATH:-}
native_img3=${NATIVE_IMG3:-0}
fi
dfu_image=${DFU_IMAGE:-}
gdb_port=${GDB_PORT:-}
paused=${PAUSED:-0}
usb_bridge_port=${USB_BRIDGE_PORT:-26050}
debug_uart=${DEBUG_UART:-1}
case $debug_uart in
0|1) ;;
*)
echo "DEBUG_UART must be 0 or 1." >&2
exit 1
;;
esac
case $native_img3 in
0|1) ;;
*)
echo "NATIVE_IMG3 must be 0 or 1." >&2
exit 1
;;
esac
case $gdb_port in
''|*[!0-9]*)
if [ -n "$gdb_port" ]; then
echo "GDB_PORT must be an integer between 1 and 65535." >&2
exit 1
fi
;;
esac
if [ -n "$gdb_port" ] &&
{ [ "$gdb_port" -lt 1 ] || [ "$gdb_port" -gt 65535 ]; }; then
echo "GDB_PORT must be between 1 and 65535." >&2
exit 1
fi
if [ "$paused" != 0 ] && [ -z "$gdb_port" ]; then
gdb_port=1234
fi
case $usb_bridge_port in
''|*[!0-9]*)
echo "USB_BRIDGE_PORT must be an integer between 0 and 65535." >&2
exit 1
;;
esac
if [ "$usb_bridge_port" -gt 65535 ]; then
echo "USB_BRIDGE_PORT must be between 0 and 65535." >&2
exit 1
fi
if [ ! -x "$qemu_bin" ]; then
echo "QEMU introuvable: $qemu_bin" >&2
echo "Configurez ce dépôt avec --target-list=arm-softmmu puis compilez-le." >&2
echo "QEMU not found: $qemu_bin" >&2
echo "Configure this repository with --target-list=arm-softmmu," \
"then build it." >&2
exit 1
fi
if [ ! -f "$rom_path" ]; then
echo "SecureROM introuvable: $rom_path" >&2
echo "Placez votre dump A6 de 65536 octets dans firmware/s5l8950x-secure-rom.bin." >&2
echo "SecureROM not found: $rom_path" >&2
echo "Place your 65536-byte A6 dump at" \
"firmware/s5l8950x-secure-rom.bin." >&2
exit 1
fi
@@ -26,22 +86,85 @@ set -- \
-display none \
-serial mon:stdio
if [ "$debug_uart" = 1 ]; then
set -- "$@" \
-global s5l8950x-usb-otg.force-debug-uarts=on \
-global s5l8950x-aes.force-debug-uarts=on
fi
if [ -n "$gid_key_path" ]; then
if [ ! -f "$gid_key_path" ]; then
echo "Lab GID key not found: $gid_key_path" >&2
exit 1
fi
set -- "$@" -global "s5l8950x-aes.gid-key-file=$gid_key_path"
fi
if [ "$native_img3" = 1 ]; then
set -- "$@" \
-global s5l8950x-usb-otg.native-img3-handoff=on \
-global s5l8950x-aes.authenticated-ibec-handoff=on
fi
if [ -n "$dfu_image" ]; then
if [ ! -f "$dfu_image" ]; then
echo "Image DFU introuvable: $dfu_image" >&2
echo "DFU image not found: $dfu_image" >&2
exit 1
fi
set -- "$@" -global "s5l8950x-usb-otg.dfu-image=$dfu_image"
fi
if [ "$usb_bridge_port" -ne 0 ]; then
usb_chardev="socket,id=a6usb,host=127.0.0.1,port=$usb_bridge_port"
usb_chardev="$usb_chardev,server=on,wait=off"
set -- "$@" \
-chardev "$usb_chardev" \
-global "s5l8950x-usb-otg.usb-bridge=a6usb"
fi
if [ -n "$gdb_port" ]; then
set -- "$@" -gdb "tcp:127.0.0.1:$gdb_port"
fi
if [ "$paused" != 0 ]; then
set -- "$@" -S
fi
echo "QEMU macOS : $qemu_bin"
echo "SecureROM : $rom_path"
if [ -n "$dfu_image" ]; then
echo "Image DFU : $dfu_image"
echo "Injection DFU interne active."
echo "DFU image : $dfu_image"
echo "Internal DFU injection is enabled."
else
echo "La SecureROM attend en dfuIDLE."
echo "SecureROM is waiting in dfuIDLE."
fi
echo "A6 UART : this console (Ctrl+A C = monitor, Ctrl+A X = quit)"
if [ -n "$gid_key_path" ]; then
echo "AES GID : lab identity active (private key is not displayed)"
else
echo "AES GID : no lab GID slot configured"
fi
if [ "$native_img3" = 1 ]; then
echo "IMG3 handoff: authenticated iBEC transfer to 0xbff00000"
else
echo "IMG3 handoff: synthetic USB bridge compatibility mode"
fi
if [ "$debug_uart" = 1 ]; then
echo "debug-uarts: forced to 3 in iBSS/iBEC after IMG3 validation"
else
echo "debug-uarts: native firmware behavior (DEBUG_UART=0)"
fi
if [ "$usb_bridge_port" -ne 0 ]; then
echo "USB tools : QEMU at 127.0.0.1:$usb_bridge_port"
echo "irecovery : scripts/irecovery-qemu -q"
else
echo "USB tools : disabled (USB_BRIDGE_PORT=0)"
fi
if [ -n "$gdb_port" ]; then
echo "Debugger : remote GDB at 127.0.0.1:$gdb_port"
echo "LLDB : lldb -o 'gdb-remote 127.0.0.1:$gdb_port'"
fi
if [ "$paused" != 0 ]; then
echo "CPU : paused until the debugger issues continue"
fi
echo "UART A6 : cette console (Ctrl+A C = moniteur, Ctrl+A X = quitter)"
exec "$qemu_bin" "$@"
+46 -33
View File
@@ -9,6 +9,8 @@ param(
[switch]$NoWindowsUsb,
[switch]$SkipBuild,
[switch]$Trace,
[ValidateRange(0, 65535)]
[int]$GdbPort = 0,
[switch]$Paused
)
@@ -16,7 +18,7 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
throw 'Ce lanceur requiert Windows natif.'
throw 'This launcher requires native Windows.'
}
$qemuSource = $PSScriptRoot
@@ -32,22 +34,22 @@ $bridgeStdout = Join-Path $buildDir 'a6-usb-bridge.log'
$bridgeStderr = Join-Path $buildDir 'a6-usb-bridge-error.log'
if (-not (Test-Path -LiteralPath $RomPath -PathType Leaf)) {
throw "SecureROM introuvable : $RomPath"
throw "SecureROM not found: $RomPath"
}
$rom = Get-Item -LiteralPath $RomPath
if ($rom.Length -ne 0x10000) {
throw "La SecureROM doit faire exactement 65536 octets (taille actuelle : $($rom.Length))."
throw "SecureROM must be exactly 65536 bytes (actual size: $($rom.Length))."
}
$dfuImage = $null
if ($DfuImagePath) {
if (-not (Test-Path -LiteralPath $DfuImagePath -PathType Leaf)) {
throw "Image DFU/iBSS introuvable : $DfuImagePath"
throw "DFU/iBSS image not found: $DfuImagePath"
}
$dfuImage = Get-Item -LiteralPath $DfuImagePath
if ($dfuImage.Length -eq 0) {
throw "L'image DFU/iBSS est vide : $($dfuImage.FullName)"
throw "The DFU/iBSS image is empty: $($dfuImage.FullName)"
}
}
@@ -57,13 +59,13 @@ $env:Path = "$mingwBin;$msysBin;$env:Path"
if (-not $SkipBuild) {
if (-not (Test-Path -LiteralPath $ninja -PathType Leaf)) {
throw "Ninja MinGW64 introuvable : $ninja"
throw "MinGW64 Ninja not found: $ninja"
}
if (-not (Test-Path -LiteralPath (Join-Path $msysBin 'sort.exe') -PathType Leaf)) {
throw "Les outils MSYS2 sont incomplets : $msysBin"
throw "The MSYS2 tool set is incomplete: $msysBin"
}
if (-not (Test-Path -LiteralPath (Join-Path $buildDir 'build.ninja') -PathType Leaf)) {
throw "Le dossier qemu\build n'est pas configure. Configurez QEMU depuis MSYS2 MinGW64 avec --target-list=arm-softmmu."
throw "qemu\build is not configured. Configure QEMU from MSYS2 MinGW64 with --target-list=arm-softmmu."
}
[IO.Directory]::CreateDirectory($buildTemp) | Out-Null
@@ -73,17 +75,17 @@ if (-not $SkipBuild) {
& $ninja -C $buildDir qemu-system-arm.exe
if ($LASTEXITCODE -ne 0) {
throw "La compilation QEMU a echoue avec le code $LASTEXITCODE."
throw "The QEMU build failed with exit code $LASTEXITCODE."
}
}
if (-not (Test-Path -LiteralPath $qemu -PathType Leaf)) {
throw "Binaire QEMU introuvable : $qemu"
throw "QEMU binary not found: $qemu"
}
$machines = & $qemu -machine help 2>&1
if ($LASTEXITCODE -ne 0 -or -not ($machines -match '^iphone5\s')) {
throw 'Ce binaire QEMU ne contient pas la machine iphone5.'
throw 'This QEMU binary does not include the iphone5 machine.'
}
$qemuArgs = @(
@@ -107,13 +109,19 @@ if ($windowsUsb) {
)
}
if ($Paused) {
$qemuArgs += @('-S', '-display', 'none', '-serial', 'null', '-monitor', 'stdio')
} else {
# UART0 is connected explicitly to this native Windows console. The
# monitor is multiplexed on the same chardev (Ctrl+A C toggles it).
$qemuArgs += @('-display', 'none', '-serial', 'mon:stdio')
if ($Paused -and $GdbPort -eq 0) {
$GdbPort = 1234
}
if ($GdbPort -gt 0) {
$qemuArgs += @('-gdb', "tcp:127.0.0.1:$GdbPort")
}
if ($Paused) {
$qemuArgs += '-S'
}
# UART0 and the monitor stay multiplexed on the native console, including
# while the CPU is stopped for a debugger (Ctrl+A C toggles the monitor).
$qemuArgs += @('-display', 'none', '-serial', 'mon:stdio')
if ($Trace) {
$tracePath = Join-Path $buildDir 'a6-boot.log'
@@ -121,32 +129,37 @@ if ($Trace) {
'-d', 'in_asm,cpu_reset,guest_errors,unimp',
'-D', $tracePath
)
Write-Host "Trace QEMU : $tracePath"
Write-Host "QEMU trace : $tracePath"
}
Write-Host "QEMU Windows natif : $qemu"
Write-Host "Native Windows QEMU : $qemu"
Write-Host "SecureROM : $($rom.FullName)"
if ($dfuImage) {
Write-Host "Image DFU/iBSS : $($dfuImage.FullName)"
Write-Host "DFU/iBSS image : $($dfuImage.FullName)"
}
if ($windowsUsb) {
if (-not (Test-Path -LiteralPath $bridge -PathType Leaf)) {
& $bridgeBuild -Configuration Release
}
Write-Host 'USB Windows : Apple DFU VID_05AC&PID_1227 via UDE'
Write-Host "Pont QEMU : 127.0.0.1:$UsbBridgePort"
Write-Host "Journal pont USB : $bridgeStdout"
Write-Host 'Windows USB : Apple DFU VID_05AC&PID_1227 through UDE'
Write-Host "QEMU bridge : 127.0.0.1:$UsbBridgePort"
Write-Host "USB bridge log : $bridgeStdout"
}
if (-not $Paused) {
if ($dfuImage) {
Write-Host 'Le cable USB et le transfert DFU sont simules en interne.'
} elseif ($windowsUsb) {
Write-Host 'Windows voit le téléphone DFU virtuel ; idevicerestore reste inchangé.'
} else {
Write-Host 'Recovery et cable USB sont simulés : la ROM attend en mode DFU.'
}
Write-Host 'UART A6 : cette console (Ctrl+A C = moniteur)'
Write-Host 'Utilisez Ctrl+A puis X pour quitter.'
if ($dfuImage) {
Write-Host 'The USB cable and DFU transfer are simulated internally.'
} elseif ($windowsUsb) {
Write-Host 'Windows exposes the virtual DFU phone to unmodified host tools.'
} else {
Write-Host 'Recovery and the USB cable are simulated; the ROM is waiting in DFU mode.'
}
Write-Host 'A6 UART : this console (Ctrl+A C = monitor)'
Write-Host 'Press Ctrl+A, then X to quit.'
if ($GdbPort -gt 0) {
Write-Host "Debugger : remote GDB at 127.0.0.1:$GdbPort"
Write-Host "LLDB : lldb -o 'gdb-remote 127.0.0.1:$GdbPort'"
}
if ($Paused) {
Write-Host 'CPU : paused until the debugger issues continue'
}
$bridgeProcess = $null
+89 -7
View File
@@ -5,6 +5,9 @@
#include "hw/core/boards.h"
#include "hw/core/loader.h"
#include "hw/misc/unimp.h"
#include "hw/misc/s5l8950x-aes.h"
#include "hw/misc/s5l8950x-pke.h"
#include "hw/misc/s5l8950x-sha1.h"
#include "hw/core/sysbus.h"
#include "system/address-spaces.h"
#include "target/arm/cpu.h"
@@ -30,6 +33,39 @@ static const ARMCPRegInfo s5l8950x_cp_reginfo[] = {
.access = PL1_W,
.type = ARM_CP_NOP,
},
{
.name = "APPLE_SWIFT_AUX_CFG",
.cp = 15,
.opc1 = 1,
.crn = 15,
.crm = 2,
.opc2 = 4,
.access = PL1_RW,
.type = ARM_CP_CONST,
.resetvalue = 0,
},
{
.name = "APPLE_SWIFT_AUX_CFG2",
.cp = 15,
.opc1 = 1,
.crn = 15,
.crm = 2,
.opc2 = 5,
.access = PL1_RW,
.type = ARM_CP_CONST,
.resetvalue = 0,
},
{
.name = "APPLE_SWIFT_AUX_CFG3",
.cp = 15,
.opc1 = 1,
.crn = 15,
.crm = 2,
.opc2 = 7,
.access = PL1_RW,
.type = ARM_CP_CONST,
.resetvalue = 0,
},
};
/* Addresses */
@@ -40,21 +76,30 @@ static const ARMCPRegInfo s5l8950x_cp_reginfo[] = {
#define S5L8950X_SDIO_BASE 0x20000000
#define S5L8950X_USB_PHY_BASE 0x36000000
#define S5L8950X_USB_BASE 0x36100000
#define S5L8950X_SHA1_BASE 0x3C500000
#define S5L8950X_SHA1_BASE 0x30200000
#define S5L8950X_SPI_BASE 0x3D200000
#define S5L8950X_AES_BASE 0x3E000000
#define S5L8950X_AES_DMA_BASE 0x37000000
#define S5L8950X_AES_BASE 0x37800000
#define S5L8950X_IIC_BASE 0x3E100000
#define S5L8950X_PKE_BASE 0x3E200000
#define S5L8950X_PKE_BASE 0x33100000
#define S5L8950X_WDT_BASE 0x3E300000
#define S5L8950X_CHIPID_BASE 0x3F500000
#define S5L8950X_TIMER_BASE 0x3F200000
#define S5L8950X_PMGR_BASE 0x3F100000
#define S5L8950X_CLOCK_BASE 0x3F800000
#define S5L8950X_MEMCTL_BASE 0x39500000
#define S5L8950X_AMC_BASE 0x40000000
#define S5L8950X_AIC_BASE 0x3F200000
#define S5L8950X_UART_BASE 0x44300000
#define S5L8950X_UART_BASE 0x32B00000
#define S5L8950X_GPIO_BASE 0x3FA00000
#define S5L8950X_DRAM_BASE 0x80000000
static const hwaddr s5l8950x_i2c_base[] = {
0x33200000,
0x33300000,
0x33400000,
};
static void s5l8950x_init(Object *obj)
{
S5L8950XState *s = S5L8950X(obj);
@@ -84,11 +129,8 @@ static void s5l8950x_realize(DeviceState *dev, Error **errp)
/* Create unimplemented devices */
create_unimplemented_device("s5l8950x.sdio", S5L8950X_SDIO_BASE, 0x10000);
create_unimplemented_device("s5l8950x.sha1", S5L8950X_SHA1_BASE, 0x1000);
create_unimplemented_device("s5l8950x.spi", S5L8950X_SPI_BASE, 0x1000);
create_unimplemented_device("s5l8950x.aes", S5L8950X_AES_BASE, 0x1000);
create_unimplemented_device("s5l8950x.iic", S5L8950X_IIC_BASE, 0x1000);
create_unimplemented_device("s5l8950x.pke", S5L8950X_PKE_BASE, 0x1000);
create_unimplemented_device("s5l8950x.wdt", S5L8950X_WDT_BASE, 0x1000);
create_unimplemented_device("s5l8950x.amc", S5L8950X_AMC_BASE, 0x1000);
@@ -98,6 +140,22 @@ static void s5l8950x_realize(DeviceState *dev, Error **errp)
sysbus_realize_and_unref(s->chipid, &error_fatal);
sysbus_mmio_map(s->chipid, 0, S5L8950X_CHIPID_BASE);
DeviceState *dev_aes = qdev_new(TYPE_S5L8950X_AES);
s->aes = SYS_BUS_DEVICE(dev_aes);
sysbus_realize_and_unref(s->aes, &error_fatal);
sysbus_mmio_map(s->aes, 0, S5L8950X_AES_DMA_BASE);
sysbus_mmio_map(s->aes, 1, S5L8950X_AES_BASE);
DeviceState *dev_pke = qdev_new(TYPE_S5L8950X_PKE);
s->pke = SYS_BUS_DEVICE(dev_pke);
sysbus_realize_and_unref(s->pke, &error_fatal);
sysbus_mmio_map(s->pke, 0, S5L8950X_PKE_BASE);
DeviceState *dev_sha1 = qdev_new(TYPE_S5L8950X_SHA1);
s->sha1 = SYS_BUS_DEVICE(dev_sha1);
sysbus_realize_and_unref(s->sha1, &error_fatal);
sysbus_mmio_map(s->sha1, 0, S5L8950X_SHA1_BASE);
DeviceState *dev_aic = qdev_new(TYPE_S5L8950X_AIC);
s->aic = SYS_BUS_DEVICE(dev_aic);
sysbus_realize_and_unref(s->aic, &error_fatal);
@@ -112,14 +170,38 @@ static void s5l8950x_realize(DeviceState *dev, Error **errp)
s->uart = SYS_BUS_DEVICE(dev_uart);
sysbus_realize_and_unref(s->uart, &error_fatal);
sysbus_mmio_map(s->uart, 0, S5L8950X_UART_BASE);
/* iBoot's A6 UART table maps 0x32b00000 to external IRQ 0x44. */
sysbus_connect_irq(s->uart, 0,
qdev_get_gpio_in(dev_aic, 0x44));
DeviceState *dev_pmgr = qdev_new(TYPE_S5L8950X_PMGR);
s->pmgr = SYS_BUS_DEVICE(dev_pmgr);
sysbus_realize_and_unref(s->pmgr, &error_fatal);
sysbus_mmio_map(s->pmgr, 0, S5L8950X_PMGR_BASE);
DeviceState *dev_clock = qdev_new(TYPE_S5L8950X_CLOCK);
s->clock = SYS_BUS_DEVICE(dev_clock);
sysbus_realize_and_unref(s->clock, &error_fatal);
sysbus_mmio_map(s->clock, 0, S5L8950X_CLOCK_BASE);
DeviceState *dev_memctl = qdev_new(TYPE_S5L8950X_MEMCTL);
s->memctl = SYS_BUS_DEVICE(dev_memctl);
sysbus_realize_and_unref(s->memctl, &error_fatal);
sysbus_mmio_map(s->memctl, 0, S5L8950X_MEMCTL_BASE);
for (size_t i = 0; i < ARRAY_SIZE(s5l8950x_i2c_base); i++) {
DeviceState *dev_i2c = qdev_new(TYPE_S5L8950X_I2C);
s->i2c[i] = SYS_BUS_DEVICE(dev_i2c);
sysbus_realize_and_unref(s->i2c[i], &error_fatal);
sysbus_mmio_map(s->i2c[i], 0, s5l8950x_i2c_base[i]);
sysbus_connect_irq(s->i2c[i], 0,
qdev_get_gpio_in(dev_aic, 0x46 + i));
}
DeviceState *dev_gpio = qdev_new(TYPE_S5L8950X_GPIO);
qdev_prop_set_bit(dev_gpio, "force-dfu", true);
qdev_prop_set_uint8(dev_gpio, "board-straps", 2);
s->gpio = SYS_BUS_DEVICE(dev_gpio);
sysbus_realize_and_unref(s->gpio, &error_fatal);
sysbus_mmio_map(s->gpio, 0, S5L8950X_GPIO_BASE);
+25 -7
View File
@@ -9,6 +9,8 @@
#define TYPE_S5L8950X_UART "s5l8950x-uart"
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XUartState, S5L8950X_UART)
#define S5L8950X_UART_RX_FIFO_SIZE 16
struct S5L8950XUartState {
SysBusDevice parent_obj;
@@ -22,7 +24,7 @@ struct S5L8950XUartState {
uint32_t ubrdiv;
uint32_t ufracval;
uint8_t rx_fifo[16];
uint8_t rx_fifo[S5L8950X_UART_RX_FIFO_SIZE];
int rx_count;
qemu_irq irq;
};
@@ -40,6 +42,9 @@ struct S5L8950XUartState {
#define UART_UBRDIV 0x28
#define UART_UFRACVAL 0x2C
#define UART_UFCON_RX_FIFO_RESET BIT(1)
#define UART_UFCON_TX_FIFO_RESET BIT(2)
static void s5l8950x_uart_update_irq(S5L8950XUartState *s)
{
if (s->rx_count > 0) {
@@ -76,7 +81,7 @@ static uint64_t s5l8950x_uart_read(void *opaque, hwaddr offset, unsigned size)
break;
case UART_UFSTAT:
/* Return rx count in low bits, no tx fifo used here */
val = s->rx_count & 0xf;
val = s->rx_count;
break;
case UART_UMSTAT:
val = 0;
@@ -89,6 +94,7 @@ static uint64_t s5l8950x_uart_read(void *opaque, hwaddr offset, unsigned size)
s->rx_count--;
memmove(s->rx_fifo, s->rx_fifo + 1, s->rx_count);
s5l8950x_uart_update_irq(s);
qemu_chr_fe_accept_input(&s->chr);
}
break;
case UART_UBRDIV:
@@ -118,7 +124,14 @@ static void s5l8950x_uart_write(void *opaque, hwaddr offset, uint64_t val, unsig
s->ucon = val;
break;
case UART_UFCON:
s->ufcon = val;
if (val & UART_UFCON_RX_FIFO_RESET) {
s->rx_count = 0;
s5l8950x_uart_update_irq(s);
qemu_chr_fe_accept_input(&s->chr);
}
/* FIFO reset bits are self-clearing; TX is unbuffered. */
s->ufcon = val & ~(UART_UFCON_RX_FIFO_RESET |
UART_UFCON_TX_FIFO_RESET);
break;
case UART_UMCON:
s->umcon = val;
@@ -151,9 +164,13 @@ static void s5l8950x_uart_write(void *opaque, hwaddr offset, uint64_t val, unsig
static const MemoryRegionOps s5l8950x_uart_ops = {
.read = s5l8950x_uart_read,
.write = s5l8950x_uart_write,
.endianness = DEVICE_NATIVE_ENDIAN,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.min_access_size = 4,
.min_access_size = 1,
.max_access_size = 4,
},
.impl = {
.min_access_size = 1,
.max_access_size = 4,
},
};
@@ -162,7 +179,7 @@ static int s5l8950x_uart_can_receive(void *opaque)
{
S5L8950XUartState *s = S5L8950X_UART(opaque);
return sizeof(s->rx_fifo) - s->rx_count;
return S5L8950X_UART_RX_FIFO_SIZE - s->rx_count;
}
static void s5l8950x_uart_receive(void *opaque, const uint8_t *buf, int size)
@@ -170,7 +187,8 @@ static void s5l8950x_uart_receive(void *opaque, const uint8_t *buf, int size)
S5L8950XUartState *s = S5L8950X_UART(opaque);
int i;
for (i = 0; i < size && s->rx_count < sizeof(s->rx_fifo); i++) {
for (i = 0; i < size &&
s->rx_count < S5L8950X_UART_RX_FIFO_SIZE; i++) {
s->rx_fifo[s->rx_count++] = buf[i];
}
s5l8950x_uart_update_irq(s);
+31
View File
@@ -18,6 +18,17 @@
/* Recovery/DFU button sampled by SecureROM as GPIO 0x1906. */
#define S5L8950X_GPIO_DFU_OFFSET (((0x19 * 8) + 6) * sizeof(uint32_t))
/*
* SecureROM packs these four board straps into bits [11:8] of the
* boot-policy word at 0x3f106000. Profile 2 is the authenticated A6
* production path used by this n41ap machine.
*/
#define S5L8950X_GPIO_STRAP_0_OFFSET (((0x02 * 8) + 2) * sizeof(uint32_t))
#define S5L8950X_GPIO_STRAP_1_OFFSET (((0x19 * 8) + 5) * sizeof(uint32_t))
#define S5L8950X_GPIO_STRAP_2_OFFSET (((0x1a * 8) + 0) * sizeof(uint32_t))
#define S5L8950X_GPIO_STRAP_3_OFFSET (((0x1a * 8) + 1) * sizeof(uint32_t))
/* iBEC samples GPIO 1 as an active-low boot/power button. */
#define S5L8950X_GPIO_BOOT_BUTTON_OFFSET sizeof(uint32_t)
#define S5L8950X_GPIO_INPUT_LEVEL BIT(0)
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XGPIOState, S5L8950X_GPIO)
@@ -27,6 +38,7 @@ struct S5L8950XGPIOState {
MemoryRegion iomem;
uint32_t regs[S5L8950X_GPIO_NUM_REGS];
bool force_dfu;
uint8_t board_straps;
};
static uint64_t s5l8950x_gpio_read(void *opaque, hwaddr offset,
@@ -40,9 +52,27 @@ static uint64_t s5l8950x_gpio_read(void *opaque, hwaddr offset,
}
value = s->regs[offset / sizeof(uint32_t)];
if (offset == S5L8950X_GPIO_BOOT_BUTTON_OFFSET) {
/*
* No host button is currently wired up: expose the released level so
* iBEC does not mistake the zero-filled register file for a button
* held continuously and power the device off after its timeout.
*/
value |= S5L8950X_GPIO_INPUT_LEVEL;
}
if (s->force_dfu && offset == S5L8950X_GPIO_DFU_OFFSET) {
value |= S5L8950X_GPIO_INPUT_LEVEL;
}
if ((offset == S5L8950X_GPIO_STRAP_0_OFFSET &&
(s->board_straps & BIT(0))) ||
(offset == S5L8950X_GPIO_STRAP_1_OFFSET &&
(s->board_straps & BIT(1))) ||
(offset == S5L8950X_GPIO_STRAP_2_OFFSET &&
(s->board_straps & BIT(2))) ||
(offset == S5L8950X_GPIO_STRAP_3_OFFSET &&
(s->board_straps & BIT(3)))) {
value |= S5L8950X_GPIO_INPUT_LEVEL;
}
return value;
}
@@ -76,6 +106,7 @@ static const MemoryRegionOps s5l8950x_gpio_ops = {
static const Property s5l8950x_gpio_properties[] = {
DEFINE_PROP_BOOL("force-dfu", S5L8950XGPIOState, force_dfu, false),
DEFINE_PROP_UINT8("board-straps", S5L8950XGPIOState, board_straps, 0),
};
static void s5l8950x_gpio_reset(DeviceState *dev)
+138 -3
View File
@@ -9,9 +9,11 @@
#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/timer.h"
#include "hw/core/cpu.h"
#include "hw/core/sysbus.h"
#include "hw/core/irq.h"
#include "qom/object.h"
#include "target/arm/cpu.h"
#define TYPE_S5L8950X_AIC "s5l8950x-aic"
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAicState, S5L8950X_AIC)
@@ -30,12 +32,19 @@ OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAicState, S5L8950X_AIC)
#define AIC_ACK 0x2004
#define AIC_IPI_SET 0x2008
#define AIC_IPI_CLR 0x200c
#define AIC_TIMER_CTRL 0x2010
#define AIC_TIMER_COMPARE 0x2014
#define AIC_TIMER_STATE 0x2018
#define AIC_TIMER_MASK_SET 0x201c
#define AIC_TIMER_MASK_CLR 0x2020
#define AIC_SRC_CFG_BASE 0x3000
#define AIC_MASK_SET_BASE 0x4100
#define AIC_MASK_CLR_BASE 0x4180
#define AIC_INT_STATE_BASE 0x4200
#define AIC_EVENT_EXT 0x00010000
#define AIC_EVENT_TIMER 0x00070001
#define AIC_CLOCK_HZ 24000000
struct S5L8950XAicState {
SysBusDevice parent_obj;
@@ -44,6 +53,13 @@ struct S5L8950XAicState {
uint32_t pending[S5L8950X_AIC_NUM_BANKS];
uint32_t src_cfg[S5L8950X_AIC_NUM_IRQS];
uint32_t glb_cfg;
uint32_t timer_ctrl;
uint32_t timer_compare;
uint32_t timer_state;
bool timer_irq_enabled;
bool timer_pending;
bool timer_has_fired;
QEMUTimer *timer;
qemu_irq irq;
qemu_irq fiq;
};
@@ -59,7 +75,7 @@ static bool s5l8950x_aic_irq_is_active(S5L8950XAicState *s, int irq)
static void s5l8950x_aic_update(S5L8950XAicState *s)
{
bool active = false;
bool active = s->timer_pending && s->timer_irq_enabled;
int irq;
for (irq = 0; irq < S5L8950X_AIC_NUM_IRQS; irq++) {
@@ -72,10 +88,83 @@ static void s5l8950x_aic_update(S5L8950XAicState *s)
qemu_irq_lower(s->fiq);
}
static uint64_t s5l8950x_aic_ticks(void)
{
return muldiv64(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL), AIC_CLOCK_HZ,
NANOSECONDS_PER_SECOND);
}
static void s5l8950x_aic_timer_reschedule(S5L8950XAicState *s);
static void s5l8950x_aic_timer_expired(void *opaque)
{
S5L8950XAicState *s = opaque;
CPUState *cs = first_cpu;
vaddr pc = cs ? ARM_CPU(cs)->env.regs[15] : 0;
/*
* iBoot initially shares the top of its bootstrap stack with the IRQ
* stack. On A6 the CPU reaches WFE well before the first 10 ms timer
* deadline, while a non-icount TCG run can take longer in host time.
* Do not inject that first local event until the bootstrap code has
* actually reached WFE; later events retain their normal timing and may
* preempt scheduler tasks.
*/
if (!s->timer_has_fired && cs &&
(!cs->halted || pc < 0x10000854 || pc > 0x10000860)) {
timer_mod(s->timer,
qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) +
NANOSECONDS_PER_SECOND / 1000);
return;
}
if (!s->timer_has_fired) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x-aic: first local timer event at "
"PC=0x%" VADDR_PRIx "\n", pc);
}
s->timer_has_fired = true;
s->timer_state |= 1;
s->timer_pending = true;
s5l8950x_aic_update(s);
}
static void s5l8950x_aic_timer_reschedule(S5L8950XAicState *s)
{
uint32_t delta = s->timer_compare;
int64_t delta_ns;
timer_del(s->timer);
/* AICv2's compare register is a relative countdown in 24 MHz ticks. */
if (!(s->timer_ctrl & 1) || delta == UINT32_MAX) {
return;
}
if (!delta) {
delta = 1;
}
delta_ns = muldiv64(delta, NANOSECONDS_PER_SECOND, AIC_CLOCK_HZ);
if (!delta_ns) {
delta_ns = 1;
}
timer_mod(s->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + delta_ns);
}
static uint32_t s5l8950x_aic_ack(S5L8950XAicState *s)
{
int irq;
/*
* The per-CPU timer is reported by AICv2 as local event 1, not as an
* external source. iBoot maps 0x00070001 to its internal vector 0xc1.
* Acknowledge also masks it until iBoot writes TIMER_MASK_CLR at the end
* of the handler, mirroring the external-source acknowledge behaviour.
*/
if (s->timer_pending && s->timer_irq_enabled) {
s->timer_irq_enabled = false;
s5l8950x_aic_update(s);
return AIC_EVENT_TIMER;
}
for (irq = 0; irq < S5L8950X_AIC_NUM_IRQS; irq++) {
if (s5l8950x_aic_irq_is_active(s, irq)) {
s->mask[irq / 32] |= BIT(irq % 32);
@@ -105,8 +194,7 @@ static uint64_t s5l8950x_aic_read(void *opaque, hwaddr offset, unsigned size)
S5L8950XAicState *s = opaque;
uint64_t ticks;
ticks = muldiv64(qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL), 24000000,
NANOSECONDS_PER_SECOND);
ticks = s5l8950x_aic_ticks();
switch (offset) {
case AIC_REV:
return 2;
@@ -126,6 +214,15 @@ static uint64_t s5l8950x_aic_read(void *opaque, hwaddr offset, unsigned size)
return 0;
case AIC_ACK:
return s5l8950x_aic_ack(s);
case AIC_TIMER_CTRL:
return s->timer_ctrl;
case AIC_TIMER_COMPARE:
return s->timer_compare;
case AIC_TIMER_STATE:
return s->timer_state;
case AIC_TIMER_MASK_SET:
case AIC_TIMER_MASK_CLR:
return 0;
case AIC_SRC_CFG_BASE ... AIC_SRC_CFG_BASE +
S5L8950X_AIC_NUM_IRQS * 4 - 4:
return s->src_cfg[(offset - AIC_SRC_CFG_BASE) / 4];
@@ -163,6 +260,35 @@ static void s5l8950x_aic_write(void *opaque, hwaddr offset,
case AIC_IPI_SET:
case AIC_IPI_CLR:
break;
case AIC_TIMER_CTRL:
s->timer_ctrl = value;
if (!(s->timer_ctrl & 1)) {
timer_del(s->timer);
}
break;
case AIC_TIMER_COMPARE:
s->timer_compare = value;
s5l8950x_aic_timer_reschedule(s);
break;
case AIC_TIMER_STATE:
s->timer_state &= ~value;
if (value & 1) {
s->timer_pending = false;
s5l8950x_aic_update(s);
}
break;
case AIC_TIMER_MASK_SET:
if (value & 2) {
s->timer_irq_enabled = false;
s5l8950x_aic_update(s);
}
break;
case AIC_TIMER_MASK_CLR:
if (value & 2) {
s->timer_irq_enabled = true;
s5l8950x_aic_update(s);
}
break;
case AIC_SRC_CFG_BASE ... AIC_SRC_CFG_BASE +
S5L8950X_AIC_NUM_IRQS * 4 - 4:
s->src_cfg[(offset - AIC_SRC_CFG_BASE) / 4] = value;
@@ -209,6 +335,13 @@ static void s5l8950x_aic_reset(DeviceState *dev)
memset(s->pending, 0, sizeof(s->pending));
memset(s->src_cfg, 0, sizeof(s->src_cfg));
s->glb_cfg = 0;
s->timer_ctrl = 0;
s->timer_compare = UINT32_MAX;
s->timer_state = 0;
s->timer_irq_enabled = false;
s->timer_pending = false;
s->timer_has_fired = false;
timer_del(s->timer);
s5l8950x_aic_update(s);
}
@@ -217,6 +350,8 @@ static void s5l8950x_aic_init(Object *obj)
S5L8950XAicState *s = S5L8950X_AIC(obj);
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL,
s5l8950x_aic_timer_expired, s);
memory_region_init_io(&s->iomem, obj, &s5l8950x_aic_ops, s,
TYPE_S5L8950X_AIC, 0x8000);
sysbus_init_mmio(sbd, &s->iomem);
+10 -1
View File
@@ -16,7 +16,16 @@ system_ss.add(when: 'CONFIG_PL310', if_true: files('arm_l2x0.c'))
system_ss.add(when: 'CONFIG_INTEGRATOR_DEBUG', if_true: files('arm_integrator_debug.c'))
system_ss.add(when: 'CONFIG_A9SCU', if_true: files('a9scu.c'))
system_ss.add(when: 'CONFIG_ARM11SCU', if_true: files('arm11scu.c'))
system_ss.add(when: 'CONFIG_S5L8950X', if_true: files('s5l8950x-chipid.c', 's5l8950x-pmgr.c'))
system_ss.add(when: 'CONFIG_S5L8950X', if_true: files(
's5l8950x-aes.c',
's5l8950x-pke.c',
's5l8950x-sha1.c',
's5l8950x-chipid.c',
's5l8950x-clock.c',
's5l8950x-i2c.c',
's5l8950x-memctl.c',
's5l8950x-pmgr.c',
))
system_ss.add(when: 'CONFIG_ARM_V7M', if_true: files('armv7m_ras.c'))
+589
View File
@@ -0,0 +1,589 @@
/*
* Apple A6 (S5L8950X) AES accelerator and its two-channel DMA front-end.
*
* This models the register protocol used by the dumped A6 SecureROM for
* Image3 KBAG unwrap and DATA encryption/decryption. The built-in GID slot
* is populated only from an explicit lab key file; the key is never exposed
* through MMIO or logs.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "crypto/aes.h"
#include "hw/misc/s5l8950x-aes.h"
#include "hw/core/cpu.h"
#include "hw/core/sysbus.h"
#include "hw/core/qdev-properties.h"
#include "qemu/log.h"
#include "qemu/module.h"
#include "qemu/units.h"
#include "system/address-spaces.h"
#include "system/dma.h"
#include "target/arm/cpu.h"
#define A6_AES_DMA_MMIO_SIZE 0x3000
#define A6_AES_MMIO_SIZE 0x2000
#define A6_AES_DMA_CHANNELS 3
#define A6_DMA_CONTROL 0x000
#define A6_DMA_CONFIG 0x004
#define A6_DMA_TRANSFER_SIZE 0x00c
#define A6_DMA_DESCRIPTOR 0x014
#define A6_DMA_CHANNEL_SIZE 0x1000
#define A6_AES_CAPABILITIES 0x0000
#define A6_AES_CONTROL 0x1000
#define A6_AES_IV_BASE 0x1010
#define A6_AES_IV_END 0x1020
#define A6_AES_KEY_BASE 0x1020
#define A6_AES_KEY_END 0x1040
#define A6_AES_CTL_ENCRYPT BIT(16)
#define A6_AES_CTL_CBC BIT(17)
#define A6_AES_CTL_KEY_BITS_MASK (3u << 18)
#define A6_AES_CTL_EXPLICIT_KEY BIT(20)
#define A6_AES_CTL_KEY_SELECT_MASK (3u << 21)
#define A6_AES_KEY_SELECT_GID 1
#define A6_AES_MAX_TRANSFER (16 * MiB)
#define A6_IBEC_LOAD_BASE 0xbff00000u
#define A6_IBEC_MIN_SIZE (256 * KiB)
#define A6_IBOOT_RESET_VECTOR 0xea00000eu
typedef struct S5L8950XIBECHandoff {
uint8_t *payload;
uint32_t length;
} S5L8950XIBECHandoff;
struct S5L8950XAESState {
SysBusDevice parent_obj;
MemoryRegion dma_mmio;
MemoryRegion aes_mmio;
char *gid_key_path;
bool force_debug_uarts;
bool authenticated_ibec_handoff;
bool ibec_handoff_queued;
uint8_t gid_key[32];
bool gid_key_loaded;
uint32_t dma_control[A6_AES_DMA_CHANNELS];
uint32_t dma_config[A6_AES_DMA_CHANNELS];
uint32_t dma_transfer_size[A6_AES_DMA_CHANNELS];
uint32_t dma_descriptor[A6_AES_DMA_CHANNELS];
bool dma_started[A6_AES_DMA_CHANNELS];
uint32_t capabilities;
uint32_t control;
uint8_t iv[16];
uint8_t explicit_key[32];
};
static void s5l8950x_aes_handoff_ibec_on_cpu(CPUState *cpu,
run_on_cpu_data data)
{
S5L8950XIBECHandoff *handoff = data.host_ptr;
if (address_space_write_rom(&address_space_memory, A6_IBEC_LOAD_BASE,
MEMTXATTRS_UNSPECIFIED, handoff->payload,
handoff->length) != MEMTX_OK) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: iBEC handoff write failed at 0x%08x\n",
A6_IBEC_LOAD_BASE);
goto out;
}
/*
* iBSS has already completed the IMG3, ticket, nonce and AES work before
* this callback is queued. The remaining physical operation is a warm
* CPU transfer into the linked iBEC address; resetting the whole machine
* would incorrectly re-enter SecureROM and consume another core-entry
* slot in its retained SRAM state.
*/
cpu_reset(cpu);
ARM_CPU(cpu)->env.regs[0] = 1;
cpu->halted = 0;
cpu_set_pc(cpu, A6_IBEC_LOAD_BASE);
cpu_exit(cpu);
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: transferred authenticated iBEC (%u bytes) "
"to PC=0x%08x\n",
handoff->length, A6_IBEC_LOAD_BASE);
out:
g_free(handoff->payload);
g_free(handoff);
}
static void s5l8950x_aes_queue_ibec_handoff(S5L8950XAESState *s,
const uint8_t *payload,
uint32_t length)
{
S5L8950XIBECHandoff *handoff;
CPUState *cpu = first_cpu;
if (!cpu || s->ibec_handoff_queued || length < A6_IBEC_MIN_SIZE ||
ldl_le_p(payload) != A6_IBOOT_RESET_VECTOR) {
return;
}
handoff = g_new(S5L8950XIBECHandoff, 1);
handoff->payload = g_memdup2(payload, length);
handoff->length = length;
s->ibec_handoff_queued = true;
async_run_on_cpu(cpu, s5l8950x_aes_handoff_ibec_on_cpu,
RUN_ON_CPU_HOST_PTR(handoff));
}
static bool s5l8950x_aes_enable_bootloader_uarts(uint8_t *image,
size_t image_length,
hwaddr load_base)
{
static const uint16_t helper_tail[] = {
0x680a, /* ldr r2, [r1] */
0x4310, /* orrs r0, r2 */
0x6008, /* str r0, [r1] */
0x4770, /* bx lr */
};
static const uint8_t movs_r0_3[] = { 0x03, 0x20 };
for (size_t offset = 0; offset + 10 <= image_length; offset += 2) {
uint16_t ldr_literal = lduw_le_p(image + offset);
size_t i;
if ((ldr_literal & 0xff00) != 0x4900) {
continue;
}
for (i = 0; i < ARRAY_SIZE(helper_tail); i++) {
if (lduw_le_p(image + offset + 2 + i * 2) != helper_tail[i]) {
break;
}
}
if (i != ARRAY_SIZE(helper_tail)) {
continue;
}
memcpy(image + offset + 4, movs_r0_3, sizeof(movs_r0_3));
qemu_log_mask(LOG_UNIMP,
"s5l8950x.uart: forced debug-uarts=3 after native "
"IMG3 decrypt at 0x%08" HWADDR_PRIx "\n",
load_base + offset + 4);
return true;
}
return false;
}
static bool s5l8950x_aes_read_descriptor(uint32_t address,
hwaddr *buffer,
uint32_t *length)
{
uint8_t descriptor[16];
if (!address ||
dma_memory_read(&address_space_memory, address, descriptor,
sizeof(descriptor),
MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
return false;
}
/*
* A6 DMA addresses are 32-bit physical addresses. ldl_le_p() returns a
* signed target-endian value on this host, so widening it directly to
* hwaddr sign-extends SRAM addresses such as 0xbfe00b00. Preserve the
* descriptor's raw 32-bit bit pattern before widening it.
*/
*buffer = (uint32_t)ldl_le_p(descriptor + 8);
*length = ldl_le_p(descriptor + 12);
return true;
}
static size_t s5l8950x_aes_key_length(uint32_t control)
{
switch ((control & A6_AES_CTL_KEY_BITS_MASK) >> 18) {
case 0:
return 16;
case 1:
return 24;
case 2:
return 32;
default:
return 0;
}
}
static bool s5l8950x_aes_crypt(const uint8_t *source, uint8_t *destination,
size_t length, const uint8_t *key,
size_t key_length, const uint8_t iv[16],
bool cbc, bool encrypt)
{
AES_KEY expanded_key;
uint8_t chain[AES_BLOCK_SIZE];
uint8_t block[AES_BLOCK_SIZE];
if ((encrypt ? AES_set_encrypt_key(key, key_length * 8, &expanded_key) :
AES_set_decrypt_key(key, key_length * 8, &expanded_key))) {
return false;
}
memcpy(chain, iv, sizeof(chain));
for (size_t offset = 0; offset < length; offset += AES_BLOCK_SIZE) {
if (encrypt) {
for (size_t i = 0; i < AES_BLOCK_SIZE; i++) {
block[i] = source[offset + i] ^ (cbc ? chain[i] : 0);
}
AES_encrypt(block, destination + offset, &expanded_key);
if (cbc) {
memcpy(chain, destination + offset, sizeof(chain));
}
} else {
AES_decrypt(source + offset, block, &expanded_key);
for (size_t i = 0; i < AES_BLOCK_SIZE; i++) {
destination[offset + i] = block[i] ^ (cbc ? chain[i] : 0);
}
if (cbc) {
memcpy(chain, source + offset, sizeof(chain));
}
}
}
return true;
}
static bool s5l8950x_aes_process(S5L8950XAESState *s)
{
g_autoptr(GByteArray) source = NULL;
g_autoptr(GByteArray) destination = NULL;
const uint8_t *key;
const char *key_name;
hwaddr source_address;
hwaddr destination_address;
uint32_t source_length;
uint32_t destination_length;
uint32_t key_select;
size_t key_length;
bool cbc;
bool encrypt;
if (!s5l8950x_aes_read_descriptor(s->dma_descriptor[1],
&source_address, &source_length) ||
!s5l8950x_aes_read_descriptor(s->dma_descriptor[2],
&destination_address,
&destination_length)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: invalid DMA descriptor\n");
return false;
}
if (!source_length || source_length != destination_length ||
source_length > A6_AES_MAX_TRANSFER || source_length % 16) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: invalid DMA lengths %u/%u\n",
source_length, destination_length);
return false;
}
key_length = s5l8950x_aes_key_length(s->control);
if (!key_length) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: unsupported key-size selector\n");
return false;
}
key_select = (s->control & A6_AES_CTL_KEY_SELECT_MASK) >> 21;
if (key_select == A6_AES_KEY_SELECT_GID) {
if (!s->gid_key_loaded || key_length != sizeof(s->gid_key)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: GID operation requested without "
"a matching 256-bit lab key\n");
return false;
}
key = s->gid_key;
key_name = "lab-GID";
} else if (key_select == 0 &&
(s->control & A6_AES_CTL_EXPLICIT_KEY)) {
key = s->explicit_key;
key_name = "explicit";
} else {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: unsupported built-in key slot %u\n",
key_select);
return false;
}
cbc = (s->control & A6_AES_CTL_CBC) != 0;
encrypt = (s->control & A6_AES_CTL_ENCRYPT) != 0;
source = g_byte_array_sized_new(source_length);
g_byte_array_set_size(source, source_length);
destination = g_byte_array_sized_new(destination_length);
g_byte_array_set_size(destination, destination_length);
if (dma_memory_read(&address_space_memory, source_address, source->data,
source_length,
MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: DMA source read failed at 0x%08"
HWADDR_PRIx "\n", source_address);
return false;
}
if (!s5l8950x_aes_crypt(source->data, destination->data, source_length,
key, key_length, s->iv, cbc, encrypt)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: AES operation failed\n");
return false;
}
if (s->force_debug_uarts && !encrypt &&
key_select == 0 && (s->control & A6_AES_CTL_EXPLICIT_KEY) &&
!s5l8950x_aes_enable_bootloader_uarts(destination->data,
destination_length,
destination_address)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.uart: debug_enable_uarts helper not found "
"after native IMG3 decrypt\n");
}
if (dma_memory_write(&address_space_memory, destination_address,
destination->data, destination_length,
MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.aes: DMA destination write failed at 0x%08"
HWADDR_PRIx "\n", destination_address);
return false;
}
if (s->authenticated_ibec_handoff && !encrypt && key_select == 0 &&
(s->control & A6_AES_CTL_EXPLICIT_KEY)) {
s5l8950x_aes_queue_ibec_handoff(s, destination->data,
destination_length);
}
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: %s AES-%zu-%s key=%s bytes=%u "
"src=0x%08" HWADDR_PRIx " dst=0x%08" HWADDR_PRIx "\n",
encrypt ? "encrypt" : "decrypt", key_length * 8,
cbc ? "CBC" : "ECB",
key_name, source_length, source_address,
destination_address);
return true;
}
static uint64_t s5l8950x_aes_dma_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XAESState *s = opaque;
unsigned channel = offset / A6_DMA_CHANNEL_SIZE;
hwaddr reg = offset % A6_DMA_CHANNEL_SIZE;
if (channel >= A6_AES_DMA_CHANNELS) {
return 0;
}
switch (reg) {
case A6_DMA_CONTROL:
return s->dma_control[channel];
case A6_DMA_CONFIG:
return s->dma_config[channel];
case A6_DMA_TRANSFER_SIZE:
return s->dma_transfer_size[channel];
case A6_DMA_DESCRIPTOR:
return s->dma_descriptor[channel];
default:
return 0;
}
}
static void s5l8950x_aes_dma_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XAESState *s = opaque;
unsigned channel = offset / A6_DMA_CHANNEL_SIZE;
hwaddr reg = offset % A6_DMA_CHANNEL_SIZE;
if (channel >= A6_AES_DMA_CHANNELS) {
return;
}
switch (reg) {
case A6_DMA_CONTROL:
s->dma_control[channel] = value;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: DMA%u control=0x%08" PRIx64 "\n",
channel, value);
if (value == 2) {
s->dma_started[channel] = false;
} else if (channel && (value & 1)) {
s->dma_started[channel] = true;
if (s->dma_started[1] && s->dma_started[2]) {
s5l8950x_aes_process(s);
s->dma_started[1] = false;
s->dma_started[2] = false;
/* Status 0 is idle/complete; SecureROM polls bits 16..17. */
s->dma_control[1] &= ~(3u << 16);
s->dma_control[2] &= ~(3u << 16);
}
}
break;
case A6_DMA_CONFIG:
s->dma_config[channel] = value;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: DMA%u config=0x%08" PRIx64 "\n",
channel, value);
break;
case A6_DMA_TRANSFER_SIZE:
s->dma_transfer_size[channel] = value;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: DMA%u length=0x%08" PRIx64 "\n",
channel, value);
break;
case A6_DMA_DESCRIPTOR:
s->dma_descriptor[channel] = value;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: DMA%u descriptor=0x%08" PRIx64 "\n",
channel, value);
break;
default:
break;
}
}
static uint64_t s5l8950x_aes_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XAESState *s = opaque;
switch (offset) {
case A6_AES_CAPABILITIES:
return s->capabilities;
case A6_AES_CONTROL:
return s->control;
case A6_AES_IV_BASE ... A6_AES_IV_END - 4:
return ldl_le_p(s->iv + offset - A6_AES_IV_BASE);
case A6_AES_KEY_BASE ... A6_AES_KEY_END - 4:
/* Explicit key registers are write-only on the real device. */
return 0;
default:
return 0;
}
}
static void s5l8950x_aes_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XAESState *s = opaque;
switch (offset) {
case A6_AES_CAPABILITIES:
s->capabilities = value;
break;
case A6_AES_CONTROL:
s->control = value;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: control=0x%08" PRIx64 "\n", value);
break;
case A6_AES_IV_BASE ... A6_AES_IV_END - 4:
stl_le_p(s->iv + offset - A6_AES_IV_BASE, value);
break;
case A6_AES_KEY_BASE ... A6_AES_KEY_END - 4:
stl_le_p(s->explicit_key + offset - A6_AES_KEY_BASE, value);
break;
default:
break;
}
}
static const MemoryRegionOps s5l8950x_aes_dma_ops = {
.read = s5l8950x_aes_dma_read,
.write = s5l8950x_aes_dma_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid.min_access_size = 4,
.valid.max_access_size = 4,
};
static const MemoryRegionOps s5l8950x_aes_ops = {
.read = s5l8950x_aes_read,
.write = s5l8950x_aes_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid.min_access_size = 4,
.valid.max_access_size = 4,
};
static void s5l8950x_aes_reset(DeviceState *dev)
{
S5L8950XAESState *s = S5L8950X_AES(dev);
memset(s->dma_control, 0, sizeof(s->dma_control));
memset(s->dma_config, 0, sizeof(s->dma_config));
memset(s->dma_transfer_size, 0, sizeof(s->dma_transfer_size));
memset(s->dma_descriptor, 0, sizeof(s->dma_descriptor));
memset(s->dma_started, 0, sizeof(s->dma_started));
s->capabilities = 0;
s->control = 0;
memset(s->iv, 0, sizeof(s->iv));
memset(s->explicit_key, 0, sizeof(s->explicit_key));
s->ibec_handoff_queued = false;
}
static void s5l8950x_aes_realize(DeviceState *dev, Error **errp)
{
S5L8950XAESState *s = S5L8950X_AES(dev);
g_autofree gchar *contents = NULL;
gsize length = 0;
g_autoptr(GError) error = NULL;
if (!s->gid_key_path) {
return;
}
if (!g_file_get_contents(s->gid_key_path, &contents, &length, &error)) {
error_setg(errp, "could not load A6 lab GID key '%s': %s",
s->gid_key_path, error->message);
return;
}
if (length != sizeof(s->gid_key)) {
error_setg(errp, "A6 lab GID key '%s' must be exactly 32 bytes",
s->gid_key_path);
return;
}
memcpy(s->gid_key, contents, sizeof(s->gid_key));
s->gid_key_loaded = true;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.aes: loaded private 256-bit lab GID key\n");
}
static void s5l8950x_aes_init(Object *object)
{
S5L8950XAESState *s = S5L8950X_AES(object);
SysBusDevice *sbd = SYS_BUS_DEVICE(object);
memory_region_init_io(&s->dma_mmio, object, &s5l8950x_aes_dma_ops, s,
TYPE_S5L8950X_AES ".dma",
A6_AES_DMA_MMIO_SIZE);
sysbus_init_mmio(sbd, &s->dma_mmio);
memory_region_init_io(&s->aes_mmio, object, &s5l8950x_aes_ops, s,
TYPE_S5L8950X_AES, A6_AES_MMIO_SIZE);
sysbus_init_mmio(sbd, &s->aes_mmio);
}
static const Property s5l8950x_aes_properties[] = {
DEFINE_PROP_STRING("gid-key-file", S5L8950XAESState, gid_key_path),
DEFINE_PROP_BOOL("force-debug-uarts", S5L8950XAESState,
force_debug_uarts, false),
DEFINE_PROP_BOOL("authenticated-ibec-handoff", S5L8950XAESState,
authenticated_ibec_handoff, false),
};
static void s5l8950x_aes_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
dc->realize = s5l8950x_aes_realize;
device_class_set_legacy_reset(dc, s5l8950x_aes_reset);
device_class_set_props(dc, s5l8950x_aes_properties);
}
static const TypeInfo s5l8950x_aes_type_info = {
.name = TYPE_S5L8950X_AES,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(S5L8950XAESState),
.instance_init = s5l8950x_aes_init,
.class_init = s5l8950x_aes_class_init,
};
static void s5l8950x_aes_register_types(void)
{
type_register_static(&s5l8950x_aes_type_info);
}
type_init(s5l8950x_aes_register_types)
+5 -1
View File
@@ -18,7 +18,11 @@ static uint64_t s5l8950x_chipid_read(void *opaque, hwaddr offset, unsigned size)
/* n41ap: CPFM 0x03, SCEP 0x10, BDID 0x00, IBFL 0x00. */
return 0x200D;
case 0x04:
return 0x00;
/*
* SecureROM leaves the high handoff bit asserted before entering
* iBSS. Early iBoot aborts immediately when this latch is clear.
*/
return 0x80000000;
case 0x08:
return 0xDEADBEEF;
case 0x0C:
+137
View File
@@ -0,0 +1,137 @@
/*
* Apple S5L8950X clock/PLL parameter controller.
*
* iBoot programs the ordinary clock registers directly and uses the command
* ports at 0x480..0x4a4 to query PLL parameters. The corresponding status
* ports contain two result bytes plus busy bits. Clock changes complete
* synchronously here; returning a non-zero divider is enough for iBoot to
* derive its early clock tree without inventing asynchronous hardware.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "hw/arm/s5l8950x.h"
#include "hw/core/sysbus.h"
#include "qom/object.h"
#define S5L8950X_CLOCK_REGION_SIZE 0x1000
#define S5L8950X_CLOCK_NUM_REGS \
(S5L8950X_CLOCK_REGION_SIZE / sizeof(uint32_t))
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XClockState, S5L8950X_CLOCK)
struct S5L8950XClockState {
SysBusDevice parent_obj;
MemoryRegion iomem;
uint32_t regs[S5L8950X_CLOCK_NUM_REGS];
};
static uint64_t s5l8950x_clock_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XClockState *s = opaque;
return s->regs[offset / sizeof(uint32_t)];
}
static void s5l8950x_clock_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XClockState *s = opaque;
uint32_t reg = value;
uint32_t selector = (reg >> 8) & 0xff;
uint32_t result = 1;
/*
* Parameter 8 describes the divider layout used by iBoot's early clock
* setup. Its bits 2..5 must encode a value in the accepted 4..6 range;
* the other parameters are ordinary non-zero divisors.
*/
if (selector == 8) {
result = 0x10;
}
s->regs[offset / sizeof(uint32_t)] = reg;
/* Read-command ports: publish the result and leave the busy bit clear. */
switch (offset) {
case 0x480:
s->regs[0x490 / 4] =
(s->regs[0x490 / 4] & ~0x0000ff01u) | (result << 8);
break;
case 0x488:
s->regs[0x490 / 4] =
(s->regs[0x490 / 4] & ~0x00ff0010u) | (result << 16);
break;
case 0x498:
s->regs[0x4a8 / 4] =
(s->regs[0x4a8 / 4] & ~0x0000ff01u) | (result << 8);
break;
case 0x4a0:
s->regs[0x4a8 / 4] =
(s->regs[0x4a8 / 4] & ~0x00ff0010u) | (result << 16);
break;
case 0x484:
case 0x48c:
case 0x49c:
case 0x4a4:
/* Write commands are consumed by the clock engine immediately. */
s->regs[offset / 4] = 0;
break;
default:
break;
}
}
static const MemoryRegionOps s5l8950x_clock_ops = {
.read = s5l8950x_clock_read,
.write = s5l8950x_clock_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4,
},
};
static void s5l8950x_clock_reset(DeviceState *dev)
{
S5L8950XClockState *s = S5L8950X_CLOCK(dev);
memset(s->regs, 0, sizeof(s->regs));
s->regs[0x490 / 4] = 0x00010100;
s->regs[0x4a8 / 4] = 0x00010100;
}
static void s5l8950x_clock_init(Object *obj)
{
S5L8950XClockState *s = S5L8950X_CLOCK(obj);
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
memory_region_init_io(&s->iomem, obj, &s5l8950x_clock_ops, s,
TYPE_S5L8950X_CLOCK,
S5L8950X_CLOCK_REGION_SIZE);
sysbus_init_mmio(sbd, &s->iomem);
}
static void s5l8950x_clock_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
device_class_set_legacy_reset(dc, s5l8950x_clock_reset);
}
static const TypeInfo s5l8950x_clock_info = {
.name = TYPE_S5L8950X_CLOCK,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(S5L8950XClockState),
.instance_init = s5l8950x_clock_init,
.class_init = s5l8950x_clock_class_init,
};
static void s5l8950x_clock_register_types(void)
{
type_register_static(&s5l8950x_clock_info);
}
type_init(s5l8950x_clock_register_types)
+115
View File
@@ -0,0 +1,115 @@
/*
* Apple S5L8950X I2C controller (minimal polled-transfer model)
*
* Early iBoot uses the three controllers while bringing up board devices.
* The target devices are not modelled yet, so reads return zero, but command
* FIFO writes complete synchronously and without a NAK. This is sufficient
* to preserve the firmware's normal timeout/error paths while allowing the
* boot chain to continue through board discovery.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "hw/arm/s5l8950x.h"
#include "hw/core/irq.h"
#include "hw/core/sysbus.h"
#include "qom/object.h"
#define S5L8950X_I2C_REGION_SIZE 0x1000
#define S5L8950X_I2C_STATUS 0x14
#define S5L8950X_I2C_STATUS_DONE BIT(27)
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XI2CState, S5L8950X_I2C)
struct S5L8950XI2CState {
SysBusDevice parent_obj;
MemoryRegion iomem;
qemu_irq irq;
uint32_t regs[S5L8950X_I2C_REGION_SIZE / sizeof(uint32_t)];
};
static uint64_t s5l8950x_i2c_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XI2CState *s = opaque;
uint32_t value = s->regs[offset / sizeof(uint32_t)];
if (offset == S5L8950X_I2C_STATUS) {
value |= S5L8950X_I2C_STATUS_DONE;
}
return value;
}
static void s5l8950x_i2c_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XI2CState *s = opaque;
if (offset == S5L8950X_I2C_STATUS) {
s->regs[offset / sizeof(uint32_t)] &= ~value;
if (!(s->regs[offset / sizeof(uint32_t)] &
S5L8950X_I2C_STATUS_DONE)) {
qemu_irq_lower(s->irq);
}
return;
}
s->regs[offset / sizeof(uint32_t)] = value;
if (offset == 0) {
s->regs[S5L8950X_I2C_STATUS / sizeof(uint32_t)] |=
S5L8950X_I2C_STATUS_DONE;
qemu_irq_raise(s->irq);
}
}
static const MemoryRegionOps s5l8950x_i2c_ops = {
.read = s5l8950x_i2c_read,
.write = s5l8950x_i2c_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4,
},
};
static void s5l8950x_i2c_reset(DeviceState *dev)
{
S5L8950XI2CState *s = S5L8950X_I2C(dev);
memset(s->regs, 0, sizeof(s->regs));
qemu_irq_lower(s->irq);
}
static void s5l8950x_i2c_init(Object *obj)
{
S5L8950XI2CState *s = S5L8950X_I2C(obj);
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
memory_region_init_io(&s->iomem, obj, &s5l8950x_i2c_ops, s,
TYPE_S5L8950X_I2C, S5L8950X_I2C_REGION_SIZE);
sysbus_init_mmio(sbd, &s->iomem);
sysbus_init_irq(sbd, &s->irq);
}
static void s5l8950x_i2c_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
device_class_set_legacy_reset(dc, s5l8950x_i2c_reset);
}
static const TypeInfo s5l8950x_i2c_info = {
.name = TYPE_S5L8950X_I2C,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(S5L8950XI2CState),
.instance_init = s5l8950x_i2c_init,
.class_init = s5l8950x_i2c_class_init,
};
static void s5l8950x_i2c_register_types(void)
{
type_register_static(&s5l8950x_i2c_info);
}
type_init(s5l8950x_i2c_register_types)
+130
View File
@@ -0,0 +1,130 @@
/*
* Apple S5L8950X DRAM controller training/status model.
*
* iBEC programs the timing registers and then waits for the controller and
* PHY training state in register zero. QEMU's RAM is already usable, so the
* training phases complete synchronously while ordinary register values are
* retained for debugger inspection.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "hw/arm/s5l8950x.h"
#include "hw/core/sysbus.h"
#include "qom/object.h"
#define S5L8950X_MEMCTL_REGION_SIZE 0x1000
#define S5L8950X_MEMCTL_NUM_REGS \
(S5L8950X_MEMCTL_REGION_SIZE / sizeof(uint32_t))
/* Controller ready, PHY ready and all low training stages complete. */
#define S5L8950X_MEMCTL_READY 0x801003ffu
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XMemctlState, S5L8950X_MEMCTL)
struct S5L8950XMemctlState {
SysBusDevice parent_obj;
MemoryRegion iomem;
uint32_t regs[S5L8950X_MEMCTL_NUM_REGS];
};
static uint64_t s5l8950x_memctl_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XMemctlState *s = opaque;
if (offset == 0) {
uint32_t status = s->regs[0] | S5L8950X_MEMCTL_READY;
/*
* The 0b101 command starts a training phase whose lane bits are
* active-low. iBEC clears the command before waiting for them to
* return to the completed state.
*/
if ((s->regs[0x14 / 4] & 5) == 5) {
status &= ~0x000003f0u;
}
/*
* Register 0x08 bit 31 requests a controller state transition. The
* corresponding completion state is exposed in status bit 10 and is
* cleared again when iBEC drops the request.
*/
if (s->regs[0x08 / 4] & BIT(31)) {
status |= BIT(10);
} else {
status &= ~BIT(10);
}
return status;
}
/*
* The PHY command/status block starts at 0x34. Its status register at
* 0x44 reports both command-complete stages once the programmed timing
* values have been accepted.
*/
if (offset == 0x44) {
return s->regs[offset / sizeof(uint32_t)] | 0x00500000u;
}
return s->regs[offset / sizeof(uint32_t)];
}
static void s5l8950x_memctl_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XMemctlState *s = opaque;
s->regs[offset / sizeof(uint32_t)] = value;
}
static const MemoryRegionOps s5l8950x_memctl_ops = {
.read = s5l8950x_memctl_read,
.write = s5l8950x_memctl_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4,
},
};
static void s5l8950x_memctl_reset(DeviceState *dev)
{
S5L8950XMemctlState *s = S5L8950X_MEMCTL(dev);
memset(s->regs, 0, sizeof(s->regs));
}
static void s5l8950x_memctl_init(Object *obj)
{
S5L8950XMemctlState *s = S5L8950X_MEMCTL(obj);
SysBusDevice *sbd = SYS_BUS_DEVICE(obj);
memory_region_init_io(&s->iomem, obj, &s5l8950x_memctl_ops, s,
TYPE_S5L8950X_MEMCTL,
S5L8950X_MEMCTL_REGION_SIZE);
sysbus_init_mmio(sbd, &s->iomem);
}
static void s5l8950x_memctl_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
device_class_set_legacy_reset(dc, s5l8950x_memctl_reset);
}
static const TypeInfo s5l8950x_memctl_info = {
.name = TYPE_S5L8950X_MEMCTL,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(S5L8950XMemctlState),
.instance_init = s5l8950x_memctl_init,
.class_init = s5l8950x_memctl_class_init,
};
static void s5l8950x_memctl_register_types(void)
{
type_register_static(&s5l8950x_memctl_info);
}
type_init(s5l8950x_memctl_register_types)
+285
View File
@@ -0,0 +1,285 @@
/*
* Apple A6 (S5L8950X) public-key accelerator.
*
* The A6 SecureROM uses this block as a modular-arithmetic coprocessor while
* validating the RSA-2048/RSA-1024 certificate chain embedded in Image3.
* Its software driver stages little-endian operands in the 2 KiB operand
* window and drives a fixed sequence of commands for the public exponent
* 65537. This functional model completes that sequence atomically when all
* operands have been staged; the remaining commands retain the computed
* result in operand slot 1, exactly where the ROM reads it back.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "hw/misc/s5l8950x-pke.h"
#include "hw/core/sysbus.h"
#include "qemu/log.h"
#include "qemu/module.h"
#define A6_PKE_MMIO_SIZE 0x1000
#define A6_PKE_OPERAND_BASE 0x0800
#define A6_PKE_OPERAND_SIZE 0x0800
#define A6_PKE_CONFIG 0x0000
#define A6_PKE_START 0x0008
#define A6_PKE_COMMAND 0x000c
#define A6_PKE_STATUS 0x0010
#define A6_PKE_CONTROL 0x0014
#define A6_PKE_RSA_2048_STAGE 0x01050003
#define A6_PKE_RSA_1024_STAGE 0x010e0003
#define A6_PKE_RSA_STAGE_START 9
#define A6_PKE_MAX_BYTES 256
#define A6_PKE_MAX_LIMBS (A6_PKE_MAX_BYTES / sizeof(uint32_t))
#define A6_PKE_PUBLIC_EXPONENT 65537u
struct S5L8950XPKEState {
SysBusDevice parent_obj;
MemoryRegion iomem;
uint32_t config;
uint32_t start;
uint32_t command;
uint32_t status;
uint32_t control;
uint8_t operands[A6_PKE_OPERAND_SIZE];
bool rsa_result_ready;
};
static int s5l8950x_pke_compare(const uint32_t *a, const uint32_t *b,
size_t limbs)
{
for (size_t i = limbs; i-- > 0;) {
if (a[i] != b[i]) {
return a[i] > b[i] ? 1 : -1;
}
}
return 0;
}
static void s5l8950x_pke_subtract(uint32_t *a, const uint32_t *b,
size_t limbs)
{
uint64_t borrow = 0;
for (size_t i = 0; i < limbs; i++) {
uint64_t subtrahend = (uint64_t)b[i] + borrow;
uint64_t minuend = a[i];
a[i] = minuend - subtrahend;
borrow = minuend < subtrahend;
}
}
/* Both operands must be reduced. Their sum then needs at most one subtract. */
static void s5l8950x_pke_add_mod(uint32_t *a, const uint32_t *b,
const uint32_t *modulus, size_t limbs)
{
uint64_t carry = 0;
for (size_t i = 0; i < limbs; i++) {
uint64_t sum = (uint64_t)a[i] + b[i] + carry;
a[i] = sum;
carry = sum >> 32;
}
if (carry || s5l8950x_pke_compare(a, modulus, limbs) >= 0) {
s5l8950x_pke_subtract(a, modulus, limbs);
}
}
static void s5l8950x_pke_mod_mul(uint32_t *out, const uint32_t *a,
const uint32_t *b,
const uint32_t *modulus, size_t limbs)
{
uint32_t result[A6_PKE_MAX_LIMBS] = { 0 };
uint32_t current[A6_PKE_MAX_LIMBS] = { 0 };
memcpy(current, a, limbs * sizeof(*a));
while (s5l8950x_pke_compare(current, modulus, limbs) >= 0) {
s5l8950x_pke_subtract(current, modulus, limbs);
}
for (size_t bit = 0; bit < limbs * 32; bit++) {
if (b[bit / 32] & BIT(bit % 32)) {
s5l8950x_pke_add_mod(result, current, modulus, limbs);
}
s5l8950x_pke_add_mod(current, current, modulus, limbs);
}
memcpy(out, result, limbs * sizeof(*out));
}
static bool s5l8950x_pke_rsa_public(S5L8950XPKEState *s)
{
uint32_t modulus[A6_PKE_MAX_LIMBS] = { 0 };
uint32_t base[A6_PKE_MAX_LIMBS] = { 0 };
uint32_t result[A6_PKE_MAX_LIMBS] = { 0 };
uint32_t squared[A6_PKE_MAX_LIMBS] = { 0 };
size_t width = 64 * ((s->config & 3) + 1);
size_t limbs;
uint32_t exponent = A6_PKE_PUBLIC_EXPONENT;
if (width > A6_PKE_MAX_BYTES || width * 2 > sizeof(s->operands)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.pke: unsupported operand width %zu\n",
width);
return false;
}
limbs = width / sizeof(uint32_t);
for (size_t i = 0; i < limbs; i++) {
modulus[i] = ldl_le_p(s->operands + i * sizeof(uint32_t));
base[i] = ldl_le_p(s->operands + width +
i * sizeof(uint32_t));
}
if (!(modulus[0] & 1) ||
!s5l8950x_pke_compare(modulus, result, limbs)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.pke: invalid RSA modulus\n");
return false;
}
result[0] = 1;
while (exponent) {
if (exponent & 1) {
s5l8950x_pke_mod_mul(result, result, base, modulus, limbs);
}
exponent >>= 1;
if (exponent) {
s5l8950x_pke_mod_mul(squared, base, base, modulus, limbs);
memcpy(base, squared, limbs * sizeof(*base));
}
}
for (size_t i = 0; i < limbs; i++) {
stl_le_p(s->operands + width + i * sizeof(uint32_t), result[i]);
}
s->rsa_result_ready = true;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.pke: completed RSA-%zu public operation\n",
width * 8);
return true;
}
static uint64_t s5l8950x_pke_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XPKEState *s = opaque;
if (offset >= A6_PKE_OPERAND_BASE &&
offset <= A6_PKE_MMIO_SIZE - sizeof(uint32_t)) {
return ldl_le_p(s->operands + offset - A6_PKE_OPERAND_BASE);
}
switch (offset) {
case A6_PKE_CONFIG:
return s->config;
case A6_PKE_START:
return s->start;
case A6_PKE_COMMAND:
return s->command;
case A6_PKE_STATUS:
return s->status;
case A6_PKE_CONTROL:
return s->control;
default:
return 0;
}
}
static void s5l8950x_pke_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XPKEState *s = opaque;
if (offset >= A6_PKE_OPERAND_BASE &&
offset <= A6_PKE_MMIO_SIZE - sizeof(uint32_t)) {
stl_le_p(s->operands + offset - A6_PKE_OPERAND_BASE, value);
return;
}
switch (offset) {
case A6_PKE_CONFIG:
s->config = value;
s->rsa_result_ready = false;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.pke: config=0x%08" PRIx64 "\n", value);
break;
case A6_PKE_START:
s->start = value;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.pke: command=0x%08x start=0x%08" PRIx64
" config=0x%08x\n",
s->command, value, s->config);
if (!s->rsa_result_ready && value == A6_PKE_RSA_STAGE_START &&
(s->command == A6_PKE_RSA_2048_STAGE ||
s->command == A6_PKE_RSA_1024_STAGE)) {
s5l8950x_pke_rsa_public(s);
}
/* The ROM polls bit zero until the command has completed. */
s->start &= ~1u;
break;
case A6_PKE_COMMAND:
s->command = value;
break;
case A6_PKE_STATUS:
s->status = value;
break;
case A6_PKE_CONTROL:
s->control = value;
break;
default:
break;
}
}
static const MemoryRegionOps s5l8950x_pke_ops = {
.read = s5l8950x_pke_read,
.write = s5l8950x_pke_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid.min_access_size = 4,
.valid.max_access_size = 4,
};
static void s5l8950x_pke_reset(DeviceState *dev)
{
S5L8950XPKEState *s = S5L8950X_PKE(dev);
s->config = 0;
s->start = 0;
s->command = 0;
s->status = 0;
s->control = 0;
s->rsa_result_ready = false;
memset(s->operands, 0, sizeof(s->operands));
}
static void s5l8950x_pke_init(Object *object)
{
S5L8950XPKEState *s = S5L8950X_PKE(object);
SysBusDevice *sbd = SYS_BUS_DEVICE(object);
memory_region_init_io(&s->iomem, object, &s5l8950x_pke_ops, s,
TYPE_S5L8950X_PKE, A6_PKE_MMIO_SIZE);
sysbus_init_mmio(sbd, &s->iomem);
}
static void s5l8950x_pke_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
device_class_set_legacy_reset(dc, s5l8950x_pke_reset);
}
static const TypeInfo s5l8950x_pke_type_info = {
.name = TYPE_S5L8950X_PKE,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(S5L8950XPKEState),
.instance_init = s5l8950x_pke_init,
.class_init = s5l8950x_pke_class_init,
};
static void s5l8950x_pke_register_types(void)
{
type_register_static(&s5l8950x_pke_type_info);
}
type_init(s5l8950x_pke_register_types)
+8 -1
View File
@@ -17,6 +17,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XPmgrState, S5L8950X_PMGR)
#define PMGR_SPECIAL_DOMAIN_CTRL 0x100
#define PMGR_SPECIAL_DOMAIN_STATUS 0x2034
#define PMGR_DOMAIN_ACTIVE BIT(30)
#define PMGR_TRANSITION_DONE BIT(29)
#define PMGR_PLL0_CTRL 0x60
#define PMGR_PLL_LOCKED BIT(29)
@@ -57,7 +58,6 @@ static void s5l8950x_pmgr_write(void *opaque, hwaddr offset,
if (offset == PMGR_PLL0_CTRL) {
reg |= PMGR_PLL_LOCKED;
}
s->regs[idx] = reg;
/* Power-domain transitions also complete immediately. */
if (offset < PMGR_DOMAIN_CTRL_STRIDE * PMGR_DOMAIN_COUNT &&
@@ -65,14 +65,21 @@ static void s5l8950x_pmgr_write(void *opaque, hwaddr offset,
uint32_t domain = offset / PMGR_DOMAIN_CTRL_STRIDE;
uint32_t status = (PMGR_DOMAIN_STATUS_BASE / 4) + domain;
if (reg & BIT(31)) {
reg |= PMGR_TRANSITION_DONE;
}
s->regs[status] = deposit32(s->regs[status], 30, 1,
!!(reg & PMGR_DOMAIN_ACTIVE));
} else if (offset == PMGR_SPECIAL_DOMAIN_CTRL) {
uint32_t status = PMGR_SPECIAL_DOMAIN_STATUS / 4;
if (reg & BIT(31)) {
reg |= PMGR_TRANSITION_DONE;
}
s->regs[status] = deposit32(s->regs[status], 30, 1,
!!(reg & PMGR_DOMAIN_ACTIVE));
}
s->regs[idx] = reg;
} else {
qemu_log_mask(LOG_UNIMP, "%s: Out of bounds write to offset 0x%" HWADDR_PRIx
" with value 0x%" PRIx64 "\n", __func__, offset, val);
+223
View File
@@ -0,0 +1,223 @@
/*
* Apple A6 (S5L8950X) SHA-1 accelerator used by the SecureROM.
*
* The ROM supplies already padded 64-byte SHA-1 blocks through the input
* window. QEMU's hash API expects the original message, so this model keeps
* the submitted blocks, validates/removes the standard SHA-1 padding, and
* exposes the resulting five digest words through the hardware registers.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "crypto/hash.h"
#include "hw/core/cpu.h"
#include "hw/misc/s5l8950x-sha1.h"
#include "hw/core/sysbus.h"
#include "qemu/log.h"
#include "qemu/module.h"
#include "qemu/units.h"
#define A6_SHA1_MMIO_SIZE 0x100
#define A6_SHA1_RESET 0x008
#define A6_SHA1_COMMAND 0x00c
#define A6_SHA1_STATUS 0x010
#define A6_SHA1_DIGEST_BASE 0x040
#define A6_SHA1_DIGEST_SIZE QCRYPTO_HASH_DIGEST_LEN_SHA1
#define A6_SHA1_INPUT_BASE 0x080
#define A6_SHA1_BLOCK_SIZE 64
#define A6_SHA1_MAX_MESSAGE (16 * MiB)
struct S5L8950XSHA1State {
SysBusDevice parent_obj;
MemoryRegion iomem;
uint32_t reset;
uint32_t command;
uint32_t status;
uint8_t input[A6_SHA1_BLOCK_SIZE];
uint8_t digest[A6_SHA1_DIGEST_SIZE];
GByteArray *padded;
bool digest_valid;
};
static bool s5l8950x_sha1_finalize(S5L8950XSHA1State *s)
{
uint64_t bit_length;
uint64_t message_length;
uint8_t *digest = s->digest;
size_t digest_length = sizeof(s->digest);
if (!s->padded->len || s->padded->len % A6_SHA1_BLOCK_SIZE ||
s->padded->len < 9) {
return false;
}
bit_length = ldq_be_p(s->padded->data + s->padded->len - 8);
if (bit_length & 7) {
return false;
}
message_length = bit_length / 8;
if (message_length > A6_SHA1_MAX_MESSAGE ||
message_length >= s->padded->len - 8 ||
s->padded->data[message_length] != 0x80) {
return false;
}
for (size_t i = message_length + 1; i < s->padded->len - 8; i++) {
if (s->padded->data[i]) {
return false;
}
}
if (qcrypto_hash_bytes(QCRYPTO_HASH_ALGO_SHA1, s->padded->data,
message_length, &digest, &digest_length,
NULL) < 0 || digest_length != sizeof(s->digest)) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.sha1: SHA-1 calculation failed\n");
return false;
}
s->digest_valid = true;
qemu_log_mask(LOG_UNIMP,
"s5l8950x.sha1: PC=0x%" VADDR_PRIx
" hashed %" PRIu64 " guest bytes\n",
current_cpu ? current_cpu->cc->get_pc(current_cpu) : 0,
message_length);
return true;
}
static void s5l8950x_sha1_submit(S5L8950XSHA1State *s)
{
if (s->padded->len > A6_SHA1_MAX_MESSAGE + 2 * A6_SHA1_BLOCK_SIZE) {
qemu_log_mask(LOG_GUEST_ERROR,
"s5l8950x.sha1: guest message is too large\n");
return;
}
g_byte_array_append(s->padded, s->input, sizeof(s->input));
s->digest_valid = false;
}
static uint64_t s5l8950x_sha1_read(void *opaque, hwaddr offset,
unsigned size)
{
S5L8950XSHA1State *s = opaque;
if (offset >= A6_SHA1_INPUT_BASE &&
offset <= A6_SHA1_MMIO_SIZE - sizeof(uint32_t)) {
return ldl_le_p(s->input + offset - A6_SHA1_INPUT_BASE);
}
if (offset >= A6_SHA1_DIGEST_BASE &&
offset < A6_SHA1_DIGEST_BASE + A6_SHA1_DIGEST_SIZE) {
if (!s->digest_valid) {
s5l8950x_sha1_finalize(s);
}
return ldl_le_p(s->digest + offset - A6_SHA1_DIGEST_BASE);
}
switch (offset) {
case A6_SHA1_RESET:
return s->reset;
case A6_SHA1_COMMAND:
return s->command;
case A6_SHA1_STATUS:
return s->status;
default:
return 0;
}
}
static void s5l8950x_sha1_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
S5L8950XSHA1State *s = opaque;
if (offset >= A6_SHA1_INPUT_BASE &&
offset <= A6_SHA1_MMIO_SIZE - sizeof(uint32_t)) {
stl_le_p(s->input + offset - A6_SHA1_INPUT_BASE, value);
return;
}
switch (offset) {
case A6_SHA1_RESET:
s->reset = value;
if (value & 0x10) {
g_byte_array_set_size(s->padded, 0);
memset(s->input, 0, sizeof(s->input));
memset(s->digest, 0, sizeof(s->digest));
s->digest_valid = false;
}
break;
case A6_SHA1_COMMAND:
s->command = value;
if (value & 1) {
s5l8950x_sha1_submit(s);
}
/* The SecureROM waits for bit zero to clear after each block. */
s->command &= ~1u;
break;
case A6_SHA1_STATUS:
s->status = value;
break;
default:
break;
}
}
static const MemoryRegionOps s5l8950x_sha1_ops = {
.read = s5l8950x_sha1_read,
.write = s5l8950x_sha1_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid.min_access_size = 4,
.valid.max_access_size = 4,
};
static void s5l8950x_sha1_reset(DeviceState *dev)
{
S5L8950XSHA1State *s = S5L8950X_SHA1(dev);
s->reset = 0;
s->command = 0;
s->status = 0;
s->digest_valid = false;
memset(s->input, 0, sizeof(s->input));
memset(s->digest, 0, sizeof(s->digest));
if (s->padded) {
g_byte_array_set_size(s->padded, 0);
}
}
static void s5l8950x_sha1_init(Object *object)
{
S5L8950XSHA1State *s = S5L8950X_SHA1(object);
SysBusDevice *sbd = SYS_BUS_DEVICE(object);
s->padded = g_byte_array_new();
memory_region_init_io(&s->iomem, object, &s5l8950x_sha1_ops, s,
TYPE_S5L8950X_SHA1, A6_SHA1_MMIO_SIZE);
sysbus_init_mmio(sbd, &s->iomem);
}
static void s5l8950x_sha1_finalize_instance(Object *object)
{
S5L8950XSHA1State *s = S5L8950X_SHA1(object);
g_clear_pointer(&s->padded, g_byte_array_unref);
}
static void s5l8950x_sha1_class_init(ObjectClass *klass, const void *data)
{
DeviceClass *dc = DEVICE_CLASS(klass);
device_class_set_legacy_reset(dc, s5l8950x_sha1_reset);
}
static const TypeInfo s5l8950x_sha1_type_info = {
.name = TYPE_S5L8950X_SHA1,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(S5L8950XSHA1State),
.instance_init = s5l8950x_sha1_init,
.instance_finalize = s5l8950x_sha1_finalize_instance,
.class_init = s5l8950x_sha1_class_init,
};
static void s5l8950x_sha1_register_types(void)
{
type_register_static(&s5l8950x_sha1_type_info);
}
type_init(s5l8950x_sha1_register_types)
+1072 -67
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -14,6 +14,9 @@
#define TYPE_S5L8950X_UART "s5l8950x-uart"
#define TYPE_S5L8950X_TIMER "s5l8950x-timer"
#define TYPE_S5L8950X_PMGR "s5l8950x-pmgr"
#define TYPE_S5L8950X_CLOCK "s5l8950x-clock"
#define TYPE_S5L8950X_MEMCTL "s5l8950x-memctl"
#define TYPE_S5L8950X_I2C "s5l8950x-i2c"
#define TYPE_S5L8950X_GPIO "s5l8950x-gpio"
#define TYPE_S5L8950X_USB_PHY "s5l8950x-usb-phy"
#define TYPE_S5L8950X_USB_OTG "s5l8950x-usb-otg"
@@ -31,10 +34,16 @@ struct S5L8950XState {
MemoryRegion sram;
SysBusDevice *chipid;
SysBusDevice *aes;
SysBusDevice *pke;
SysBusDevice *sha1;
SysBusDevice *aic;
SysBusDevice *uart;
SysBusDevice *timer;
SysBusDevice *pmgr;
SysBusDevice *clock;
SysBusDevice *memctl;
SysBusDevice *i2c[3];
SysBusDevice *gpio;
SysBusDevice *usb_phy;
SysBusDevice *usb_otg;
+13
View File
@@ -0,0 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_MISC_S5L8950X_AES_H
#define HW_MISC_S5L8950X_AES_H
#include "hw/core/sysbus.h"
#include "qom/object.h"
#define TYPE_S5L8950X_AES "s5l8950x-aes"
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XAESState, S5L8950X_AES)
#endif /* HW_MISC_S5L8950X_AES_H */
+11
View File
@@ -0,0 +1,11 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_MISC_S5L8950X_PKE_H
#define HW_MISC_S5L8950X_PKE_H
#include "hw/core/sysbus.h"
#define TYPE_S5L8950X_PKE "s5l8950x-pke"
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XPKEState, S5L8950X_PKE)
#endif /* HW_MISC_S5L8950X_PKE_H */
+11
View File
@@ -0,0 +1,11 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifndef HW_MISC_S5L8950X_SHA1_H
#define HW_MISC_S5L8950X_SHA1_H
#include "hw/core/sysbus.h"
#define TYPE_S5L8950X_SHA1 "s5l8950x-sha1"
OBJECT_DECLARE_SIMPLE_TYPE(S5L8950XSHA1State, S5L8950X_SHA1)
#endif /* HW_MISC_S5L8950X_SHA1_H */
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Validate and display the bounded structure of an Apple Image3 file."""
import argparse
import json
from pathlib import Path
import struct
import sys
IMG3_HEADER = struct.Struct("<4s4I")
TAG_HEADER = struct.Struct("<4s2I")
class Img3Error(ValueError):
pass
def fourcc(raw):
return raw[::-1].decode("ascii", errors="replace")
def scalar(tag, payload):
if tag == "TYPE" and len(payload) >= 4:
return fourcc(payload[:4])
if tag == "VERS" and len(payload) > 4:
return payload[4:].split(b"\0", 1)[0].decode("ascii", errors="replace")
if tag in {"CHIP", "BORD", "SEPO", "PROD", "SDOM"} and len(payload) == 4:
return f"0x{struct.unpack('<I', payload)[0]:08x}"
if tag == "ECID" and len(payload) == 8:
return f"0x{struct.unpack('<Q', payload)[0]:016x}"
if tag == "KBAG" and len(payload) >= 8:
key_modifier, key_bits = struct.unpack_from("<II", payload)
expected = 8 + 16 + key_bits // 8
suffix = "" if len(payload) == expected else f" payload={len(payload)}"
return (
f"key_modifier={key_modifier} key_bits={key_bits}" + suffix
)
return None
def parse_img3(data):
if len(data) < IMG3_HEADER.size:
raise Img3Error("file is shorter than the 20-byte Image3 header")
magic, full_size, data_size, shsh_offset, image_type = (
IMG3_HEADER.unpack_from(data)
)
if fourcc(magic) != "Img3":
raise Img3Error(f"bad magic {magic.hex()} (expected Image3)")
if full_size < IMG3_HEADER.size or full_size > len(data):
raise Img3Error(
f"full_size {full_size} is outside the {len(data)}-byte input"
)
if data_size > full_size - IMG3_HEADER.size:
raise Img3Error("data_size extends past full_size")
if shsh_offset > data_size:
raise Img3Error("shsh_offset extends past data_size")
tags = []
offset = IMG3_HEADER.size
end = IMG3_HEADER.size + data_size
while offset < end:
if end - offset < TAG_HEADER.size:
raise Img3Error(f"truncated tag header at 0x{offset:x}")
raw_tag, total_size, payload_size = TAG_HEADER.unpack_from(data, offset)
tag = fourcc(raw_tag)
if total_size < TAG_HEADER.size:
raise Img3Error(f"{tag} at 0x{offset:x} has an invalid total size")
if payload_size > total_size - TAG_HEADER.size:
raise Img3Error(
f"{tag} at 0x{offset:x} has an invalid payload size"
)
if total_size > end - offset:
raise Img3Error(f"{tag} at 0x{offset:x} extends past data_size")
payload_start = offset + TAG_HEADER.size
payload = data[payload_start:payload_start + payload_size]
tags.append({
"tag": tag,
"offset": offset,
"payload_offset": payload_start,
"total_size": total_size,
"payload_size": payload_size,
"padding_size": total_size - TAG_HEADER.size - payload_size,
"value": scalar(tag, payload),
})
offset += total_size
if offset != end:
raise Img3Error("tag sizes do not exactly cover data_size")
return {
"magic": "Img3",
"file_size": len(data),
"full_size": full_size,
"data_size": data_size,
"shsh_offset": shsh_offset,
"image_type": fourcc(struct.pack("<I", image_type)),
"trailing_size": len(data) - full_size,
"tags": tags,
}
def print_human(path, image):
print(f"{path}: {image['magic']} type={image['image_type']}")
print(
f" file={image['file_size']} full={image['full_size']} "
f"data={image['data_size']} shsh_offset=0x{image['shsh_offset']:x} "
f"trailing={image['trailing_size']}"
)
for tag in image["tags"]:
value = f" value={tag['value']}" if tag["value"] is not None else ""
print(
f" 0x{tag['offset']:08x} {tag['tag']:<4} "
f"total={tag['total_size']:<8} data={tag['payload_size']:<8} "
f"padding={tag['padding_size']}{value}"
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("image", type=Path)
parser.add_argument("--json", action="store_true", dest="as_json")
parser.add_argument("--extract-data", type=Path, metavar="PATH")
args = parser.parse_args()
data = args.image.read_bytes()
image = parse_img3(data)
if args.extract_data:
data_tags = [tag for tag in image["tags"] if tag["tag"] == "DATA"]
if len(data_tags) != 1:
raise Img3Error(f"expected one DATA tag, found {len(data_tags)}")
tag = data_tags[0]
start = tag["payload_offset"]
args.extract_data.write_bytes(data[start:start + tag["payload_size"]])
print(f"Extracted {tag['payload_size']} bytes to {args.extract_data}")
if args.as_json:
print(json.dumps(image, indent=2))
else:
print_human(args.image, image)
if __name__ == "__main__":
try:
main()
except (OSError, Img3Error) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Create a lab-signed A6 APTicket and prefix it to a ticketed iBEC IMG3."""
import argparse
import hashlib
import json
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
IMG3_HEADER = struct.Struct("<4s4I")
IMG3_MAGIC = b"3gmI"
SHA1_WITH_RSA = bytes.fromhex("300b06092a864886f70d010105")
SIGNATURE_SIZE = 128
DEFAULT_ECID = 0x200000
DEFAULT_CHIP_ID = 0x8950
DEFAULT_BOARD_ID = 0
DEFAULT_PRODUCTION_MODE = 1
DEFAULT_SECURITY_DOMAIN = 3
DEFAULT_BUILD_IDENTITY = "iBoot-3406.60.10~70"
# iBoot 3406 expects the complete IMG3-era manifest shape even though the
# boot-only lab path validates just the device binding, nonce, and iBEC digest.
# Values for components that are not sent by this emulator are deterministic
# lab placeholders. They do not come from an Apple-issued ticket.
MANIFEST_DIGEST_TAGS = (
7, 8, 9, 10, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28,
75, 78, 79, 80, 228, 229,
)
MANIFEST_FLAG_TAGS = (
48, 49, 50, 51, 54, 55, 56, 57, 59, 60, 61, 62,
84, 85, 86, 231, 232, 233,
)
MANIFEST_BUILD_TAGS = (6, 20, 22)
class TicketBuildError(RuntimeError):
pass
def boot_nonce(value):
try:
decoded = bytes.fromhex(value.removeprefix("0x"))
except ValueError as error:
raise argparse.ArgumentTypeError(
"boot nonce must be hexadecimal"
) from error
if len(decoded) != 8:
raise argparse.ArgumentTypeError(
"boot nonce must contain exactly 8 bytes"
)
return decoded
def encode_length(length):
if length < 0:
raise TicketBuildError("negative DER length")
if length < 0x80:
return bytes((length,))
encoded = length.to_bytes((length.bit_length() + 7) // 8, "big")
return bytes((0x80 | len(encoded),)) + encoded
def encode_tlv(identifier, payload):
return bytes((identifier,)) + encode_length(len(payload)) + payload
def encode_context(tag, payload, constructed=False):
identifier = 0xA0 if constructed else 0x80
if tag < 31:
return encode_tlv(identifier | tag, payload)
encoded_tag = bytearray((tag & 0x7F,))
tag >>= 7
while tag:
encoded_tag.insert(0, 0x80 | (tag & 0x7F))
tag >>= 7
prefix = bytes((identifier | 0x1F,)) + bytes(encoded_tag)
return prefix + encode_length(len(payload)) + payload
def sign_sha1(payload, key):
try:
result = subprocess.run(
["openssl", "dgst", "-sha1", "-sign", str(key)],
input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
)
except FileNotFoundError as error:
raise TicketBuildError("openssl is required") from error
except subprocess.CalledProcessError as error:
detail = error.stderr.decode("utf-8", errors="replace").strip()
raise TicketBuildError(f"openssl failed: {detail}") from error
return result.stdout
def component_digest(component):
if len(component) < IMG3_HEADER.size:
raise TicketBuildError("iBEC is shorter than its IMG3 header")
magic, full_size, data_size, shsh_offset, _ = IMG3_HEADER.unpack_from(
component
)
if magic != IMG3_MAGIC or full_size != len(component):
raise TicketBuildError("iBEC is not a bounded IMG3 container")
if data_size != len(component) - IMG3_HEADER.size:
raise TicketBuildError("iBEC IMG3 data_size is inconsistent")
if shsh_offset != data_size:
raise TicketBuildError("iBEC must use external-ticket IMG3 layout")
return hashlib.sha1(component[12:]).digest()
def placeholder_digest(tag):
label = f"QEMU A6 lab placeholder manifest tag {tag}".encode("ascii")
return hashlib.sha1(label).digest()
def build_manifest(args, digest):
fields = {tag: placeholder_digest(tag) for tag in MANIFEST_DIGEST_TAGS}
fields.update(
{tag: (1).to_bytes(4, "little") for tag in MANIFEST_FLAG_TAGS}
)
fields.update(
{
tag: args.build_identity.encode("ascii")
for tag in MANIFEST_BUILD_TAGS
}
)
fields.update({
1: args.ecid.to_bytes(8, "little"),
2: args.chip_id.to_bytes(4, "little"),
3: args.board_id.to_bytes(4, "little"),
4: args.production_mode.to_bytes(4, "little"),
5: args.security_domain.to_bytes(4, "little"),
18: hashlib.sha1(args.boot_nonce).digest(),
230: digest,
})
return fields
def atomic_write(path, payload):
path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
prefix=path.name + ".", dir=path.parent, delete=False
) as temporary:
temporary.write(payload)
temporary_path = Path(temporary.name)
temporary_path.replace(path)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--component", required=True, type=Path)
parser.add_argument("--identity", required=True, type=Path)
parser.add_argument("--ticket-output", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument(
"--ecid", type=lambda value: int(value, 0), default=DEFAULT_ECID
)
parser.add_argument(
"--chip-id", type=lambda value: int(value, 0), default=DEFAULT_CHIP_ID
)
parser.add_argument(
"--board-id", type=lambda value: int(value, 0), default=DEFAULT_BOARD_ID
)
parser.add_argument(
"--production-mode", type=lambda value: int(value, 0), choices=(0, 1),
default=DEFAULT_PRODUCTION_MODE,
)
parser.add_argument(
"--security-domain", type=lambda value: int(value, 0),
default=DEFAULT_SECURITY_DOMAIN,
)
parser.add_argument("--build-identity", default=DEFAULT_BUILD_IDENTITY)
parser.add_argument(
"--boot-nonce", type=boot_nonce, required=True,
help=(
"raw 8-byte iBSS boot nonce in hexadecimal; stored as the "
"original APTicket [18] SHA-1 nonce binding"
),
)
args = parser.parse_args()
required = (
(args.component, "ticketed iBEC"),
(args.identity / "ticket-leaf-key.pem", "ticket leaf private key"),
(args.identity / "ticket-cert-chain.der", "ticket certificate chain"),
)
for path, description in required:
if not path.is_file():
raise TicketBuildError(f"{description} not found: {path}")
for path in (args.ticket_output, args.output):
if path.exists():
raise TicketBuildError(f"refusing to overwrite output: {path}")
component = args.component.read_bytes()
digest = component_digest(component)
replacements = build_manifest(args, digest)
# DER SET members must be sorted by their complete encoded byte strings.
# No external or previously issued ticket is used as input.
encoded_fields = [
encode_context(tag, payload) for tag, payload in replacements.items()
]
fields = b"".join(sorted(encoded_fields))
manifest_der = encode_tlv(0x31, fields)
signature = sign_sha1(manifest_der, args.identity / "ticket-leaf-key.pem")
if len(signature) != SIGNATURE_SIZE:
raise TicketBuildError(
f"ticket leaf produced {len(signature)} signature bytes; "
"expected 128"
)
chain = (args.identity / "ticket-cert-chain.der").read_bytes()
ticket = encode_tlv(
0x30,
SHA1_WITH_RSA + encode_tlv(0x31, fields)
+ encode_tlv(0x04, signature)
+ encode_context(1, chain, constructed=True),
)
padded_size = (len(ticket) + 63) & ~63
combined = ticket + bytes((0xFF,)) * (padded_size - len(ticket)) + component
atomic_write(args.ticket_output, ticket)
atomic_write(args.output, combined)
metadata = {
"format": 1,
"ticket_size": len(ticket),
"ticket_padded_size": padded_size,
"component_size": len(component),
"combined_size": len(combined),
"ecid": f"0x{args.ecid:016x}",
"chip_id": f"0x{args.chip_id:04x}",
"board_id": args.board_id,
"production_mode": args.production_mode,
"security_domain": args.security_domain,
"build_identity": args.build_identity,
"boot_nonce": args.boot_nonce.hex(),
"boot_nonce_sha1": hashlib.sha1(args.boot_nonce).hexdigest(),
"manifest_tags": sorted(replacements),
"ibec_manifest_tag": 230,
"ibec_sha1_range": "0x0c..EOF",
"ibec_sha1": digest.hex(),
"ticket_sha256": hashlib.sha256(ticket).hexdigest(),
"combined_sha256": hashlib.sha256(combined).hexdigest(),
}
args.output.with_name(args.output.name + ".json").write_text(
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
)
print(
f"Created lab APTicket ({len(ticket)} bytes) and ticketed iBEC "
f"({len(combined)} bytes): {args.output}"
)
print(f"Manifest [230] iBEC SHA-1: {digest.hex()}")
if __name__ == "__main__":
try:
main()
except (OSError, TicketBuildError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Create an isolated A6 IMG3 lab identity and a derived SecureROM image."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import stat
import subprocess
import sys
ROM_SIZE = 0x10000
ROOT_CERT_OFFSET = 0xBFD0
ROOT_CERT_SLOT_SIZE = 1215
ROOT_SUBJECT = (
"/C=US/O=Apple Inc./OU=Apple Certification Authority/CN=Apple Root CA"
)
INTERMEDIATE_SUBJECT = (
"/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/"
"CN=Apple Secure Boot Certification Authority"
)
IMG3_LEAF_SUBJECT = (
"/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/"
"CN=A6-Darwin-Prod-CEPO10-SDOM3-Lab"
)
TICKET_LEAF_SUBJECT = (
"/C=ZZ/O=QEMU A6 Lab/OU=Secure Boot Research/"
"CN=H5P-Darwin-Prod-CEPO1-Ticket-DataCenter"
)
class LabIdentityError(RuntimeError):
pass
def run(command, cwd):
try:
subprocess.run(command, cwd=cwd, check=True)
except FileNotFoundError as error:
raise LabIdentityError(
f"required program not found: {command[0]}"
) from error
except subprocess.CalledProcessError as error:
raise LabIdentityError(
f"command failed with exit status {error.returncode}: "
+ " ".join(command)
) from error
def openssl(*arguments, cwd):
run(["openssl", *arguments], cwd)
def sha256(data):
return hashlib.sha256(data).hexdigest()
def der_object_size(data, offset):
if offset >= len(data) or data[offset] != 0x30:
raise LabIdentityError(
f"no DER SEQUENCE at SecureROM offset 0x{offset:x}"
)
if offset + 2 > len(data):
raise LabIdentityError("truncated DER length")
first = data[offset + 1]
if first < 0x80:
return 2 + first
length_bytes = first & 0x7F
if not 1 <= length_bytes <= 4 or offset + 2 + length_bytes > len(data):
raise LabIdentityError("invalid DER length")
payload_size = int.from_bytes(
data[offset + 2:offset + 2 + length_bytes], "big"
)
return 2 + length_bytes + payload_size
def ensure_clean_output(path):
if path.exists():
if not path.is_dir():
raise LabIdentityError(
f"output exists and is not a directory: {path}"
)
if any(path.iterdir()):
raise LabIdentityError(
f"output directory is not empty: {path}; choose a new directory"
)
else:
path.mkdir(parents=True, mode=0o700)
def create_identity(source_rom, output, config):
source = source_rom.read_bytes()
if len(source) != ROM_SIZE:
raise LabIdentityError(
f"SecureROM must be exactly {ROM_SIZE} bytes, got {len(source)}"
)
embedded_size = der_object_size(source, ROOT_CERT_OFFSET)
if embedded_size != ROOT_CERT_SLOT_SIZE:
raise LabIdentityError(
"unexpected embedded trust anchor size at 0xbfd0: "
f"{embedded_size}, expected {ROOT_CERT_SLOT_SIZE}"
)
ensure_clean_output(output)
os.chmod(output, 0o700)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048",
"-out", "root-key.pem", cwd=output,
)
openssl(
"req", "-new", "-x509", "-sha1", "-days", "3650",
"-set_serial", "0x02", "-key", "root-key.pem",
# iBSS parses the complete fixed 1215-byte root slot. Its public
# subject layout plus the standard policy extension in the config
# preserves that DER size while all trust material remains lab-owned.
"-subj", ROOT_SUBJECT,
"-config", str(config), "-extensions", "v3_root",
"-out", "root.pem", cwd=output,
)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048",
"-out", "intermediate-key.pem", cwd=output,
)
openssl(
"req", "-new", "-sha256", "-key", "intermediate-key.pem",
"-subj", INTERMEDIATE_SUBJECT,
"-config", str(config),
"-out", "intermediate.csr", cwd=output,
)
openssl(
"x509", "-req", "-sha1", "-days", "3650",
"-set_serial", "0xA60010", "-in", "intermediate.csr",
"-CA", "root.pem", "-CAkey", "root-key.pem",
"-extfile", str(config), "-extensions", "v3_intermediate",
"-out", "intermediate.pem", cwd=output,
)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:1024",
"-out", "img3-leaf-key.pem", cwd=output,
)
openssl(
"req", "-new", "-sha256", "-key", "img3-leaf-key.pem",
"-subj", IMG3_LEAF_SUBJECT,
"-config", str(config),
"-out", "img3-leaf.csr", cwd=output,
)
openssl(
"x509", "-req", "-sha1", "-days", "3650",
"-set_serial", "0xA60105", "-in", "img3-leaf.csr",
"-CA", "intermediate.pem", "-CAkey", "intermediate-key.pem",
"-extfile", str(config), "-extensions", "v3_img3_leaf",
"-out", "img3-leaf.pem", cwd=output,
)
openssl(
"genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:1024",
"-out", "ticket-leaf-key.pem", cwd=output,
)
openssl(
"req", "-new", "-sha256", "-key", "ticket-leaf-key.pem",
# iBSS compares this public ticket-class name while certificate trust
# still comes exclusively from the generated lab root and keys.
"-subj", TICKET_LEAF_SUBJECT,
"-config", str(config),
"-out", "ticket-leaf.csr", cwd=output,
)
openssl(
"x509", "-req", "-sha1", "-days", "3650",
"-set_serial", "0xA6010B", "-in", "ticket-leaf.csr",
"-CA", "intermediate.pem", "-CAkey", "intermediate-key.pem",
"-extfile", str(config), "-extensions", "v3_ticket_leaf",
"-out", "ticket-leaf.pem", cwd=output,
)
for name in ("root", "intermediate", "img3-leaf", "ticket-leaf"):
openssl(
"x509", "-in", f"{name}.pem", "-outform", "DER",
"-out", f"{name}.der", cwd=output,
)
openssl(
"verify", "-auth_level", "0", "-ignore_critical",
"-CAfile", "root.pem",
"-untrusted", "intermediate.pem", "img3-leaf.pem", cwd=output,
)
openssl(
"verify", "-auth_level", "0", "-ignore_critical",
"-CAfile", "root.pem",
"-untrusted", "intermediate.pem", "ticket-leaf.pem", cwd=output,
)
root_der = (output / "root.der").read_bytes()
intermediate_der = (output / "intermediate.der").read_bytes()
leaf_der = (output / "img3-leaf.der").read_bytes()
ticket_leaf_der = (output / "ticket-leaf.der").read_bytes()
if len(root_der) > ROOT_CERT_SLOT_SIZE:
raise LabIdentityError(
f"lab root DER is {len(root_der)} bytes and does not fit the "
f"{ROOT_CERT_SLOT_SIZE}-byte SecureROM slot"
)
(output / "cert-chain.der").write_bytes(intermediate_der + leaf_der)
(output / "ticket-cert-chain.der").write_bytes(
intermediate_der + ticket_leaf_der
)
gid_key = os.urandom(32)
(output / "gid-key.bin").write_bytes(gid_key)
patched = bytearray(source)
patched[ROOT_CERT_OFFSET:ROOT_CERT_OFFSET + ROOT_CERT_SLOT_SIZE] = (
root_der + bytes(ROOT_CERT_SLOT_SIZE - len(root_der))
)
derived_rom = output / "s5l8950x-secure-rom-lab.bin"
derived_rom.write_bytes(patched)
for name in (
"root-key.pem", "intermediate-key.pem", "img3-leaf-key.pem",
"ticket-leaf-key.pem", "gid-key.bin",
):
os.chmod(output / name, stat.S_IRUSR | stat.S_IWUSR)
for name in ("intermediate.csr", "img3-leaf.csr", "ticket-leaf.csr"):
(output / name).unlink()
manifest = {
"format": 1,
"source_rom": str(source_rom.resolve()),
"source_rom_sha256": sha256(source),
"derived_rom": derived_rom.name,
"derived_rom_sha256": sha256(patched),
"root_certificate_offset": ROOT_CERT_OFFSET,
"root_certificate_slot_size": ROOT_CERT_SLOT_SIZE,
"root_certificate_der_size": len(root_der),
"root_certificate_sha256": sha256(root_der),
"certificate_chain_der_size": len(intermediate_der) + len(leaf_der),
"ticket_certificate_chain_der_size": (
len(intermediate_der) + len(ticket_leaf_der)
),
"gid_key_bits": 256,
"gid_key_sha256": sha256(gid_key),
}
(output / "identity.json").write_text(
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
return manifest
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source_rom", type=Path)
parser.add_argument("output_directory", type=Path)
args = parser.parse_args()
script_directory = Path(__file__).resolve().parent
config = script_directory / "a6-lab-openssl.cnf"
if not config.is_file():
raise LabIdentityError(f"OpenSSL configuration not found: {config}")
if not args.source_rom.is_file():
raise LabIdentityError(f"SecureROM not found: {args.source_rom}")
if shutil.which("openssl") is None:
raise LabIdentityError("openssl is required")
manifest = create_identity(
args.source_rom.resolve(), args.output_directory.resolve(), config
)
print(f"Lab identity created in {args.output_directory.resolve()}")
print(
"Derived SecureROM SHA-256: " + manifest["derived_rom_sha256"]
)
print(
"GID key: 256-bit private lab value (SHA-256 fingerprint "
+ manifest["gid_key_sha256"] + ")"
)
if __name__ == "__main__":
try:
main()
except (OSError, LabIdentityError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+354
View File
@@ -0,0 +1,354 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Build a signed, optionally GID-wrapped IMG3 for the A6 lab identity."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
IMG3_HEADER = struct.Struct("<4s4I")
TAG_HEADER = struct.Struct("<4s2I")
IMG3_MAGIC = b"3gmI"
SHSH_SIZE = 128
DEFAULT_ECID = 0x200000
DEFAULT_SECURITY_DOMAIN = 3
DEFAULT_PRODUCTION_MODE = 1
DEFAULT_BOARD_ID = 0
DEFAULT_CHIP_EPOCH = 0x10
class Img3BuildError(RuntimeError):
pass
def raw_tag(name):
return name.encode("ascii")[::-1]
def display_tag(value):
return value[::-1].decode("ascii", errors="replace")
def parse_template(data):
if len(data) < IMG3_HEADER.size:
raise Img3BuildError("template is shorter than the IMG3 header")
magic, full_size, data_size, _, image_type = IMG3_HEADER.unpack_from(data)
if magic != IMG3_MAGIC:
raise Img3BuildError("template does not have IMG3 magic")
if full_size > len(data) or data_size > full_size - IMG3_HEADER.size:
raise Img3BuildError("template IMG3 sizes are out of bounds")
tags = []
offset = IMG3_HEADER.size
end = IMG3_HEADER.size + data_size
while offset < end:
if end - offset < TAG_HEADER.size:
raise Img3BuildError(f"truncated tag at 0x{offset:x}")
tag, total_size, payload_size = TAG_HEADER.unpack_from(data, offset)
if (
total_size < TAG_HEADER.size
or payload_size > total_size - TAG_HEADER.size
or total_size > end - offset
):
raise Img3BuildError(f"invalid tag at 0x{offset:x}")
tags.append({
"name": display_tag(tag),
"payload": data[
offset + TAG_HEADER.size:offset + TAG_HEADER.size + payload_size
],
"padding": total_size - TAG_HEADER.size - payload_size,
})
offset += total_size
if offset != end:
raise Img3BuildError("template tag sizes do not cover data_size")
return image_type, tags
def make_tag(name, payload, padding=0):
total_size = TAG_HEADER.size + len(payload) + padding
return (
TAG_HEADER.pack(raw_tag(name), total_size, len(payload))
+ payload
+ bytes(padding)
)
def openssl_filter(arguments, payload):
try:
result = subprocess.run(
["openssl", *arguments], input=payload, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, check=True,
)
except FileNotFoundError as error:
raise Img3BuildError("openssl is required") from error
except subprocess.CalledProcessError as error:
detail = error.stderr.decode("utf-8", errors="replace").strip()
raise Img3BuildError(f"openssl failed: {detail}") from error
return result.stdout
def aes256_cbc(payload, key, iv, decrypt=False):
if len(payload) % 16:
raise Img3BuildError("AES-CBC input must be a multiple of 16 bytes")
arguments = [
"enc", "-aes-256-cbc", "-nopad", "-K", key.hex(), "-iv", iv.hex()
]
if decrypt:
arguments.append("-d")
return openssl_filter(arguments, payload)
def sign_sha1(payload, private_key):
return openssl_filter(
["dgst", "-sha1", "-sign", str(private_key)], payload
)
def build_image(template, payload, identity, ecid, encrypted, ticketed,
security_domain, production_mode, board_id, chip_epoch):
image_type, template_tags = parse_template(template)
chain = None
leaf_key = None
if not ticketed:
chain = (identity / "cert-chain.der").read_bytes()
leaf_key = identity / "img3-leaf-key.pem"
if not leaf_key.is_file():
raise Img3BuildError(f"leaf private key not found: {leaf_key}")
if encrypted and len(payload) % 16:
raise Img3BuildError(
"encrypted DATA length must be a multiple of the AES block size"
)
content_iv = os.urandom(16) if encrypted else None
content_key = os.urandom(32) if encrypted else None
image_data = (
aes256_cbc(payload, content_key, content_iv) if encrypted else payload
)
elements = []
saw_type = False
saw_data = False
for tag in template_tags:
name = tag["name"]
if name in {"KBAG", "ECID", "SHSH", "CERT"}:
continue
if name == "TYPE":
saw_type = True
if name == "DATA":
saw_data = True
padding = (-(TAG_HEADER.size + len(image_data))) % 4
elements.append(make_tag("DATA", image_data, padding))
else:
elements.append(make_tag(name, tag["payload"], tag["padding"]))
if not saw_type or not saw_data:
raise Img3BuildError("template must contain TYPE and DATA tags")
if not ticketed:
# The public IPSW template is unpersonalized and therefore omits the
# device-bound scalar tags that SecureROM requires after SHSH
# validation. The iBEC path carries these values in its external
# APTicket instead, matching the original iOS 10 boot flow.
existing_tags = {tag["name"] for tag in template_tags}
for name, value in (
("SDOM", security_domain),
("PROD", production_mode),
("CEPO", chip_epoch),
("BORD", board_id),
):
if name not in existing_tags:
elements.append(make_tag(name, struct.pack("<I", value)))
if encrypted:
gid_key_path = identity / "gid-key.bin"
gid_key = gid_key_path.read_bytes()
if len(gid_key) != 32:
raise Img3BuildError("lab GID key must be exactly 32 bytes")
clear_keybag = content_iv + content_key
wrapped_keybag = aes256_cbc(clear_keybag, gid_key, bytes(16))
unwrapped_keybag = aes256_cbc(
wrapped_keybag, gid_key, bytes(16), decrypt=True
)
if unwrapped_keybag != clear_keybag:
raise Img3BuildError("internal KBAG AES round-trip failed")
elements.append(
make_tag("KBAG", struct.pack("<II", 1, 256) + wrapped_keybag)
)
before_shsh = b"".join(elements)
if ticketed:
data_size = len(before_shsh)
full_size = IMG3_HEADER.size + data_size
header = IMG3_HEADER.pack(
IMG3_MAGIC, full_size, data_size, data_size, image_type
)
result = header + before_shsh
if encrypted:
decrypted = aes256_cbc(
image_data, content_key, content_iv, decrypt=True
)
if decrypted != payload:
raise Img3BuildError("internal DATA AES round-trip failed")
metadata = {
"format": 1,
"image_type": display_tag(struct.pack("<I", image_type)),
"full_size": full_size,
"data_size": data_size,
"shsh_offset": data_size,
"authentication": "external-apticket",
"encrypted": encrypted,
"kbag_key_modifier": 1 if encrypted else None,
"kbag_key_bits": 256 if encrypted else None,
"apticket_component_range": "0x0c..EOF",
"apticket_component_sha1": hashlib.sha1(result[12:]).hexdigest(),
"img3_sha256": hashlib.sha256(result).hexdigest(),
}
return result, metadata
# Apple's A6 TSS response uses a 64-byte ECID element (8 data + 44 pad).
elements.append(make_tag("ECID", struct.pack("<Q", ecid), 44))
before_shsh = b"".join(elements)
cert_element_size = TAG_HEADER.size + len(chain)
data_size = (
len(before_shsh) + TAG_HEADER.size + SHSH_SIZE + cert_element_size
)
full_size = IMG3_HEADER.size + data_size
header = IMG3_HEADER.pack(
IMG3_MAGIC, full_size, data_size, len(before_shsh), image_type
)
signed_bytes = header[12:] + before_shsh
signature = sign_sha1(signed_bytes, leaf_key)
if len(signature) != SHSH_SIZE:
raise Img3BuildError(
f"IMG3 leaf produced a {len(signature)}-byte signature; "
"expected 128"
)
result = (
header + before_shsh + make_tag("SHSH", signature)
+ make_tag("CERT", chain)
)
if len(result) != full_size:
raise Img3BuildError("internal IMG3 size mismatch")
if encrypted:
decrypted = aes256_cbc(
image_data, content_key, content_iv, decrypt=True
)
if decrypted != payload:
raise Img3BuildError("internal DATA AES round-trip failed")
metadata = {
"format": 1,
"image_type": display_tag(struct.pack("<I", image_type)),
"full_size": full_size,
"data_size": data_size,
"shsh_offset": len(before_shsh),
"ecid": f"0x{ecid:016x}",
"security_domain": security_domain,
"production_mode": production_mode,
"board_id": board_id,
"chip_epoch": chip_epoch,
"authentication": "embedded-shsh",
"encrypted": encrypted,
"kbag_key_modifier": 1 if encrypted else None,
"kbag_key_bits": 256 if encrypted else None,
"signed_range": "0x0c..SHSH",
"signed_sha1": hashlib.sha1(signed_bytes).hexdigest(),
"img3_sha256": hashlib.sha256(result).hexdigest(),
}
return result, metadata
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--template", required=True, type=Path)
parser.add_argument("--payload", required=True, type=Path)
parser.add_argument("--identity", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument(
"--ecid", type=lambda value: int(value, 0), default=DEFAULT_ECID
)
parser.add_argument(
"--security-domain", type=lambda value: int(value, 0),
default=DEFAULT_SECURITY_DOMAIN,
)
parser.add_argument(
"--production-mode", type=lambda value: int(value, 0), choices=(0, 1),
default=DEFAULT_PRODUCTION_MODE,
)
parser.add_argument(
"--board-id", type=lambda value: int(value, 0),
default=DEFAULT_BOARD_ID,
)
parser.add_argument(
"--chip-epoch", type=lambda value: int(value, 0),
default=DEFAULT_CHIP_EPOCH,
)
parser.add_argument("--encrypt", action="store_true")
parser.add_argument(
"--ticketed", action="store_true",
help="omit embedded ECID/SHSH/CERT for an external APTicket (iBEC)",
)
args = parser.parse_args()
for path, description in (
(args.template, "IMG3 template"),
(args.payload, "payload"),
):
if not path.is_file():
raise Img3BuildError(f"{description} not found: {path}")
if not args.ticketed and not (args.identity / "cert-chain.der").is_file():
raise Img3BuildError(
"lab certificate chain not found: "
f"{args.identity / 'cert-chain.der'}"
)
if args.output.exists():
raise Img3BuildError(f"refusing to overwrite output: {args.output}")
image, metadata = build_image(
args.template.read_bytes(), args.payload.read_bytes(),
args.identity.resolve(), args.ecid, args.encrypt, args.ticketed,
args.security_domain, args.production_mode, args.board_id,
args.chip_epoch,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
prefix=args.output.name + ".", dir=args.output.parent, delete=False
) as temporary:
temporary.write(image)
temporary_path = Path(temporary.name)
temporary_path.replace(args.output)
manifest_path = args.output.with_name(args.output.name + ".json")
manifest_path.write_text(
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
)
print(
f"Created {metadata['image_type']} IMG3: {args.output} "
f"({metadata['full_size']} bytes)"
)
print(f"Encrypted DATA/KBAG: {'yes' if args.encrypt else 'no'}")
if args.ticketed:
print(
"APTicket component SHA-1: "
f"{metadata['apticket_component_sha1']}"
)
else:
print(f"Signed SHA-1: {metadata['signed_sha1']}")
if __name__ == "__main__":
try:
main()
except (OSError, Img3BuildError, ValueError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+52
View File
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: GPL-2.0-or-later
[req]
distinguished_name = distinguished_name
prompt = no
# The A6 SecureROM compares the complete ASN.1 Common Name object, including
# its PrintableString tag, for the public Apple secure-boot authority name.
string_mask = default
[distinguished_name]
C = ZZ
O = QEMU A6 Lab
OU = Secure Boot Research
CN = QEMU A6 Lab
[v3_root]
keyUsage = critical, keyCertSign, cRLSign
basicConstraints = critical, CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
# Standard public Apple Root CA policy metadata. Reusing its DER shape keeps
# the generated lab root at the exact 1215-byte A6 trust-anchor size; the
# certificate public key and signature are still generated locally.
2.5.29.32 = DER:30:82:01:04:30:82:01:00:06:09:2A:86:48:86:F7:63:64:05:01:30:81:F2:30:2A:06:08:2B:06:01:05:05:07:02:01:16:1E:68:74:74:70:73:3A:2F:2F:77:77:77:2E:61:70:70:6C:65:2E:63:6F:6D:2F:61:70:70:6C:65:63:61:2F:30:81:C3:06:08:2B:06:01:05:05:07:02:02:30:81:B6:1A:81:B3:52:65:6C:69:61:6E:63:65:20:6F:6E:20:74:68:69:73:20:63:65:72:74:69:66:69:63:61:74:65:20:62:79:20:61:6E:79:20:70:61:72:74:79:20:61:73:73:75:6D:65:73:20:61:63:63:65:70:74:61:6E:63:65:20:6F:66:20:74:68:65:20:74:68:65:6E:20:61:70:70:6C:69:63:61:62:6C:65:20:73:74:61:6E:64:61:72:64:20:74:65:72:6D:73:20:61:6E:64:20:63:6F:6E:64:69:74:69:6F:6E:73:20:6F:66:20:75:73:65:2C:20:63:65:72:74:69:66:69:63:61:74:65:20:70:6F:6C:69:63:79:20:61:6E:64:20:63:65:72:74:69:66:69:63:61:74:69:6F:6E:20:70:72:61:63:74:69:63:65:20:73:74:61:74:65:6D:65:6E:74:73:2E
[v3_intermediate]
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
[v3_img3_leaf]
basicConstraints = critical, CA:false
keyUsage = digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
# Apple IMG3 certificate constraint object for A6 production/Darwin images.
# This is public metadata, not an Apple key. The nested DER object constrains
# CEPO=0x10, SDOM=3, PROD=1 and CHIP=0x8950, matching the emulated
# 0x3f500000 fuse word (0x200d).
1.2.840.113635.100.6.1.1 = critical, DER:04:81:84:33:67:6d:49:84:00:00:00:70:00:00:00:00:00:00:00:74:72:65:63:4f:50:45:43:1c:00:00:00:04:00:00:00:10:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:4d:4f:44:53:1c:00:00:00:04:00:00:00:03:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:44:4f:52:50:1c:00:00:00:04:00:00:00:01:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:50:49:48:43:1c:00:00:00:04:00:00:00:50:89:00:00:00:00:00:00:00:00:00:00:00:00:00:00
[v3_ticket_leaf]
basicConstraints = critical, CA:false
keyUsage = digitalSignature
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
# A6 APTicket signing constraint. The original iBSS decoder requires the
# public ticket OID and constrains CHIP=0x8950, PROD=1 and SDOM=3. These are
# values of the emulated n41ap, while the certificate and key remain private
# to this lab identity.
1.2.840.113635.100.6.1.11 = critical, DER:30:1e:a1:1c:31:1a:82:04:50:89:00:00:84:04:01:00:00:00:85:04:03:00:00:00:9f:81:6b:04:10:00:00:00
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Replace an iBoot payload's embedded IMG3 trust anchor with the lab root."""
import argparse
import hashlib
import json
from pathlib import Path
import sys
import tempfile
ROM_SIZE = 0x10000
ROOT_CERT_OFFSET = 0xBFD0
ROOT_CERT_SLOT_SIZE = 1215
class RootPatchError(RuntimeError):
pass
def der_object_size(data):
if len(data) < 2 or data[0] != 0x30:
raise RootPatchError("lab root is not a DER SEQUENCE")
first = data[1]
if first < 0x80:
return 2 + first
length_bytes = first & 0x7F
if not 1 <= length_bytes <= 4 or 2 + length_bytes > len(data):
raise RootPatchError("lab root has an invalid DER length")
payload_size = int.from_bytes(data[2:2 + length_bytes], "big")
return 2 + length_bytes + payload_size
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source-rom", required=True, type=Path)
parser.add_argument("--identity", required=True, type=Path)
parser.add_argument("--input", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
if args.output.exists():
raise RootPatchError(f"refusing to overwrite output: {args.output}")
source_rom = args.source_rom.read_bytes()
if len(source_rom) != ROM_SIZE:
raise RootPatchError(f"source SecureROM must be {ROM_SIZE} bytes")
source_anchor = source_rom[
ROOT_CERT_OFFSET:ROOT_CERT_OFFSET + ROOT_CERT_SLOT_SIZE
]
lab_root = (args.identity / "root.der").read_bytes()
if der_object_size(lab_root) != len(lab_root):
raise RootPatchError("lab root contains trailing data")
if len(lab_root) > ROOT_CERT_SLOT_SIZE:
raise RootPatchError(
"lab root does not fit the iBoot trust-anchor slot"
)
lab_anchor = lab_root + bytes(ROOT_CERT_SLOT_SIZE - len(lab_root))
payload = args.input.read_bytes()
offsets = []
start = 0
while True:
offset = payload.find(source_anchor, start)
if offset < 0:
break
offsets.append(offset)
start = offset + 1
if len(offsets) != 1:
raise RootPatchError(
f"expected exactly one embedded source root, found {len(offsets)}"
)
offset = offsets[0]
patched = (
payload[:offset]
+ lab_anchor
+ payload[offset + ROOT_CERT_SLOT_SIZE:]
)
args.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
prefix=args.output.name + ".", dir=args.output.parent, delete=False
) as temporary:
temporary.write(patched)
temporary_path = Path(temporary.name)
temporary_path.replace(args.output)
metadata = {
"format": 1,
"input": str(args.input.resolve()),
"output": args.output.name,
"trust_anchor_offset": offset,
"trust_anchor_slot_size": ROOT_CERT_SLOT_SIZE,
"input_sha256": hashlib.sha256(payload).hexdigest(),
"output_sha256": hashlib.sha256(patched).hexdigest(),
"lab_root_sha256": hashlib.sha256(lab_root).hexdigest(),
}
args.output.with_name(args.output.name + ".json").write_text(
json.dumps(metadata, indent=2) + "\n", encoding="utf-8"
)
print(
f"Patched {args.output} ({len(patched)} bytes), "
f"trust anchor at 0x{offset:x}"
)
if __name__ == "__main__":
try:
main()
except (OSError, RootPatchError) as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(2)
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
usage()
{
echo "usage: $0 SECUREROM OUTPUT_DIRECTORY [BOOT_NONCE_HEX]" >&2
exit 2
}
[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage
source_rom=$1
output_dir=$2
boot_nonce=${3:-${A6_BOOT_NONCE:-8d82693c897d1b9d}}
ibss_payload=${A6_IBSS_PAYLOAD:-"$source_dir/firmware/iBSS.iphone5.RELEASE.bin"}
ibec_payload=${A6_IBEC_PAYLOAD:-"$source_dir/firmware/iBEC.iphone5.RELEASE.bin"}
for input in \
"$source_rom" \
"$source_dir/firmware/iBSS.iphone5.RELEASE.dfu" \
"$source_dir/firmware/iBEC.iphone5.RELEASE.dfu" \
"$ibss_payload" \
"$ibec_payload"; do
if [ ! -f "$input" ]; then
echo "required input not found: $input" >&2
exit 1
fi
done
python3 "$script_dir/a6-lab-identity.py" "$source_rom" "$output_dir"
output_dir=$(CDPATH= cd -- "$output_dir" && pwd)
python3 "$script_dir/a6-lab-patch-iboot-root.py" \
--source-rom "$source_rom" --identity "$output_dir" \
--input "$ibss_payload" \
--output "$output_dir/iBSS.lab-root.bin"
python3 "$script_dir/a6-lab-patch-iboot-root.py" \
--source-rom "$source_rom" --identity "$output_dir" \
--input "$ibec_payload" \
--output "$output_dir/iBEC.lab-root.bin"
python3 "$script_dir/a6-lab-img3.py" \
--template "$source_dir/firmware/iBSS.iphone5.RELEASE.dfu" \
--payload "$output_dir/iBSS.lab-root.bin" \
--identity "$output_dir" --encrypt \
--output "$output_dir/iBSS.chain-encrypted.dfu"
python3 "$script_dir/a6-lab-img3.py" \
--template "$source_dir/firmware/iBEC.iphone5.RELEASE.dfu" \
--payload "$output_dir/iBEC.lab-root.bin" \
--identity "$output_dir" --encrypt --ticketed \
--output "$output_dir/iBEC.ticketed-encrypted.img3"
python3 "$script_dir/a6-lab-apticket.py" \
--component "$output_dir/iBEC.ticketed-encrypted.img3" \
--identity "$output_dir" \
--boot-nonce "$boot_nonce" \
--ticket-output "$output_dir/apticket-nonce.der" \
--output "$output_dir/iBEC.chain-nonce-encrypted.dfu"
echo
echo "A6 lab chain created in $output_dir"
echo "Run: A6_LAB_DIR='$output_dir' ./Run-iPhone5-macOS.sh"
+136
View File
@@ -0,0 +1,136 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
build_root=${A6_RECOVERY_BUILD_DIR:-"$source_dir/build/macos-recovery"}
prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"}
downloads="$build_root/downloads"
libirecovery_commit=95dec3aa25b1e30654ca107eb971971f6a216520
libirecovery_sha256=fcd91f2d5c6c3d70bba5b6f790ae37e
libirecovery_sha256=${libirecovery_sha256}dcc2ff9419ac56de67770bb5f1cc2eca9
idevicerestore_commit=540c352c4c44896f7415abef87a166e8bbaea9b0
idevicerestore_sha256=d971449c0838fe6733e6cd18268ffa2e5
idevicerestore_sha256=${idevicerestore_sha256}499b8d9034e10cf2984c2c5835913b3
libirecovery_archive="$downloads/libirecovery-$libirecovery_commit.tar.gz"
idevicerestore_archive="$downloads/idevicerestore-$idevicerestore_commit.tar.gz"
github_base=https://github.com/libimobiledevice
libirecovery_url="$github_base/libirecovery/archive/$libirecovery_commit.tar.gz"
idevicerestore_path="idevicerestore/archive/$idevicerestore_commit.tar.gz"
idevicerestore_url="$github_base/$idevicerestore_path"
fail()
{
echo "error: $*" >&2
exit 1
}
for command_name in curl shasum tar patch autoreconf pkg-config make; do
command -v "$command_name" >/dev/null 2>&1 ||
fail "required command not found: $command_name"
done
if command -v brew >/dev/null 2>&1; then
brew_prefix=$(brew --prefix)
else
brew_prefix=/usr/local
fi
pkg_config_path="$prefix/lib/pkgconfig:$brew_prefix/lib/pkgconfig"
pkg_config_path="$pkg_config_path:$brew_prefix/opt/libusb/lib/pkgconfig"
if [ -n "${PKG_CONFIG_PATH:-}" ]; then
pkg_config_path="$pkg_config_path:$PKG_CONFIG_PATH"
fi
export PKG_CONFIG_PATH=$pkg_config_path
for package in \
libimobiledevice-glue-1.0 \
libimobiledevice-1.0 \
libusbmuxd-2.0 \
libplist-2.0 \
libtatsu-1.0 \
libzip \
libcurl \
zlib; do
pkg-config --exists "$package" || fail \
"missing $package dependency; run: brew install libimobiledevice libusb libzip"
done
mkdir -p "$downloads" "$prefix"
fetch_verified()
{
archive=$1
url=$2
expected=$3
temporary="$archive.tmp"
if [ -f "$archive" ]; then
actual=$(shasum -a 256 "$archive" | awk '{print $1}')
if [ "$actual" = "$expected" ]; then
return
fi
fi
echo "Downloading: $url"
curl -fsSL "$url" -o "$temporary"
actual=$(shasum -a 256 "$temporary" | awk '{print $1}')
[ "$actual" = "$expected" ] ||
fail "unexpected SHA-256 for $url: $actual"
mv "$temporary" "$archive"
}
fetch_verified "$libirecovery_archive" "$libirecovery_url" \
"$libirecovery_sha256"
fetch_verified "$idevicerestore_archive" "$idevicerestore_url" \
"$idevicerestore_sha256"
work_dir=$(mktemp -d "$build_root/source.XXXXXX")
case $work_dir in
"$build_root"/source.*) ;;
*) fail "unexpected temporary directory: $work_dir" ;;
esac
trap 'rm -rf "$work_dir"' EXIT HUP INT TERM
tar -xzf "$libirecovery_archive" -C "$work_dir"
tar -xzf "$idevicerestore_archive" -C "$work_dir"
libirecovery_source="$work_dir/libirecovery-$libirecovery_commit"
idevicerestore_source="$work_dir/idevicerestore-$idevicerestore_commit"
echo "Applying the QEMU backend to libirecovery"
patch -d "$libirecovery_source" -p1 \
-i "$script_dir/patches/libirecovery-qemu.patch"
printf '%s\n' '1.3.1-qemu-a6' > "$libirecovery_source/.tarball-version"
(
cd "$libirecovery_source"
autoreconf -fiv
./configure --prefix="$prefix"
make -j4
make install
)
printf '%s\n' '1.0.0-qemu-a6' > "$idevicerestore_source/.tarball-version"
echo "Applying boot-only mode without filesystem extraction"
patch -d "$idevicerestore_source" -p1 \
-i "$script_dir/patches/idevicerestore-no-restore-fs.patch"
echo "Applying QEMU boot support for a local lab identity"
patch -d "$idevicerestore_source" -p1 \
-i "$script_dir/patches/idevicerestore-qemu-lab-boot.patch"
(
cd "$idevicerestore_source"
autoreconf -fiv
./configure --prefix="$prefix"
make -j4
make install
)
echo
echo "Tools installed in $prefix/bin"
echo "Start QEMU with ./Run-iPhone5-macOS.sh, then run:"
echo " scripts/irecovery-qemu -q"
echo " scripts/idevicerestore-qemu -d -y /path/to/Restore.ipsw"
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""Fetch only A6 boot-chain files from Apple's iOS 10.3.4 IPSW.
The complete IPSW is about 2 GB. This utility reads its ZIP directory with
HTTP range requests, then downloads only BuildManifest.plist and the selected
iBSS/iBEC pair. It does not personalize or decrypt the images.
"""
import argparse
import binascii
import os
from pathlib import Path
import re
import struct
import sys
import urllib.request
import zlib
DEFAULT_URL = (
"https://updates.cdn-apple.com/2019/ios/"
"091-25277-20190722-0C1B94DE-992C-11E9-A2EE-E2C9A77C2E40/"
"iPhone_4.0_32bit_10.3.4_14G61_Restore.ipsw"
)
EOCD = struct.Struct("<4s4H2IH")
CENTRAL = struct.Struct("<4s6H3I5H2I")
LOCAL = struct.Struct("<4s5H3I2H")
class RangeReader:
def __init__(self, url):
self.url = url
self.total_size = None
def read(self, start, end):
request = urllib.request.Request(
self.url,
headers={"Range": f"bytes={start}-{end}"},
)
with urllib.request.urlopen(request, timeout=60) as response:
if response.status != 206:
raise RuntimeError(
f"server ignored byte range {start}-{end} "
f"(HTTP {response.status})"
)
content_range = response.headers.get("Content-Range", "")
match = re.fullmatch(r"bytes (\d+)-(\d+)/(\d+)", content_range)
if not match:
raise RuntimeError(f"invalid Content-Range: {content_range!r}")
actual_start, actual_end, total = map(int, match.groups())
if (actual_start, actual_end) != (start, end):
raise RuntimeError(
f"unexpected byte range {actual_start}-{actual_end}"
)
self.total_size = total
data = response.read(end - start + 1)
if len(data) != end - start + 1:
raise RuntimeError("truncated HTTP range response")
return data
def get_size(self):
self.read(0, 0)
return self.total_size
def read_directory(reader):
total_size = reader.get_size()
tail_size = min(total_size, 65557)
tail_offset = total_size - tail_size
tail = reader.read(tail_offset, total_size - 1)
eocd_offset = tail.rfind(b"PK\x05\x06")
if eocd_offset < 0 or eocd_offset + EOCD.size > len(tail):
raise RuntimeError("ZIP end-of-central-directory record not found")
fields = EOCD.unpack_from(tail, eocd_offset)
(
_, disk, directory_disk, disk_entries, entries,
size, offset, comment,
) = fields
if disk or directory_disk or disk_entries != entries:
raise RuntimeError("multi-disk ZIP archives are unsupported")
if eocd_offset + EOCD.size + comment > len(tail):
raise RuntimeError("truncated ZIP comment")
if offset == 0xFFFFFFFF or size == 0xFFFFFFFF or entries == 0xFFFF:
raise RuntimeError("ZIP64 directory is unsupported")
data = reader.read(offset, offset + size - 1)
result = {}
cursor = 0
for _ in range(entries):
if cursor + CENTRAL.size > len(data):
raise RuntimeError("truncated ZIP central directory")
fields = CENTRAL.unpack_from(data, cursor)
if fields[0] != b"PK\x01\x02":
raise RuntimeError("invalid ZIP central-directory signature")
flags = fields[3]
compressed_size = fields[8]
uncompressed_size = fields[9]
name_length = fields[10]
extra_length = fields[11]
comment_length = fields[12]
local_offset = fields[16]
name_start = cursor + CENTRAL.size
name_end = name_start + name_length
encoding = "utf-8" if flags & 0x800 else "cp437"
name = data[name_start:name_end].decode(encoding)
result[name] = {
"flags": flags,
"method": fields[4],
"crc32": fields[7],
"compressed_size": compressed_size,
"uncompressed_size": uncompressed_size,
"local_offset": local_offset,
}
cursor = name_end + extra_length + comment_length
return result
def extract_entry(reader, entry):
if entry["flags"] & 1:
raise RuntimeError("encrypted ZIP entries are unsupported")
if entry["compressed_size"] == 0xFFFFFFFF:
raise RuntimeError("ZIP64 entries are unsupported")
local_offset = entry["local_offset"]
header = reader.read(local_offset, local_offset + LOCAL.size - 1)
fields = LOCAL.unpack(header)
if fields[0] != b"PK\x03\x04":
raise RuntimeError("invalid ZIP local-header signature")
name_length = fields[9]
extra_length = fields[10]
data_offset = local_offset + LOCAL.size + name_length + extra_length
compressed_size = entry["compressed_size"]
compressed = reader.read(data_offset, data_offset + compressed_size - 1)
if entry["method"] == 0:
data = compressed
elif entry["method"] == 8:
data = zlib.decompress(compressed, -zlib.MAX_WBITS)
else:
raise RuntimeError(f"unsupported ZIP method {entry['method']}")
if len(data) != entry["uncompressed_size"]:
raise RuntimeError("uncompressed size mismatch")
if binascii.crc32(data) & 0xFFFFFFFF != entry["crc32"]:
raise RuntimeError("CRC-32 mismatch")
return data
def write_atomic(path, data):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_bytes(data)
os.replace(temporary, path)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--board", choices=("n41", "n42"), default="n41")
parser.add_argument("--url", default=DEFAULT_URL)
parser.add_argument("--output-dir", type=Path, default=Path("firmware"))
args = parser.parse_args()
reader = RangeReader(args.url)
directory = read_directory(reader)
requested = ["BuildManifest.plist"]
ipsw_board = "iphone5"
for component in ("iBSS", "iBEC"):
candidates = (
f"Firmware/dfu/{component}.{args.board}.RELEASE.dfu",
f"Firmware/dfu/{component}.{args.board}ap.RELEASE.dfu",
f"Firmware/dfu/{component}.{ipsw_board}.RELEASE.dfu",
)
match = next((name for name in candidates if name in directory), None)
if not match:
available = sorted(
name for name in directory
if "/dfu/" in name.lower() and component.lower() in name.lower()
)
raise RuntimeError(
"missing IPSW entry; tried: " + ", ".join(candidates) +
"; available: " + (", ".join(available) or "none")
)
requested.append(match)
for name in requested:
destination = args.output_dir / Path(name).name
print(f"Fetching {name} -> {destination}", flush=True)
write_atomic(destination, extract_entry(reader, directory[name]))
print("Raw Apple images extracted; personalization is still required.")
if __name__ == "__main__":
try:
main()
except Exception as error:
print(f"error: {error}", file=sys.stderr)
sys.exit(1)
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"}
endpoint=${QEMU_USB_ENDPOINT:-127.0.0.1:26050}
lab_dir=${A6_LAB_DIR:-}
binary="$prefix/bin/idevicerestore"
if [ ! -x "$binary" ]; then
echo "QEMU-enabled idevicerestore not found: $binary" >&2
echo "Run scripts/build-macos-recovery-tools.sh first." >&2
exit 1
fi
export LIBIRECOVERY_QEMU=$endpoint
export DYLD_LIBRARY_PATH="$prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
if [ -n "$lab_dir" ]; then
lab_dir=$(CDPATH= cd -- "$lab_dir" && pwd)
for image in iBSS.chain-encrypted.dfu iBEC.chain-nonce-encrypted.dfu; do
if [ ! -f "$lab_dir/$image" ]; then
echo "Lab image not found: $lab_dir/$image" >&2
exit 1
fi
done
export IDEVICERESTORE_QEMU_LAB_DIR=$lab_dir
export IDEVICERESTORE_QEMU_LAB_BOOT_ONLY=1
exec "$binary" -z "$@"
fi
exec "$binary" "$@"
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-or-later
set -eu
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
source_dir=$(CDPATH= cd -- "$script_dir/.." && pwd)
prefix=${A6_RECOVERY_PREFIX:-"$source_dir/build/limd-prefix"}
endpoint=${QEMU_USB_ENDPOINT:-127.0.0.1:26050}
binary="$prefix/bin/irecovery"
if [ ! -x "$binary" ]; then
echo "QEMU-enabled irecovery not found: $binary" >&2
echo "Run scripts/build-macos-recovery-tools.sh first." >&2
exit 1
fi
export LIBIRECOVERY_QEMU=$endpoint
export DYLD_LIBRARY_PATH="$prefix/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
exec "$binary" "$@"
@@ -0,0 +1,12 @@
--- a/src/idevicerestore.c
+++ b/src/idevicerestore.c
@@ -1136,7 +1136,8 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
}
}
- if (needs_os_extraction && !(client->flags & FLAG_SHSHONLY)) {
+ if (needs_os_extraction && !(client->flags & FLAG_SHSHONLY) &&
+ !(client->flags & FLAG_NO_RESTORE)) {
char* tmpf = NULL;
struct stat st;
if (client->cache_dir) {
@@ -0,0 +1,116 @@
--- a/src/dfu.c
+++ b/src/dfu.c
@@ -127,6 +127,9 @@
int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_identity, const char* component)
{
char* path = NULL;
+ const char* qemu_lab_dir = getenv("IDEVICERESTORE_QEMU_LAB_DIR");
+ int qemu_lab_component = qemu_lab_dir && qemu_lab_dir[0] &&
+ (!strcmp(component, "iBSS") || !strcmp(component, "iBEC"));
// Use a specific TSS ticket for the Ap,LocalPolicy component
plist_t tss = client->tss;
@@ -137,7 +140,26 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide
void* component_data = NULL;
size_t component_size = 0;
- if (strcmp(component, "Ap,LocalPolicy") == 0) {
+ if (qemu_lab_component) {
+ const char* filename = !strcmp(component, "iBSS") ?
+ "iBSS.chain-encrypted.dfu" :
+ "iBEC.chain-nonce-encrypted.dfu";
+ size_t path_size = strlen(qemu_lab_dir) + strlen(filename) + 2;
+ path = malloc(path_size);
+ if (!path) {
+ logger(LL_ERROR, "Out of memory\n");
+ return -1;
+ }
+ snprintf(path, path_size, "%s/%s", qemu_lab_dir, filename);
+ if (read_file(path, &component_data, &component_size) < 0) {
+ logger(LL_ERROR, "Unable to read QEMU lab %s from %s\n", component, path);
+ free(path);
+ return -1;
+ }
+ logger(LL_INFO, "Using QEMU lab %s from %s\n", component, path);
+ free(path);
+ path = NULL;
+ } else if (strcmp(component, "Ap,LocalPolicy") == 0) {
// If Ap,LocalPolicy => Inject an empty policy
component_data = malloc(sizeof(lpol_file));
component_size = sizeof(lpol_file);
@@ -168,7 +190,11 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide
void* data = NULL;
size_t size = 0;
- if (personalize_component(client, component, component_data, component_size, tss, &data, &size) < 0) {
+ if (qemu_lab_component) {
+ data = component_data;
+ size = component_size;
+ component_data = NULL;
+ } else if (personalize_component(client, component, component_data, component_size, tss, &data, &size) < 0) {
logger(LL_ERROR, "Unable to get personalized component: %s\n", component);
free(component_data);
return -1;
@@ -176,7 +202,7 @@ int dfu_send_component(struct idevicerestore_client_t* client, plist_t build_ide
free(component_data);
component_data = NULL;
- if (!client->image4supported && client->build_major > 8 && !(client->flags & FLAG_CUSTOM) && !strcmp(component, "iBEC")) {
+ if (!qemu_lab_component && !client->image4supported && client->build_major > 8 && !(client->flags & FLAG_CUSTOM) && !strcmp(component, "iBEC")) {
unsigned char* ticket = NULL;
unsigned int tsize = 0;
if (tss_response_get_ap_ticket(client->tss, &ticket, &tsize) < 0) {
@@ -517,7 +543,8 @@ int dfu_enter_recovery(struct idevicerestore_client_t* client, plist_t build_ide
logger(LL_INFO, "Nonce: ");
logger_dump_hex(LL_INFO, client->nonce, client->nonce_size);
- if (nonce_changed && !(client->flags & FLAG_CUSTOM)) {
+ if (nonce_changed && !(client->flags & FLAG_CUSTOM) &&
+ !getenv("IDEVICERESTORE_QEMU_LAB_DIR")) {
// Welcome iOS5. We have to re-request the TSS with our nonce.
plist_free(client->tss);
if (get_tss_response(client, build_identity, &client->tss) < 0) {
--- a/src/idevicerestore.c
+++ b/src/idevicerestore.c
@@ -682,6 +682,11 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
tss_enabled = 0;
logger(LL_INFO, "Custom firmware requested; TSS has been disabled.\n");
}
+ if (getenv("IDEVICERESTORE_QEMU_LAB_DIR")) {
+ /* The QEMU wrapper supplies locally personalized iBSS/iBEC images. */
+ tss_enabled = 0;
+ logger(LL_INFO, "QEMU lab boot requested; external TSS has been disabled.\n");
+ }
if (client->mode == MODE_RESTORE) {
if (!(client->flags & FLAG_ALLOW_RESTORE_MODE)) {
@@ -1111,11 +1116,16 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
/* check if all components we need are actually there */
logger(LL_INFO, "Checking IPSW for required components...\n");
- if (build_identity_check_components_in_ipsw(build_identity, client->ipsw) < 0) {
+ if (!getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY") &&
+ build_identity_check_components_in_ipsw(build_identity, client->ipsw) < 0) {
logger(LL_ERROR, "Could not find all required components in IPSW %s\n", client->ipsw->path);
return -1;
}
- logger(LL_INFO, "All required components found in IPSW\n");
+ if (getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY")) {
+ logger(LL_INFO, "QEMU lab boot only needs BuildManifest plus local iBSS/iBEC.\n");
+ } else {
+ logger(LL_INFO, "All required components found in IPSW\n");
+ }
/* Get OS (filesystem) name from build identity */
char* os_path = NULL;
@@ -1466,6 +1476,10 @@ int idevicerestore_start(struct idevicerestore_client_t* client)
mutex_unlock(&client->device_event_mutex);
}
idevicerestore_progress(client, RESTORE_STEP_PREPARE, 0.5);
+ if (getenv("IDEVICERESTORE_QEMU_LAB_BOOT_ONLY")) {
+ logger(LL_INFO, "QEMU lab iBEC is running in Recovery mode.\n");
+ return 0;
+ }
if (client->flags & FLAG_QUIT) {
return -1;
}
+700
View File
@@ -0,0 +1,700 @@
--- a/src/libirecovery.c
+++ b/src/libirecovery.c
@@ -31,6 +31,12 @@
#include <ctype.h>
#include <unistd.h>
#include <sys/stat.h>
+#ifndef _WIN32
+#include <errno.h>
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#endif
#include <libimobiledevice-glue/collection.h>
#include <libimobiledevice-glue/thread.h>
@@ -119,6 +125,11 @@
int usb_alt_interface;
unsigned int mode;
int isKIS;
+ int qemu_transport;
+ int qemu_fd;
+ uint32_t qemu_request_id;
+ uint32_t qemu_generation;
+ uint32_t qemu_poll_timeout_ms;
struct irecv_device_info device_info;
#ifndef USE_DUMMY
#ifndef _WIN32
@@ -163,6 +173,214 @@
#define debug(...) if (libirecovery_debug) fprintf(stderr, __VA_ARGS__)
static int libirecovery_debug = 0;
+
+#ifndef _WIN32
+#define QA6_USB_MAGIC 0x55364151u
+#define QA6_USB_PROTOCOL_VERSION 1u
+#define QA6_USB_MESSAGE_REQUEST 1u
+#define QA6_USB_MESSAGE_RESPONSE 2u
+#define QA6_USB_MESSAGE_INFO_REQUEST 3u
+#define QA6_USB_MESSAGE_INFO_RESPONSE 4u
+#define QA6_USB_MESSAGE_BULK_REQUEST 5u
+#define QA6_USB_MESSAGE_BULK_RESPONSE 6u
+#define QA6_USB_MESSAGE_RESET_REQUEST 7u
+#define QA6_USB_MESSAGE_RESET_RESPONSE 8u
+#define QA6_USB_STATUS_SUCCESS 0
+#define QA6_USB_STATUS_STALL -1
+#define QA6_USB_STATUS_DISCONNECTED -2
+#define QA6_USB_STATUS_PROTOCOL -3
+#define QA6_USB_STATUS_UNSUPPORTED -4
+#define QA6_USB_MAX_PAYLOAD 65535u
+#define QA6_USB_SERIAL_MAX 256u
+#define QA6_USB_DFU_STATE_ERROR 10u
+#define QA6_USB_DFU_STATE_WAIT_RESET 8u
+
+#pragma pack(push, 1)
+typedef struct qemu_a6_usb_frame_header {
+ uint32_t magic;
+ uint16_t version;
+ uint16_t type;
+ uint32_t request_id;
+ int32_t status;
+ uint32_t payload_length;
+ uint32_t transfer_length;
+ uint8_t setup[8];
+} qemu_a6_usb_frame_header;
+
+typedef struct qemu_a6_usb_device_info {
+ uint32_t generation;
+ uint16_t vendor_id;
+ uint16_t product_id;
+ uint8_t device_class;
+ uint8_t device_subclass;
+ uint8_t device_protocol;
+ uint8_t is_dfu;
+ char serial[QA6_USB_SERIAL_MAX];
+} qemu_a6_usb_device_info;
+#pragma pack(pop)
+
+static const char *qemu_a6_endpoint(void)
+{
+ const char *endpoint = getenv("LIBIRECOVERY_QEMU");
+
+ return endpoint && endpoint[0] ? endpoint : NULL;
+}
+
+static int qemu_a6_send_all(int fd, const void *data, size_t length)
+{
+ const uint8_t *cursor = data;
+
+ while (length) {
+ ssize_t sent = send(fd, cursor, length, 0);
+ if (sent < 0 && errno == EINTR) {
+ continue;
+ }
+ if (sent <= 0) {
+ return -1;
+ }
+ cursor += sent;
+ length -= sent;
+ }
+ return 0;
+}
+
+static int qemu_a6_recv_all(int fd, void *data, size_t length)
+{
+ uint8_t *cursor = data;
+
+ while (length) {
+ ssize_t received = recv(fd, cursor, length, 0);
+ if (received < 0 && errno == EINTR) {
+ continue;
+ }
+ if (received < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
+ return -2;
+ }
+ if (received <= 0) {
+ return -1;
+ }
+ cursor += received;
+ length -= received;
+ }
+ return 0;
+}
+
+static int qemu_a6_connect(void)
+{
+ const char *endpoint = qemu_a6_endpoint();
+ char host[256];
+ char port[16];
+ const char *separator;
+ struct addrinfo hints;
+ struct addrinfo *addresses = NULL;
+ struct addrinfo *address;
+ struct timeval timeout = { 1, 0 };
+ int fd = -1;
+
+ if (!endpoint || !(separator = strrchr(endpoint, ':')) ||
+ separator == endpoint || !separator[1] ||
+ (size_t)(separator - endpoint) >= sizeof(host) ||
+ strlen(separator + 1) >= sizeof(port)) {
+ return -1;
+ }
+ memcpy(host, endpoint, separator - endpoint);
+ host[separator - endpoint] = '\0';
+ strcpy(port, separator + 1);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ if (getaddrinfo(host, port, &hints, &addresses) != 0) {
+ return -1;
+ }
+ for (address = addresses; address; address = address->ai_next) {
+ fd = socket(address->ai_family, address->ai_socktype,
+ address->ai_protocol);
+ if (fd < 0) {
+ continue;
+ }
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
+ setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
+ if (connect(fd, address->ai_addr, address->ai_addrlen) == 0) {
+ break;
+ }
+ close(fd);
+ fd = -1;
+ }
+ freeaddrinfo(addresses);
+ return fd;
+}
+
+static int qemu_a6_exchange(int fd, qemu_a6_usb_frame_header *request,
+ const void *out_data,
+ qemu_a6_usb_frame_header *response,
+ void *in_data, size_t in_capacity)
+{
+ int receive_result;
+
+ if (qemu_a6_send_all(fd, request, sizeof(*request)) < 0 ||
+ (request->payload_length &&
+ qemu_a6_send_all(fd, out_data, request->payload_length) < 0)) {
+ return IRECV_E_NO_DEVICE;
+ }
+ receive_result = qemu_a6_recv_all(fd, response, sizeof(*response));
+ if (receive_result == -2) {
+ return IRECV_E_TIMEOUT;
+ }
+ if (receive_result < 0) {
+ return IRECV_E_NO_DEVICE;
+ }
+ if (response->magic != QA6_USB_MAGIC ||
+ response->version != QA6_USB_PROTOCOL_VERSION ||
+ response->request_id != request->request_id ||
+ response->payload_length > QA6_USB_MAX_PAYLOAD ||
+ response->payload_length > in_capacity) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ if (response->payload_length &&
+ qemu_a6_recv_all(fd, in_data, response->payload_length) < 0) {
+ return IRECV_E_NO_DEVICE;
+ }
+ switch (response->status) {
+ case QA6_USB_STATUS_SUCCESS:
+ return IRECV_E_SUCCESS;
+ case QA6_USB_STATUS_STALL:
+ return IRECV_E_PIPE;
+ case QA6_USB_STATUS_DISCONNECTED:
+ return IRECV_E_NO_DEVICE;
+ case QA6_USB_STATUS_UNSUPPORTED:
+ return IRECV_E_UNSUPPORTED;
+ case QA6_USB_STATUS_PROTOCOL:
+ default:
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+}
+
+static int qemu_a6_read_info_fd(int fd, qemu_a6_usb_device_info *info)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ int error;
+
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_INFO_REQUEST;
+ request.request_id = 1;
+ error = qemu_a6_exchange(fd, &request, NULL, &response,
+ info, sizeof(*info));
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_INFO_RESPONSE ||
+ response.payload_length != sizeof(*info) ||
+ response.transfer_length != sizeof(*info)) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ info->serial[sizeof(info->serial) - 1] = '\0';
+ return IRECV_E_SUCCESS;
+}
+#endif
#ifndef USE_DUMMY
#ifndef _WIN32
#ifndef HAVE_IOKIT
@@ -930,6 +1149,199 @@
}
}
+#ifndef _WIN32
+static irecv_error_t qemu_a6_open_with_ecid(irecv_client_t *pclient,
+ uint64_t ecid)
+{
+ qemu_a6_usb_device_info info;
+ irecv_client_t client;
+ int fd;
+ int error;
+
+ *pclient = NULL;
+ fd = qemu_a6_connect();
+ if (fd < 0) {
+ return IRECV_E_UNABLE_TO_CONNECT;
+ }
+ memset(&info, 0, sizeof(info));
+ error = qemu_a6_read_info_fd(fd, &info);
+ if (error != IRECV_E_SUCCESS) {
+ close(fd);
+ return error;
+ }
+ if (info.vendor_id != APPLE_VENDOR_ID ||
+ (info.product_id != IRECV_K_DFU_MODE &&
+ info.product_id != IRECV_K_WTF_MODE &&
+ info.product_id != IRECV_K_PORT_DFU_MODE &&
+ (info.product_id < IRECV_K_RECOVERY_MODE_1 ||
+ info.product_id > IRECV_K_RECOVERY_MODE_4))) {
+ close(fd);
+ return IRECV_E_NO_DEVICE;
+ }
+
+ client = calloc(1, sizeof(*client));
+ if (!client) {
+ close(fd);
+ return IRECV_E_OUT_OF_MEMORY;
+ }
+ client->qemu_transport = 1;
+ client->qemu_fd = fd;
+ client->qemu_request_id = 1;
+ client->qemu_generation = info.generation;
+ client->mode = info.product_id;
+ client->usb_interface = 0;
+ irecv_load_device_info_from_iboot_string(client, info.serial);
+ if (ecid && ecid != IRECV_K_WTF_MODE &&
+ client->device_info.ecid != ecid) {
+ irecv_close(client);
+ return IRECV_E_NO_DEVICE;
+ }
+ debug("opening QEMU A6 device %04x:%04x generation %u...\n",
+ info.vendor_id, info.product_id, info.generation);
+ *pclient = client;
+ return IRECV_E_SUCCESS;
+}
+
+static int qemu_a6_control_transfer(irecv_client_t client,
+ uint8_t bm_request_type,
+ uint8_t b_request, uint16_t w_value,
+ uint16_t w_index, unsigned char *data,
+ uint16_t w_length)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ int direction_in = (bm_request_type & 0x80) != 0;
+ int error;
+
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_REQUEST;
+ request.request_id = ++client->qemu_request_id;
+ request.payload_length = direction_in ? 0 : w_length;
+ request.transfer_length = w_length;
+ request.setup[0] = bm_request_type;
+ request.setup[1] = b_request;
+ request.setup[2] = w_value & 0xff;
+ request.setup[3] = w_value >> 8;
+ request.setup[4] = w_index & 0xff;
+ request.setup[5] = w_index >> 8;
+ request.setup[6] = w_length & 0xff;
+ request.setup[7] = w_length >> 8;
+ error = qemu_a6_exchange(client->qemu_fd, &request,
+ direction_in ? NULL : data, &response,
+ direction_in ? data : NULL,
+ direction_in ? w_length : 0);
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_RESPONSE ||
+ response.transfer_length > w_length) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ if (bm_request_type == 0xa1 && b_request == 3 && data &&
+ response.transfer_length >= 4) {
+ client->qemu_poll_timeout_ms = data[1] |
+ (data[2] << 8) | (data[3] << 16);
+ }
+ return response.transfer_length;
+}
+
+static int qemu_a6_bulk_transfer(irecv_client_t client,
+ unsigned char endpoint,
+ unsigned char *data, int length,
+ int *transferred, unsigned int timeout)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ int direction_in = (endpoint & 0x80) != 0;
+ int error;
+
+ if (length < 0 || (unsigned int)length > QA6_USB_MAX_PAYLOAD) {
+ return IRECV_E_INVALID_INPUT;
+ }
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_BULK_REQUEST;
+ request.request_id = ++client->qemu_request_id;
+ request.payload_length = direction_in ? 0 : length;
+ request.transfer_length = length;
+ request.setup[0] = endpoint;
+ request.setup[1] = timeout & 0xff;
+ request.setup[2] = (timeout >> 8) & 0xff;
+ request.setup[3] = (timeout >> 16) & 0xff;
+ request.setup[4] = (timeout >> 24) & 0xff;
+ error = qemu_a6_exchange(client->qemu_fd, &request,
+ direction_in ? NULL : data, &response,
+ direction_in ? data : NULL,
+ direction_in ? length : 0);
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_BULK_RESPONSE ||
+ response.transfer_length > (unsigned int)length) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ *transferred = response.transfer_length;
+ return IRECV_E_SUCCESS;
+}
+
+static irecv_error_t qemu_a6_reset(irecv_client_t client)
+{
+ qemu_a6_usb_frame_header request;
+ qemu_a6_usb_frame_header response;
+ uint8_t dfu_status[6];
+ int retry = 0;
+ int error;
+ while (client->qemu_poll_timeout_ms && retry++ < 5) {
+ uint32_t delay_ms = client->qemu_poll_timeout_ms > 10000 ?
+ 10000 : client->qemu_poll_timeout_ms;
+ debug("honoring QEMU DFU poll timeout: %u ms\n", delay_ms);
+ usleep((useconds_t)delay_ms * 1000);
+ client->qemu_poll_timeout_ms = 0;
+ memset(dfu_status, 0, sizeof(dfu_status));
+ error = qemu_a6_control_transfer(client, 0xa1, 3, 0, 0,
+ dfu_status, sizeof(dfu_status));
+ if (error != sizeof(dfu_status)) {
+ return error < 0 ? error : IRECV_E_USB_STATUS;
+ }
+ debug("QEMU DFU manifestation state=%u status=%u\n",
+ dfu_status[4], dfu_status[0]);
+ if (dfu_status[0] || dfu_status[4] == QA6_USB_DFU_STATE_ERROR) {
+ return IRECV_E_USB_UPLOAD;
+ }
+ if (dfu_status[4] == QA6_USB_DFU_STATE_WAIT_RESET) {
+ client->qemu_poll_timeout_ms = 0;
+ break;
+ }
+ }
+ if (client->qemu_poll_timeout_ms) {
+ return IRECV_E_TIMEOUT;
+ }
+ if (getenv("LIBIRECOVERY_QEMU_NO_RESET")) {
+ debug("leaving QEMU in DFU-WAIT-RESET for debugging\n");
+ return IRECV_E_SUCCESS;
+ }
+
+ memset(&request, 0, sizeof(request));
+ request.magic = QA6_USB_MAGIC;
+ request.version = QA6_USB_PROTOCOL_VERSION;
+ request.type = QA6_USB_MESSAGE_RESET_REQUEST;
+ request.request_id = ++client->qemu_request_id;
+ error = qemu_a6_exchange(client->qemu_fd, &request, NULL, &response,
+ NULL, 0);
+ if (error != IRECV_E_SUCCESS) {
+ return error;
+ }
+ if (response.type != QA6_USB_MESSAGE_RESET_RESPONSE ||
+ response.payload_length || response.transfer_length) {
+ return IRECV_E_UNKNOWN_ERROR;
+ }
+ return IRECV_E_SUCCESS;
+}
+#endif
+
static void irecv_copy_nonce_with_tag_from_buffer(const char* tag, unsigned char** nonce, unsigned int* nonce_size, const char *buf)
{
int taglen = strlen(tag);
@@ -1006,6 +1350,11 @@
*nonce = NULL;
*nonce_size = 0;
+ if (client->qemu_transport) {
+ irecv_copy_nonce_with_tag_from_buffer(
+ tag, nonce, nonce_size, client->device_info.serial_string);
+ return;
+ }
memset(buf, 0, sizeof(buf));
len = irecv_get_string_descriptor_ascii(client, 1, (unsigned char*)buf, sizeof(buf)-1);
@@ -1406,7 +1755,8 @@
static int check_context(irecv_client_t client)
{
- if (client == NULL || client->handle == NULL) {
+ if (client == NULL ||
+ (!client->qemu_transport && client->handle == NULL)) {
return IRECV_E_NO_DEVICE;
}
@@ -1456,6 +1806,12 @@
return IRECV_E_UNSUPPORTED;
#else
#ifndef _WIN32
+ if (client->qemu_transport) {
+ return qemu_a6_control_transfer(client, bm_request_type, b_request,
+ w_value, w_index, data, w_length);
+ }
+#endif
+#ifndef _WIN32
#ifdef HAVE_IOKIT
return iokit_usb_control_transfer(client, bm_request_type, b_request, w_value, w_index, data, w_length, timeout);
#else
@@ -1813,6 +2169,10 @@
int ret;
#ifndef _WIN32
+ if (client->qemu_transport) {
+ return qemu_a6_bulk_transfer(client, endpoint, data, length,
+ transferred, timeout);
+ }
#ifdef HAVE_IOKIT
return iokit_usb_bulk_transfer(client, endpoint, data, length, transferred, timeout);
#else
@@ -2144,11 +2504,15 @@
irecv_set_debug_level(libirecovery_debug);
}
#ifndef _WIN32
+ if (qemu_a6_endpoint()) {
+ error = qemu_a6_open_with_ecid(pclient, ecid);
+ } else {
#ifdef HAVE_IOKIT
- error = iokit_open_with_ecid(pclient, ecid);
+ error = iokit_open_with_ecid(pclient, ecid);
#else
- error = libusb_open_with_ecid(pclient, ecid);
+ error = libusb_open_with_ecid(pclient, ecid);
#endif
+ }
#else
error = win32_open_with_ecid(pclient, ecid);
#endif
@@ -2166,12 +2530,14 @@
}
#ifdef HAVE_IOKIT
- error = (*client->handle)->CreateDeviceAsyncEventSource(client->handle, &client->async_event_source);
- if (error != IRECV_E_SUCCESS) {
- free(client);
- return error;
+ if (!client->qemu_transport) {
+ error = (*client->handle)->CreateDeviceAsyncEventSource(client->handle, &client->async_event_source);
+ if (error != IRECV_E_SUCCESS) {
+ free(client);
+ return error;
+ }
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), client->async_event_source, kCFRunLoopDefaultMode);
}
- CFRunLoopAddSource(CFRunLoopGetCurrent(), client->async_event_source, kCFRunLoopDefaultMode);
#endif
if (client->mode == IRECV_K_DFU_MODE || client->mode == IRECV_K_PORT_DFU_MODE || client->mode == IRECV_K_WTF_MODE || client->mode == KIS_PRODUCT_ID) {
@@ -2237,6 +2603,10 @@
#ifndef _WIN32
debug("Setting to configuration %d\n", configuration);
+ if (client->qemu_transport) {
+ client->usb_config = configuration;
+ return IRECV_E_SUCCESS;
+ }
#ifdef HAVE_IOKIT
IOReturn result;
@@ -2355,6 +2725,17 @@
debug("Setting to interface %d:%d\n", usb_interface, usb_alt_interface);
#ifndef _WIN32
+ if (client->qemu_transport) {
+ if (usb_interface == 1 &&
+ qemu_a6_control_transfer(client, 0x01, 0x0B,
+ usb_alt_interface, usb_interface,
+ NULL, 0) < 0) {
+ return IRECV_E_USB_INTERFACE;
+ }
+ client->usb_interface = usb_interface;
+ client->usb_alt_interface = usb_alt_interface;
+ return IRECV_E_SUCCESS;
+ }
#ifdef HAVE_IOKIT
if (iokit_usb_set_interface(client, usb_interface, usb_alt_interface) < 0) {
return IRECV_E_USB_INTERFACE;
@@ -2393,6 +2768,9 @@
return IRECV_E_NO_DEVICE;
#ifndef _WIN32
+ if (client->qemu_transport) {
+ return qemu_a6_reset(client);
+ }
#ifdef HAVE_IOKIT
IOReturn result;
@@ -2875,6 +3253,44 @@
}
#ifndef _WIN32
+static struct irecv_usb_device_info *qemu_a6_handle_device_add(
+ const qemu_a6_usb_device_info *info)
+{
+ struct irecv_client_private client_loc;
+ struct irecv_usb_device_info *usb_dev_info;
+ irecv_device_event_t dev_event;
+
+ memset(&client_loc, 0, sizeof(client_loc));
+ client_loc.mode = info->product_id;
+ irecv_load_device_info_from_iboot_string(&client_loc, info->serial);
+ usb_dev_info = calloc(1, sizeof(*usb_dev_info));
+ if (!usb_dev_info) {
+ free(client_loc.device_info.srnm);
+ free(client_loc.device_info.imei);
+ free(client_loc.device_info.srtg);
+ free(client_loc.device_info.serial_string);
+ return NULL;
+ }
+ memcpy(&usb_dev_info->device_info, &client_loc.device_info,
+ sizeof(client_loc.device_info));
+ usb_dev_info->location = info->generation;
+ usb_dev_info->alive = 1;
+ usb_dev_info->mode = client_loc.mode;
+ collection_add(&devices, usb_dev_info);
+
+ dev_event.type = IRECV_DEVICE_ADD;
+ dev_event.mode = client_loc.mode;
+ dev_event.device_info = &usb_dev_info->device_info;
+ mutex_lock(&listener_mutex);
+ FOREACH(struct irecv_device_event_context* context, &listeners) {
+ context->callback(&dev_event, context->user_data);
+ } ENDFOREACH
+ mutex_unlock(&listener_mutex);
+ return usb_dev_info;
+}
+#endif
+
+#ifndef _WIN32
#ifdef HAVE_IOKIT
static void iokit_device_added(void *refcon, io_iterator_t iterator)
{
@@ -2977,10 +3393,70 @@
cond_t startup_cond;
mutex_t startup_mutex;
};
+
+#ifndef _WIN32
+static void *qemu_a6_event_handler(struct _irecv_event_handler_info *startup)
+{
+ struct irecv_usb_device_info *current = NULL;
+
+ mutex_lock(&startup->startup_mutex);
+ cond_signal(&startup->startup_cond);
+ mutex_unlock(&startup->startup_mutex);
+
+ for (;;) {
+ qemu_a6_usb_device_info info;
+ int listeners_active;
+ int fd;
+ int error = IRECV_E_UNABLE_TO_CONNECT;
+
+ mutex_lock(&listener_mutex);
+ listeners_active = collection_count(&listeners) != 0;
+ mutex_unlock(&listener_mutex);
+ if (!listeners_active) {
+ break;
+ }
+
+ fd = qemu_a6_connect();
+ if (fd >= 0) {
+ memset(&info, 0, sizeof(info));
+ error = qemu_a6_read_info_fd(fd, &info);
+ close(fd);
+ }
+ if (error == IRECV_E_SUCCESS) {
+ if (current &&
+ (current->location != info.generation ||
+ current->mode != info.product_id)) {
+ /*
+ * Publish removal and arrival on separate polling turns.
+ * idevicerestore deliberately waits for MODE_UNKNOWN before
+ * it accepts the next DFU/recovery attachment.
+ */
+ _irecv_handle_device_remove(current);
+ current = NULL;
+ } else if (!current) {
+ current = qemu_a6_handle_device_add(&info);
+ }
+ } else if (error == IRECV_E_NO_DEVICE && current) {
+ _irecv_handle_device_remove(current);
+ current = NULL;
+ }
+ usleep(250000);
+ }
+ if (current) {
+ _irecv_handle_device_remove(current);
+ }
+ return NULL;
+}
+#endif
static void *_irecv_event_handler(void* data)
{
struct _irecv_event_handler_info* info = (struct _irecv_event_handler_info*)data;
+#ifndef _WIN32
+ if (qemu_a6_endpoint()) {
+ return qemu_a6_event_handler(info);
+ }
+#endif
#ifdef _WIN32
struct collection newDevices;
const GUID *guids[] = { &GUID_DEVINTERFACE_KIS, &GUID_DEVINTERFACE_PORTDFU, &GUID_DEVINTERFACE_DFU, &GUID_DEVINTERFACE_IBOOT, NULL };
@@ -3381,6 +3858,10 @@
client->disconnected_callback(client, &event);
}
#ifndef _WIN32
+ if (client->qemu_transport) {
+ close(client->qemu_fd);
+ client->qemu_fd = -1;
+ } else {
#ifdef HAVE_IOKIT
if (client->usbInterface) {
(*client->usbInterface)->USBInterfaceClose(client->usbInterface);
@@ -3405,6 +3886,7 @@
client->handle = NULL;
}
#endif
+ }
#else
CloseHandle(client->handle);
#endif
+4 -4
View File
@@ -28,7 +28,7 @@ foreach ($required in @(
(Join-Path $wdkRoot "Include\$wdkVersion\km\ude\1.0\UdeCx.h")
)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "Composant de compilation introuvable : $required"
throw "Required build component not found: $required"
}
}
@@ -58,7 +58,7 @@ if ($Configuration -eq 'Release') {
& (Join-Path $toolBin 'cl.exe') @compileArgs
if ($LASTEXITCODE -ne 0) {
throw "La compilation du pilote a échoué ($LASTEXITCODE)."
throw "Driver compilation failed ($LASTEXITCODE)."
}
$kmLib = Join-Path $wdkRoot "Lib\$wdkVersion\km\x64"
@@ -81,8 +81,8 @@ if ($Configuration -eq 'Release') {
& (Join-Path $toolBin 'link.exe') @linkArgs
if ($LASTEXITCODE -ne 0) {
throw "L'édition de liens du pilote a échoué ($LASTEXITCODE)."
throw "Driver link failed ($LASTEXITCODE)."
}
Copy-Item -LiteralPath (Join-Path $projectDir 'QemuA6Ude.inf') -Destination $outputDir -Force
Write-Host "Pilote construit : $sysPath"
Write-Host "Driver built: $sysPath"
+4 -4
View File
@@ -23,7 +23,7 @@ function Find-Qa6WdkRoot {
}
}
throw 'WDK UDE introuvable. Définissez QA6_WDK_ROOT vers le dossier « c » du paquet Microsoft.Windows.WDK.x64.'
throw 'UDE WDK not found. Set QA6_WDK_ROOT to the Microsoft.Windows.WDK.x64 package c directory.'
}
function Find-Qa6WdkVersion {
@@ -34,7 +34,7 @@ function Find-Qa6WdkVersion {
Test-Path -LiteralPath (Join-Path $_.FullName 'km\ude\1.0\UdeCx.h')
} | Sort-Object Name -Descending | Select-Object -First 1
if ($null -eq $version) {
throw "Aucune version UDE dans $WdkRoot"
throw "No UDE version found under $WdkRoot"
}
return $version.Name
}
@@ -55,7 +55,7 @@ function Find-Qa6MsvcRoot {
}
}
}
throw 'MSVC x64 de Visual Studio 2022 est introuvable.'
throw 'Visual Studio 2022 x64 MSVC was not found.'
}
function Find-Qa6SignTool {
@@ -75,5 +75,5 @@ function Find-Qa6SignTool {
}
}
throw "SignTool x64 $WdkVersion est introuvable."
throw "x64 SignTool $WdkVersion was not found."
}
+7 -7
View File
@@ -9,21 +9,21 @@ $secureBoot = $null
try {
$secureBoot = Confirm-SecureBootUEFI
} catch {
Write-Verbose "État Secure Boot non lisible : $($_.Exception.Message)"
Write-Verbose "Unable to read Secure Boot state: $($_.Exception.Message)"
}
$bcdOutput = & bcdedit.exe /set testsigning on 2>&1
$bcdOutput | Out-File -LiteralPath $logPath -Encoding utf8 -Force
if ($LASTEXITCODE -ne 0) {
if ($secureBoot -eq $true) {
throw 'Secure Boot bloque le mode de signature test. Il faut le désactiver dans lUEFI avant de recommencer.'
throw 'Secure Boot blocks test-signing mode. Disable it in UEFI before trying again.'
}
throw "Impossible dactiver le mode de signature test ($LASTEXITCODE)."
throw "Unable to enable test-signing mode ($LASTEXITCODE)."
}
Write-Host 'Mode de signature test activé dans le BCD.'
Write-Host 'Un redémarrage de Windows est requis avant le chargement de QemuA6Ude.sys.'
Write-Host 'Test-signing mode is enabled in the BCD.'
Write-Host 'Restart Windows before loading QemuA6Ude.sys.'
Add-Content -LiteralPath $logPath -Encoding utf8 -Value @(
'Mode de signature test activé dans le BCD.',
'Un redémarrage de Windows est requis.'
'Test-signing mode is enabled in the BCD.',
'A Windows restart is required.'
)
+4 -4
View File
@@ -16,7 +16,7 @@ $devcon = Join-Path $wdkRoot "tools\$wdkVersion\x64\devcon.exe"
foreach ($required in @($certificate, $inf, $devcon)) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
throw "Fichier requis introuvable : $required"
throw "Required file not found: $required"
}
}
@@ -25,8 +25,8 @@ Import-Certificate -FilePath $certificate -CertStoreLocation Cert:\LocalMachine\
& $devcon install $inf 'Root\QemuA6Ude'
if ($LASTEXITCODE -ne 0) {
throw "Installation du pilote échouée ($LASTEXITCODE)."
throw "Driver installation failed ($LASTEXITCODE)."
}
Write-Host 'Pilote QemuA6Ude installé.'
Write-Host 'Le périphérique enfant USB\VID_05AC&PID_1227 doit maintenant apparaître.'
Write-Host 'QemuA6Ude driver installed.'
Write-Host 'The USB\VID_05AC&PID_1227 child device should now be visible.'
+6 -6
View File
@@ -47,25 +47,25 @@ try {
& $signTool sign /fd SHA256 /f $certificatePfx /p $certificatePasswordText (Join-Path $packageDir 'QemuA6Ude.sys')
if ($LASTEXITCODE -ne 0) {
throw 'La signature de test du fichier SYS a échoué.'
throw 'Test-signing the SYS file failed.'
}
& $inf2Cat "/driver:$packageDir" '/os:10_X64'
if ($LASTEXITCODE -ne 0) {
throw 'La génération du catalogue du pilote a échoué.'
throw 'Generating the driver catalog failed.'
}
& $signTool sign /fd SHA256 /f $certificatePfx /p $certificatePasswordText (Join-Path $packageDir 'QemuA6Ude.cat')
if ($LASTEXITCODE -ne 0) {
throw 'La signature de test du catalogue a échoué.'
throw 'Test-signing the catalog failed.'
}
Copy-Item -LiteralPath $certificateCer -Destination $packageDir -Force
& $signTool verify /pa /v (Join-Path $packageDir 'QemuA6Ude.cat')
if ($LASTEXITCODE -ne 0) {
Write-Warning 'La chaîne est volontairement non approuvée avant linstallation du certificat de test.'
Write-Warning 'The chain is intentionally untrusted until the test certificate is installed.'
}
Write-Host "Paquet pilote prêt : $packageDir"
Write-Host "Certificat public : $certificateCer"
Write-Host "Driver package ready: $packageDir"
Write-Host "Public certificate: $certificateCer"
+16 -15
View File
@@ -139,13 +139,13 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu)
if (!ReadFile(driver, request, sizeof(request), &requestLength,
NULL)) {
DWORD error = GetLastError();
fprintf(stderr, "Lecture du pilote impossible: erreur %lu\n",
fprintf(stderr, "Unable to read from driver: error %lu\n",
error);
return -1;
}
if (Qa6ValidateFrame(request, requestLength,
QA6_USB_MESSAGE_REQUEST) != 0) {
fprintf(stderr, "Trame invalide recue du pilote (%lu octets)\n",
fprintf(stderr, "Invalid frame received from driver (%lu bytes)\n",
requestLength);
return -1;
}
@@ -162,17 +162,18 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu)
fflush(stdout);
if (Qa6SendAll(qemu, request, (int)requestLength) != 0) {
fprintf(stderr, "Connexion QEMU interrompue pendant l'envoi.\n");
fprintf(stderr, "QEMU connection closed while sending.\n");
return -1;
}
if (Qa6ReceiveAll(qemu, response, QA6_USB_HEADER_SIZE) != 0) {
fprintf(stderr, "Connexion QEMU interrompue pendant la reponse.\n");
fprintf(stderr,
"QEMU connection closed while receiving a response.\n");
return -1;
}
responseHeader = (Qa6UsbFrameHeader *)response;
if (responseHeader->payload_length > QA6_USB_MAX_PAYLOAD) {
fprintf(stderr, "Longueur de reponse QEMU invalide.\n");
fprintf(stderr, "Invalid QEMU response length.\n");
return -1;
}
responseLength = QA6_USB_HEADER_SIZE +
@@ -180,13 +181,13 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu)
if (responseHeader->payload_length != 0 &&
Qa6ReceiveAll(qemu, response + QA6_USB_HEADER_SIZE,
(int)responseHeader->payload_length) != 0) {
fprintf(stderr, "Connexion QEMU interrompue dans les donnees.\n");
fprintf(stderr, "QEMU connection closed while receiving data.\n");
return -1;
}
if (Qa6ValidateFrame(response, responseLength,
QA6_USB_MESSAGE_RESPONSE) != 0 ||
responseHeader->request_id != requestHeader->request_id) {
fprintf(stderr, "Reponse QEMU invalide pour la requete #%lu.\n",
fprintf(stderr, "Invalid QEMU response for request #%lu.\n",
requestHeader->request_id);
return -1;
}
@@ -194,7 +195,7 @@ Qa6RelayLoop(HANDLE driver, SOCKET qemu)
if (!WriteFile(driver, response, responseLength, &written, NULL) ||
written != responseLength) {
DWORD error = GetLastError();
fprintf(stderr, "Ecriture vers le pilote impossible: erreur %lu\n",
fprintf(stderr, "Unable to write to driver: error %lu\n",
error);
return -1;
}
@@ -214,19 +215,19 @@ main(int argc, char **argv)
int exitCode = 1;
if (sizeof(Qa6UsbFrameHeader) != 32) {
fprintf(stderr, "Erreur interne: taille du protocole USB=%zu\n",
fprintf(stderr, "Internal error: USB protocol size=%zu\n",
sizeof(Qa6UsbFrameHeader));
return 2;
}
SetConsoleCtrlHandler(Qa6ConsoleHandler, TRUE);
if (WSAStartup(MAKEWORD(2, 2), &winsockData) != 0) {
fprintf(stderr, "Initialisation Winsock impossible.\n");
fprintf(stderr, "Unable to initialize Winsock.\n");
return 2;
}
printf("Pont USB QEMU A6 natif Windows\n");
printf("Pilote : \\\\.\\QemuA6Ude0\n");
printf("Native Windows QEMU A6 USB bridge\n");
printf("Driver : \\\\.\\QemuA6Ude0\n");
printf("QEMU : %s:%s\n", host, port);
while (InterlockedCompareExchange(&Qa6StopRequested, 0, 0) == 0) {
@@ -234,7 +235,7 @@ main(int argc, char **argv)
if (driver != INVALID_HANDLE_VALUE) {
break;
}
printf("Attente du pilote QemuA6Ude...\n");
printf("Waiting for the QemuA6Ude driver...\n");
Sleep(1000);
}
if (driver == INVALID_HANDLE_VALUE) {
@@ -246,14 +247,14 @@ main(int argc, char **argv)
if (qemu != INVALID_SOCKET) {
break;
}
printf("Attente de QEMU sur %s:%s...\n", host, port);
printf("Waiting for QEMU at %s:%s...\n", host, port);
Sleep(1000);
}
if (qemu == INVALID_SOCKET) {
goto cleanup;
}
printf("Connecte. idevicerestore peut utiliser l'iPhone DFU virtuel.\n");
printf("Connected. idevicerestore can use the virtual DFU iPhone.\n");
exitCode = Qa6RelayLoop(driver, qemu) == 0 ? 0 : 1;
cleanup:
+4 -4
View File
@@ -16,7 +16,7 @@ $outputDir = Join-Path $projectDir "build\$Configuration\x64"
$exePath = Join-Path $outputDir 'QemuA6UsbBridge.exe'
if (-not (Test-Path -LiteralPath $vsDevCmd -PathType Leaf)) {
throw "Environnement Visual Studio introuvable : $vsDevCmd"
throw "Visual Studio environment not found: $vsDevCmd"
}
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
@@ -24,7 +24,7 @@ New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
$environmentCommand = 'call "{0}" -arch=x64 -host_arch=x64 >nul && set' -f $vsDevCmd
$environmentLines = & cmd.exe /d /s /c $environmentCommand
if ($LASTEXITCODE -ne 0) {
throw 'Impossible de charger lenvironnement MSVC x64.'
throw 'Unable to load the x64 MSVC environment.'
}
foreach ($line in $environmentLines) {
$separator = $line.IndexOf('=')
@@ -50,7 +50,7 @@ $arguments += @(
& $cl @arguments
if ($LASTEXITCODE -ne 0) {
throw "La compilation du pont USB a échoué ($LASTEXITCODE)."
throw "USB bridge build failed ($LASTEXITCODE)."
}
Write-Host "Pont construit : $exePath"
Write-Host "Bridge built: $exePath"
+19
View File
@@ -6,13 +6,21 @@
#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
#pragma pack(push, 1)
typedef struct Qa6UsbFrameHeader {
@@ -25,6 +33,17 @@ typedef struct Qa6UsbFrameHeader {
unsigned int transfer_length;
unsigned char setup[8];
} Qa6UsbFrameHeader;
typedef struct Qa6UsbDeviceInfo {
unsigned int generation;
unsigned short vendor_id;
unsigned short product_id;
unsigned char device_class;
unsigned char device_subclass;
unsigned char device_protocol;
unsigned char is_dfu;
char serial[QA6_USB_SERIAL_MAX];
} Qa6UsbDeviceInfo;
#pragma pack(pop)
#define QA6_USB_HEADER_SIZE ((unsigned int)sizeof(Qa6UsbFrameHeader))