/* * 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)