summaryrefslogtreecommitdiff
path: root/hw/misc/aspeed_acry.c
diff options
context:
space:
mode:
Diffstat (limited to 'hw/misc/aspeed_acry.c')
-rw-r--r--hw/misc/aspeed_acry.c481
1 files changed, 481 insertions, 0 deletions
diff --git a/hw/misc/aspeed_acry.c b/hw/misc/aspeed_acry.c
new file mode 100644
index 0000000000..41eac7295f
--- /dev/null
+++ b/hw/misc/aspeed_acry.c
@@ -0,0 +1,481 @@
+/*
+ * ASPEED ACRY Engine
+ *
+ * Copyright (C) 2026 ASPEED Technology Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * The datasheet documents the ACRY engine as supporting both RSA and
+ * ECDSA, but ECDSA is broken on this hardware, so only RSA is modelled
+ * here.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/cutils.h"
+#include "qemu/log.h"
+#include "hw/misc/aspeed_acry.h"
+#include "hw/core/qdev-properties.h"
+#include "hw/core/irq.h"
+#include "hw/core/registerfields.h"
+#include "qapi/error.h"
+#include "crypto/akcipher.h"
+#include "crypto/der.h"
+#include "trace.h"
+
+REG32(ACRY_TRIGGER, 0x000)
+ FIELD(ACRY_TRIGGER, RSA_START, 0, 1)
+REG32(ACRY_DMA_SRC, 0x04C)
+REG32(ACRY_DMA_LEN, 0x050)
+ FIELD(ACRY_DMA_LEN, DATALEN, 0, 16)
+REG32(ACRY_INT_MASK, 0x3F8)
+ FIELD(ACRY_INT_MASK, RSA_DMA_MASK, 2, 1)
+ FIELD(ACRY_INT_MASK, RSA_ENG_MASK, 1, 1)
+REG32(ACRY_STATUS, 0x3FC)
+ FIELD(ACRY_STATUS, RSA_DMA_DONE, 2, 1)
+ FIELD(ACRY_STATUS, RSA_ENG_DONE, 1, 1)
+
+/*
+ * Total size of the interleaved buffer. Data is 4 of every 12
+ * dwords of a block, one third of the buffer, so the whole buffer is 3x
+ * the data region.
+ */
+#define ASPEED_ACRY_SRAM_SIZE (3 * ASPEED_ACRY_DATA_MAX_LEN)
+
+#define ASPEED_ACRY_BYTES_PER_DWORD 4
+#define ASPEED_ACRY_LANES_PER_BLOCK 4
+/* Each block holds 3 regions (exp, mod, data) of LANES_PER_BLOCK dwords. */
+#define ASPEED_ACRY_DWORDS_PER_BLOCK (3 * ASPEED_ACRY_LANES_PER_BLOCK)
+
+/* Dwords into each block where each operand's region starts. */
+#define ASPEED_ACRY_EXP_OFFSET (0 * ASPEED_ACRY_LANES_PER_BLOCK)
+#define ASPEED_ACRY_MOD_OFFSET (1 * ASPEED_ACRY_LANES_PER_BLOCK)
+#define ASPEED_ACRY_DATA_OFFSET (2 * ASPEED_ACRY_LANES_PER_BLOCK)
+
+static void aspeed_acry_hexdump(const char *desc, const uint8_t *buf,
+ size_t size)
+{
+ g_autoptr(GString) str = g_string_sized_new(64);
+ size_t len;
+ size_t i;
+
+ for (i = 0; i < size; i += len) {
+ len = MIN(16, size - i);
+ g_string_truncate(str, 0);
+ qemu_hexdump_line(str, buf + i, len, 1, 4);
+ trace_aspeed_acry_hexdump(desc, i, str->str);
+ }
+}
+
+/*
+ * The interleaved buffer is a series of 12-dword blocks, each split into
+ * three 4-dword regions - exp, mod, data:
+ *
+ * dword in block: 0 1 2 3 4 5 6 7 8 9 10 11
+ * region: \---- exp ----/ \---- mod ----/ \---- data ----/
+ * lane: 0 1 2 3 0 1 2 3 0 1 2 3
+ *
+ * Successive blocks hold the next 4 dwords of each operand, so operand
+ * dword d is in block (d / 4), lane (d % 4). Dwords are little-endian, so
+ * byte b of dword D is at byte D * 4 + b.
+ *
+ * Return the buffer offset of byte 'op_byte' (op_byte = 0 = least
+ * significant) of the operand whose region starts 'region' dwords into
+ * each block (0 = exp, 4 = mod, 8 = data).
+ */
+static int aspeed_acry_operand_offset(int region, int op_byte)
+{
+ int byte_in_dword;
+ int op_dword;
+ int offset;
+ int block;
+ int lane;
+
+ op_dword = op_byte / ASPEED_ACRY_BYTES_PER_DWORD;
+ byte_in_dword = op_byte % ASPEED_ACRY_BYTES_PER_DWORD;
+ block = op_dword / ASPEED_ACRY_LANES_PER_BLOCK;
+ lane = op_dword % ASPEED_ACRY_LANES_PER_BLOCK;
+
+ offset = (block * ASPEED_ACRY_DWORDS_PER_BLOCK + region + lane)
+ * ASPEED_ACRY_BYTES_PER_DWORD + byte_in_dword;
+ assert(offset < ASPEED_ACRY_SRAM_SIZE);
+
+ return offset;
+}
+
+/*
+ * Read one operand out of the buffer as a big-endian magnitude.
+ *
+ * The operand's bytes are scattered through buf; byte k (significance level
+ * k, k = 0 = least significant) is at aspeed_acry_operand_offset(region, k).
+ * Walk from the top down, drop leading zero bytes, and write the result most
+ * significant byte first into out[]. Returns the number of bytes written
+ * (the value 0 yields a single 0x00 byte, so always >= 1).
+ */
+static int aspeed_acry_extract_be(const uint8_t *buf, int region,
+ int max_bytes, uint8_t *out)
+{
+ int offset;
+ int msb;
+ int len;
+ int k;
+
+ /* Highest significance level holding a non-zero byte (skip leading 0s). */
+ for (msb = max_bytes - 1; msb >= 0; msb--) {
+ offset = aspeed_acry_operand_offset(region, msb);
+ if (buf[offset] != 0) {
+ break;
+ }
+ }
+
+ /* All bytes zero: the value is 0. */
+ if (msb < 0) {
+ out[0] = 0;
+ return 1;
+ }
+
+ /* Copy most significant byte first: level msb down to level 0. */
+ len = 0;
+ for (k = msb; k >= 0; k--) {
+ offset = aspeed_acry_operand_offset(region, k);
+ out[len++] = buf[offset];
+ }
+
+ return len;
+}
+
+/*
+ * Return a DER INTEGER body for the unsigned big-endian magnitude 'be'.
+ *
+ * DER INTEGERs are signed, so if the top byte has bit 7 set the value
+ * would decode as negative; prepend a 0x00 guard byte in that case.
+ *
+ * The padded copy is written into 'pad_buf' (caller-owned, sized len + 1)
+ * rather than a local, because qcrypto_der_encode_int() only stores the
+ * pointer we hand it - the bytes are not copied until
+ * qcrypto_der_encode_ctx_flush_and_free() - so the body must stay valid
+ * until then. Returns a pointer into 'be' or 'pad_buf' as appropriate,
+ * with the body length in *body_len.
+ */
+static const uint8_t *aspeed_acry_der_uint_body(const uint8_t *be, size_t len,
+ uint8_t *pad_buf,
+ size_t *body_len)
+{
+ if (be[0] & 0x80) {
+ pad_buf[0] = 0x00;
+ memcpy(pad_buf + 1, be, len);
+ *body_len = len + 1;
+ return pad_buf;
+ }
+
+ *body_len = len;
+ return be;
+}
+
+/*
+ * DER-encode a "RsaPubKey ::= SEQUENCE { n INTEGER, e INTEGER }" (see
+ * crypto/rsakey.h), the format expected by qcrypto_akcipher_new(). n and e
+ * are minimal big-endian magnitudes (as produced by
+ * aspeed_acry_extract_be()); the engine does a raw modexp, so the guest's
+ * exponent is always encoded here as the public 'e'.
+ */
+static uint8_t *aspeed_acry_der_encode_pubkey(const uint8_t *n, size_t n_len,
+ const uint8_t *e, size_t e_len,
+ size_t *out_len)
+{
+ QCryptoEncodeContext *ctx = qcrypto_der_encode_ctx_new();
+ uint8_t n_pad[ASPEED_ACRY_MAX_BYTES + 1];
+ uint8_t e_pad[ASPEED_ACRY_MAX_BYTES + 1];
+ const uint8_t *n_body;
+ const uint8_t *e_body;
+ size_t n_body_len;
+ size_t e_body_len;
+ uint8_t *buf;
+
+ n_body = aspeed_acry_der_uint_body(n, n_len, n_pad, &n_body_len);
+ e_body = aspeed_acry_der_uint_body(e, e_len, e_pad, &e_body_len);
+
+ qcrypto_der_encode_seq_begin(ctx);
+ qcrypto_der_encode_int(ctx, n_body, n_body_len);
+ qcrypto_der_encode_int(ctx, e_body, e_body_len);
+ qcrypto_der_encode_seq_end(ctx);
+
+ *out_len = qcrypto_der_encode_ctx_buffer_len(ctx);
+ buf = g_malloc(*out_len);
+ qcrypto_der_encode_ctx_flush_and_free(ctx, buf);
+
+ return buf;
+}
+
+/*
+ * Store the RSA result into the output SRAM data region as 'n_len' bytes
+ * (the key size): the low 'result_len' bytes are result_be (big-endian),
+ * the rest is zero. sram_as is a 0-based AddressSpace over the SRAM, so the
+ * offset from aspeed_acry_operand_offset() is used directly; each data dword
+ * is written as a little-endian word.
+ */
+static bool aspeed_acry_store_result(AspeedACRYState *s,
+ const uint8_t *result_be,
+ int result_len, int n_len, Error **errp)
+{
+ uint32_t result_word;
+ MemTxResult res;
+ int offset;
+ int src;
+ int i;
+ int j;
+
+ /* result_be is MSB-first; take bytes from its LSB end. */
+ src = result_len - 1;
+ for (i = 0; i < n_len / ASPEED_ACRY_BYTES_PER_DWORD; i++) {
+ /* Pack up to 4 result bytes (LSB first) into a little-endian dword. */
+ result_word = 0;
+ for (j = 0; j < ASPEED_ACRY_BYTES_PER_DWORD; j++) {
+ if (src >= 0) {
+ result_word |= (uint32_t)result_be[src--] << (8 * j);
+ }
+ }
+
+ offset = aspeed_acry_operand_offset(ASPEED_ACRY_DATA_OFFSET,
+ ASPEED_ACRY_BYTES_PER_DWORD * i);
+ address_space_stl_le(&s->sram_as, offset, result_word,
+ MEMTXATTRS_UNSPECIFIED, &res);
+ if (res != MEMTX_OK) {
+ error_setg(errp, "failed to write result to SRAM");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void aspeed_acry_clear_result(AspeedACRYState *s)
+{
+ MemTxResult res;
+ int offset;
+ int i;
+
+ for (i = 0; i < ASPEED_ACRY_MAX_BYTES / ASPEED_ACRY_BYTES_PER_DWORD; i++) {
+ offset = aspeed_acry_operand_offset(ASPEED_ACRY_DATA_OFFSET,
+ ASPEED_ACRY_BYTES_PER_DWORD * i);
+ address_space_stl_le(&s->sram_as, offset, 0,
+ MEMTXATTRS_UNSPECIFIED, &res);
+ if (res != MEMTX_OK) {
+ qemu_log_mask(LOG_GUEST_ERROR, "%s: failed to clear result\n",
+ __func__);
+ return;
+ }
+ }
+}
+
+static bool aspeed_acry_do_rsa(AspeedACRYState *s, Error **errp)
+{
+ QCryptoAkCipherOptions opts = {
+ .alg = QCRYPTO_AK_CIPHER_ALGO_RSA,
+ .u.rsa = {
+ .padding_alg = QCRYPTO_RSA_PADDING_ALGO_RAW,
+ },
+ };
+ uint32_t len = FIELD_EX32(s->regs[R_ACRY_DMA_LEN], ACRY_DMA_LEN, DATALEN);
+ g_autofree uint8_t *src_buf = g_malloc0(ASPEED_ACRY_SRAM_SIZE);
+ g_autofree uint8_t *data = g_malloc0(ASPEED_ACRY_DATA_MAX_LEN);
+ uint8_t result[ASPEED_ACRY_MAX_BYTES] = { 0 };
+ uint64_t src_addr = s->regs[R_ACRY_DMA_SRC];
+ g_autofree uint8_t *der_key = NULL;
+ uint8_t n[ASPEED_ACRY_MAX_BYTES];
+ uint8_t e[ASPEED_ACRY_MAX_BYTES];
+ QCryptoAkCipher *cipher = NULL;
+ int result_len = 0;
+ size_t der_len;
+ int data_len;
+ int n_len;
+ int e_len;
+
+ if (!qcrypto_akcipher_supports(&opts)) {
+ error_setg(errp, "RSA ModExp not supported by the crypto backend");
+ return false;
+ }
+
+ if (len == 0 || len > ASPEED_ACRY_SRAM_SIZE) {
+ error_setg(errp, "invalid DMA length %u", len);
+ return false;
+ }
+
+ trace_aspeed_acry_rsa_trigger(src_addr, len);
+
+ if (address_space_read(&s->dram_as, src_addr, MEMTXATTRS_UNSPECIFIED,
+ src_buf, len) != MEMTX_OK) {
+ error_setg(errp, "failed to read DMA buffer at 0x%" PRIx64, src_addr);
+ return false;
+ }
+
+ n_len = aspeed_acry_extract_be(src_buf, ASPEED_ACRY_MOD_OFFSET,
+ ASPEED_ACRY_MAX_BYTES, n);
+ e_len = aspeed_acry_extract_be(src_buf, ASPEED_ACRY_EXP_OFFSET,
+ ASPEED_ACRY_MAX_BYTES, e);
+ data_len = aspeed_acry_extract_be(src_buf, ASPEED_ACRY_DATA_OFFSET,
+ ASPEED_ACRY_DATA_MAX_LEN, data);
+
+ if (trace_event_get_state_backends(TRACE_ASPEED_ACRY_HEXDUMP)) {
+ aspeed_acry_hexdump("buf", src_buf, len);
+ aspeed_acry_hexdump("n", n, n_len);
+ aspeed_acry_hexdump("e", e, e_len);
+ aspeed_acry_hexdump("data", data, data_len);
+ }
+
+ der_key = aspeed_acry_der_encode_pubkey(n, n_len, e, e_len, &der_len);
+ cipher = qcrypto_akcipher_new(&opts, QCRYPTO_AK_CIPHER_KEY_TYPE_PUBLIC,
+ der_key, der_len, errp);
+ if (!cipher) {
+ error_prepend(errp, "failed to create RSA cipher: ");
+ return false;
+ }
+
+ result_len = qcrypto_akcipher_encrypt(cipher, data, data_len,
+ result, sizeof(result), errp);
+ qcrypto_akcipher_free(cipher);
+ if (result_len < 0) {
+ error_prepend(errp, "RSA modexp failed: ");
+ return false;
+ }
+
+ if (!aspeed_acry_store_result(s, result, result_len, n_len, errp)) {
+ return false;
+ }
+
+ if (trace_event_get_state_backends(TRACE_ASPEED_ACRY_HEXDUMP)) {
+ aspeed_acry_hexdump("result", result, result_len);
+ }
+
+ return true;
+}
+
+static uint64_t aspeed_acry_read(void *opaque, hwaddr offset, unsigned int size)
+{
+ AspeedACRYState *s = ASPEED_ACRY(opaque);
+ uint32_t reg = offset >> 2;
+
+ trace_aspeed_acry_read(offset, s->regs[reg]);
+
+ return s->regs[reg];
+}
+
+static void aspeed_acry_write(void *opaque, hwaddr offset, uint64_t data,
+ unsigned int size)
+{
+ AspeedACRYState *s = ASPEED_ACRY(opaque);
+ uint32_t reg = offset >> 2;
+ Error *local_err = NULL;
+
+ trace_aspeed_acry_write(offset, data);
+
+ switch (reg) {
+ case R_ACRY_DMA_SRC:
+ /*
+ * The DMA source register holds a CPU-visible DRAM address (e.g.
+ * 0x8xxxxxxx on AST2600); the engine addresses DRAM from offset 0,
+ * so mask off the top bit to get the DRAM-relative offset.
+ */
+ data &= 0x7FFFFFFF;
+ break;
+ case R_ACRY_STATUS:
+ data = s->regs[R_ACRY_STATUS] & ~data;
+ if (!(data & (R_ACRY_STATUS_RSA_ENG_DONE_MASK |
+ R_ACRY_STATUS_RSA_DMA_DONE_MASK))) {
+ qemu_irq_lower(s->irq);
+ }
+ break;
+ case R_ACRY_TRIGGER:
+ if (FIELD_EX32(data, ACRY_TRIGGER, RSA_START)) {
+ if (!aspeed_acry_do_rsa(s, &local_err)) {
+ qemu_log_mask(LOG_GUEST_ERROR, "%s: %s\n", __func__,
+ error_get_pretty(local_err));
+ error_free(local_err);
+ aspeed_acry_clear_result(s);
+ }
+
+ s->regs[R_ACRY_STATUS] |= R_ACRY_STATUS_RSA_ENG_DONE_MASK |
+ R_ACRY_STATUS_RSA_DMA_DONE_MASK;
+ if (s->regs[R_ACRY_INT_MASK] &
+ (R_ACRY_INT_MASK_RSA_ENG_MASK_MASK |
+ R_ACRY_INT_MASK_RSA_DMA_MASK_MASK)) {
+ qemu_irq_raise(s->irq);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ s->regs[reg] = data;
+}
+
+static const MemoryRegionOps aspeed_acry_ops = {
+ .read = aspeed_acry_read,
+ .write = aspeed_acry_write,
+ .endianness = DEVICE_LITTLE_ENDIAN,
+ .valid = {
+ .min_access_size = 1,
+ .max_access_size = 4,
+ },
+};
+
+static void aspeed_acry_reset_hold(Object *obj, ResetType type)
+{
+ AspeedACRYState *s = ASPEED_ACRY(obj);
+
+ memset(s->regs, 0, sizeof(s->regs));
+}
+
+static void aspeed_acry_realize(DeviceState *dev, Error **errp)
+{
+ SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
+ AspeedACRYState *s = ASPEED_ACRY(dev);
+
+ if (!s->dram_mr) {
+ error_setg(errp, TYPE_ASPEED_ACRY ": 'dram' link not set");
+ return;
+ }
+
+ if (!s->sram_mr) {
+ error_setg(errp, TYPE_ASPEED_ACRY ": 'sram' link not set");
+ return;
+ }
+
+ address_space_init(&s->dram_as, s->dram_mr, "dram");
+ address_space_init(&s->sram_as, s->sram_mr, "sram");
+
+ memory_region_init_io(&s->iomem, OBJECT(s), &aspeed_acry_ops, s,
+ TYPE_ASPEED_ACRY, ASPEED_ACRY_NR_REGS << 2);
+ sysbus_init_mmio(sbd, &s->iomem);
+
+ sysbus_init_irq(sbd, &s->irq);
+}
+
+static const Property aspeed_acry_properties[] = {
+ DEFINE_PROP_LINK("dram", AspeedACRYState, dram_mr,
+ TYPE_MEMORY_REGION, MemoryRegion *),
+ DEFINE_PROP_LINK("sram", AspeedACRYState, sram_mr,
+ TYPE_MEMORY_REGION, MemoryRegion *),
+};
+
+static void aspeed_acry_class_init(ObjectClass *klass, const void *data)
+{
+ DeviceClass *dc = DEVICE_CLASS(klass);
+ ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+ dc->desc = "ASPEED ACRY Engine";
+ dc->realize = aspeed_acry_realize;
+ rc->phases.hold = aspeed_acry_reset_hold;
+ device_class_set_props(dc, aspeed_acry_properties);
+}
+
+static const TypeInfo aspeed_acry_types[] = {
+ {
+ .name = TYPE_ASPEED_ACRY,
+ .parent = TYPE_SYS_BUS_DEVICE,
+ .instance_size = sizeof(AspeedACRYState),
+ .class_init = aspeed_acry_class_init,
+ },
+};
+
+DEFINE_TYPES(aspeed_acry_types)