summaryrefslogtreecommitdiff
path: root/hw/misc/aspeed_acry.c
blob: 41eac7295fd7bda2f389301afffbe984ef161fcd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
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)