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
+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)