diff --git a/docs/opentitan/romtool.md b/docs/opentitan/romtool.md new file mode 100644 index 0000000000000..3efdc6584fec7 --- /dev/null +++ b/docs/opentitan/romtool.md @@ -0,0 +1,80 @@ +# `romtool.py` + +`romtool.py` converts ROM image between different file formats. + +Supported input formats: +* `ELF`: RISC-V RV32 executable file (only supported as input ROM file) +* `BIN`: RISC-V RV32 executable file +* `VMEM`: Plain VMEM text file +* `SVMEM`: Scrambled VMEM text file with 7-bit SEC-DED +* `HEX`: Scrambled HEX text file with 7-bit SEC-DED + +## Usage + +````text +usage: romtool.py [-h] -c CFG [-o OUTPUT] [-i ROM_ID] [-z SIZE] + [-f {HEX,BIN,SVMEM,VMEM}] [-s] [-v] [-d] + rom + +QEMU OT tool to generate a scrambled ROM image. + +options: + -h, --help show this help message and exit + +Files: + rom input ROM image file + -c, --config CFG input QEMU OT config file + -o, --output OUTPUT output ROM image file + +Parameters: + -i, --rom-id ROM_ID ROM image identifier + -z, --rom-size SIZE ROM image size in bytes (accepts Ki suffix) + -f, --output-format {HEX,BIN,SVMEM,VMEM} + Output file format (default: HEX) + -s, --subst-perm Enable legacy mode with S&P mode + +Extras: + -v, --verbose increase verbosity + -d, --debug enable debug mode +```` + +### Arguments + +* `-c` specify a QEMU [configuration file](otcfg.md) from which to read all the cryptographic + constants. See [`cfggen.py`](cfggen.md) tool to generate such a file. + +* `-d` only useful to debug the script, reports any Python traceback to the standard error stream. + +* `-f` output file format. `HEX` format always output scrambled/SEC-DED data, `SVMEM` specifies a + VMEM format with scrambled/SEC-DED data. Note: input file format is automatically detected from + the content of the input ROM file. + +* `-i` ROM identifier. Required for platforms with multiple ROMs + +* `-o` outfile file, default to stdout (beware when using a binary format) + +* `-s` use legacy scrambling scheme, with extra substitute and permute stages, but less PRINCE + stages. Only useful for older files. + +* `-v` can be repeated to increase verbosity of the script, mostly for debug purpose. + +* `-z` ROM file size. Required for all input file formats but the `HEX` or `SVMEM` format. + `Ki` (kilobytes) suffix is supported. + +### Examples + +Generate a scrambled with SEC-DED data in HEX format: +````sh +# EarlGrey +scripts/opentitan/romtool.py -c ot.cfg -z 32Ki -o rom.hex rom.elf +# Darjeeling (base ROM) +scripts/opentitan/romtool.py -c ot.cfg -z 32Ki -i 0 -o rom0.hex rom0.elf +```` + +Extract clear data from a scrambled HEX file: +````sh +# EarlGrey +scripts/opentitan/romtool.py -c ot.cfg -f VMEM -o rom.vmem rom.hex +# Darjeeling (base ROM) +scripts/opentitan/romtool.py -c ot.cfg -i 0 -f VMEM -o rom.vmem rom.hex +``` diff --git a/docs/opentitan/tools.md b/docs/opentitan/tools.md index cfd46373e2c25..8924dc9f4bb02 100644 --- a/docs/opentitan/tools.md +++ b/docs/opentitan/tools.md @@ -24,6 +24,7 @@ of options and the available features. * [`cfggen.py`](cfggen.md) can be used to generate an OpenTitan [configuration file](otcfg.md) from an existing OpenTitan repository. +* [`romtool.py`](romtool.md) can be used to convert a ROM image file between different file formats. * [`otpdm.py`](otpdm.md) can be used to access the OTP Controller over a JTAG/DTM/DM link. It reads out partition's item values and can update those items. * [`otptool.py`](otptool.md) can be used to generate an OTP image from a OTP VMEM file and can be diff --git a/hw/opentitan/ot_rom_ctrl.c b/hw/opentitan/ot_rom_ctrl.c index b3d3d8a7b9272..07f6ae193a02f 100644 --- a/hw/opentitan/ot_rom_ctrl.c +++ b/hw/opentitan/ot_rom_ctrl.c @@ -38,7 +38,6 @@ #include "qemu/osdep.h" #include "qemu/bswap.h" -#include "qemu/fifo8.h" #include "qemu/log.h" #include "qemu/memalign.h" #include "qapi/error.h" @@ -122,11 +121,31 @@ static const char *REG_NAMES[REGS_COUNT] = { static const uint8_t SBOX4[16u] = { 12u, 5u, 6u, 11u, 9u, 0u, 10u, 13u, 3u, 14u, 15u, 8u, 4u, 7u, 1u, 2u }; + +static const uint64_t INV_HSIAO[7u] = { + 0x012606bd25ull, + 0x02deba8050ull, + 0x04413d89aaull, + 0x0831234ed1ull, + 0x10c2c1323bull, + 0x202dcc624cull, + 0x4098505586ull, +}; /* clang-format on */ +#define INV_HSIAO_COUNT ARRAY_SIZE(INV_HSIAO) +#define HSIAO_MASK 0x2a00000000ull + static const OtKMACAppCfg KMAC_APP_CFG = OT_KMAC_CONFIG(CSHAKE, 256u, "", "ROM_CTRL"); +typedef struct { + uint64_t *buffer; /* OT_ROM_CTRL_WORD_BYTES array, in logical order */ + unsigned word_count; /* count of words in the buffer */ + unsigned word_pos; /* current index in for computed KMAC digest */ + bool local_gen; /* scrambling/ECC generated locally, not read from file */ +} OtRomCtrlScrambled; + struct OtRomCtrlState { SysBusDevice parent_obj; @@ -138,23 +157,18 @@ struct OtRomCtrlState { uint32_t regs[REGS_COUNT]; - Fifo8 hash_fifo; uint64_t keys[2u]; /* may be NULL */ uint64_t nonce; uint64_t addr_nonce; uint64_t data_nonce; unsigned addr_width; /* bit count */ unsigned data_nonce_width; /* bit count */ - unsigned se_pos; - unsigned se_last_pos; - unsigned se_word_bytes; - uint64_t *se_buffer; + OtRomCtrlScrambled scrambled; unsigned recovered_error_count; unsigned unrecoverable_error_count; char *hexstr; bool first_reset; bool loaded; - bool scrambled_n_ecc; char *ot_id; uint32_t size; @@ -212,7 +226,7 @@ static uint64_t ot_rom_ctrl_sbox(uint64_t in, unsigned width, uint64_t sbox_mask = (1ull << width) - 1ull; uint64_t out = in & (full_mask & ~sbox_mask); - for (unsigned ix = 0; ix < width; ix += 4) { + for (unsigned ix = 0u; ix < width; ix += 4u) { uint64_t nibble = (in >> ix) & 0xfull; out |= ((uint64_t)sbox[nibble]) << ix; } @@ -241,14 +255,14 @@ static uint64_t ot_rom_ctrl_perm(uint64_t in, unsigned width, bool invert) width >>= 1u; if (!invert) { - for (unsigned ix = 0; ix < width; ix++) { + for (unsigned ix = 0u; ix < width; ix++) { uint64_t bit = (in >> (ix << 1u)) & 1ull; out |= bit << ix; bit = (in >> ((ix << 1u) + 1u)) & 1ull; out |= bit << (width + ix); } } else { - for (unsigned ix = 0; ix < width; ix++) { + for (unsigned ix = 0u; ix < width; ix++) { uint64_t bit = (in >> ix) & 1ull; out |= bit << (ix << 1u); bit = (in >> (ix + width)) & 1ull; @@ -264,7 +278,7 @@ static uint64_t ot_rom_ctrl_subst_perm_enc(uint64_t in, uint64_t key, { uint64_t state = in; - for (unsigned ix = 0; ix < num_rounds; ix++) { + for (unsigned ix = 0u; ix < num_rounds; ix++) { state ^= key; state = ot_rom_ctrl_sbox(state, width, SBOX4); state = ot_rom_ctrl_flip(state, width); @@ -282,6 +296,13 @@ static unsigned ot_rom_ctrl_addr_sp_enc(const OtRomCtrlState *s, unsigned addr) OT_ROM_CTRL_NUM_ADDR_SUBST_PERM_ROUNDS); } +static uint64_t ot_rom_ctrl_data_sp_enc(const OtRomCtrlState *s, uint64_t in) +{ + (void)s; + return ot_rom_ctrl_subst_perm_enc(in, 0, OT_ROM_CTRL_WORD_BITS, + OT_ROM_CTRL_NUM_DATA_SUBST_PERM_ROUNDS); +} + static uint64_t ot_rom_ctrl_get_keystream(const OtRomCtrlState *s, unsigned addr) { @@ -291,129 +312,18 @@ ot_rom_ctrl_get_keystream(const OtRomCtrlState *s, unsigned addr) return stream & ((1ull << OT_ROM_CTRL_WORD_BITS) - 1ull); } -static void ot_rom_ctrl_compare_and_notify(OtRomCtrlState *s) -{ - /* compare digests */ - bool rom_good = true; - for (unsigned ix = 0; ix < ROM_DIGEST_WORDS; ix++) { - if (s->regs[R_EXP_DIGEST_0 + ix] != s->regs[R_DIGEST_0 + ix]) { - rom_good = false; - error_setg(&error_fatal, - "ot_rom_ctrl: %s: Digest mismatch (expected 0x%08x got " - "0x%08x) @ %u, errors: %u single-bit, %u double-bit\n", - s->ot_id, s->regs[R_EXP_DIGEST_0 + ix], - s->regs[R_DIGEST_0 + ix], ix, s->recovered_error_count, - s->unrecoverable_error_count); - } - } - - trace_ot_rom_ctrl_notify(s->ot_id, rom_good); - - /* notify end of check */ - ibex_irq_set(&s->pwrmgr_good, rom_good); - ibex_irq_set(&s->pwrmgr_done, true); -} - -static void ot_rom_ctrl_send_kmac_req(OtRomCtrlState *s) -{ - g_assert(s->se_buffer); - fifo8_reset(&s->hash_fifo); - - while (!fifo8_is_full(&s->hash_fifo) && (s->se_pos < s->se_last_pos)) { - unsigned word_pos = s->se_pos / s->se_word_bytes; - unsigned word_off = s->se_pos % s->se_word_bytes; - unsigned phy_addr = - s->scrambled_n_ecc ? ot_rom_ctrl_addr_sp_enc(s, word_pos) : - word_pos; - uint8_t wbuf[sizeof(uint64_t)]; - stq_le_p(wbuf, s->se_buffer[phy_addr]); - uint8_t *wb = wbuf; - unsigned wl = s->se_word_bytes; - wb += word_off; - wl -= word_off; - wl = MIN(wl, fifo8_num_free(&s->hash_fifo)); - s->se_pos += wl; - while (wl--) { - fifo8_push(&s->hash_fifo, *wb++); - } - } - - g_assert(!fifo8_is_empty(&s->hash_fifo)); - - OtKMACAppReq req = { - .last = s->se_pos == s->se_last_pos, - .msg_len = fifo8_num_used(&s->hash_fifo), - }; - uint32_t blen; - const uint8_t *buf = fifo8_pop_bufptr(&s->hash_fifo, req.msg_len, &blen); - g_assert(blen == req.msg_len); - memcpy(req.msg_data, buf, req.msg_len); - - OtKMACClass *kc = OT_KMAC_GET_CLASS(s->kmac); - kc->app_request(s->kmac, s->kmac_app, &req); -} - -static void -ot_rom_ctrl_handle_kmac_response(void *opaque, const OtKMACAppRsp *rsp) +static uint64_t +ot_rom_ctrl_unscramble_word(const OtRomCtrlState *s, unsigned addr, uint64_t in) { - OtRomCtrlState *s = OT_ROM_CTRL(opaque); - - if (!rsp->done) { - ot_rom_ctrl_send_kmac_req(s); - return; - } - - if (s->scrambled_n_ecc) { - qemu_vfree(s->se_buffer); - s->se_buffer = NULL; - } - - g_assert(s->se_pos == s->se_last_pos); - - /* - * switch to ROMD mode if no unrecoverable ECC error has been detected. - * Note that real HW does this on a per 32-bit address basis, but as any - * error triggers an invalid digest and prevents the Ibex core from booting, - * this use case is mostly useless anyway. - */ - memory_region_rom_device_set_romd(&s->mem, - s->unrecoverable_error_count == 0); - - /* retrieve digest */ - for (unsigned ix = 0; ix < 8; ix++) { - uint32_t share0; - uint32_t share1; - memcpy(&share0, &rsp->digest_share0[ix * sizeof(uint32_t)], - sizeof(uint32_t)); - memcpy(&share1, &rsp->digest_share1[ix * sizeof(uint32_t)], - sizeof(uint32_t)); - s->regs[R_DIGEST_0 + ix] = share0 ^ share1; - if (!s->scrambled_n_ecc) { - s->regs[R_EXP_DIGEST_0 + ix] = s->regs[R_DIGEST_0 + ix]; - } - } - - if (trace_event_get_state(TRACE_OT_ROM_CTRL_COMPUTED_DIGEST)) { - uint8_t digest[OT_ROM_DIGEST_BYTES]; - for (unsigned ix = 0; ix < OT_ROM_DIGEST_BYTES; ix++) { - digest[ix] = rsp->digest_share0[ix] ^ rsp->digest_share1[ix]; - } - trace_ot_rom_ctrl_computed_digest( - s->ot_id, ot_common_lhexdump(digest, OT_ROM_DIGEST_BYTES, true, - s->hexstr, OT_ROM_CTRL_HEXSTR_SIZE)); - } - - trace_ot_rom_ctrl_digest_mode(s->ot_id, "stored"); - - /* compare digests and send notification */ - ot_rom_ctrl_compare_and_notify(s); + uint64_t keystream = ot_rom_ctrl_get_keystream(s, addr); + return keystream ^ in; } static uint64_t -ot_rom_ctrl_unscramble_word(const OtRomCtrlState *s, unsigned addr, uint64_t in) +ot_rom_ctrl_scramble_word(const OtRomCtrlState *s, unsigned addr, uint64_t in) { uint64_t keystream = ot_rom_ctrl_get_keystream(s, addr); - return keystream ^ in; + return ot_rom_ctrl_data_sp_enc(s, keystream ^ in); } static uint32_t ot_rom_ctrl_verify_ecc_39_32_u32( @@ -421,15 +331,10 @@ static uint32_t ot_rom_ctrl_verify_ecc_39_32_u32( { unsigned syndrome = 0u; -#define ECC_MASK 0x2a00000000ull - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x012606bd25ull) << 0u; - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x02deba8050ull) << 1u; - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x04413d89aaull) << 2u; - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x0831234ed1ull) << 3u; - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x10c2c1323bull) << 4u; - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x202dcc624cull) << 5u; - syndrome |= __builtin_parityl((data_i ^ ECC_MASK) & 0x4098505586ull) << 6u; -#undef ECC_MASK + for (unsigned int ix = 0u; ix < INV_HSIAO_COUNT; ix++) { + syndrome |= + __builtin_parityl((data_i ^ HSIAO_MASK) & INV_HSIAO[ix]) << ix; + } unsigned err = __builtin_parity(syndrome); @@ -443,7 +348,7 @@ static uint32_t ot_rom_ctrl_verify_ecc_39_32_u32( return data_i & UINT32_MAX; } - uint32_t data_o = 0; + uint32_t data_o = 0u; #define ROM_CTRL_RECOVER(_sy_, _di_, _ix_) \ ((unsigned)((syndrome == (_sy_)) ^ (bool)((_di_) & (1ull << (_ix_)))) \ @@ -500,19 +405,35 @@ static uint32_t ot_rom_ctrl_verify_ecc_39_32_u32( return data_o; } +static uint64_t ot_rom_ctrl_add_ecc_39_32_u64(uint64_t data_i) +{ + uint64_t ecc = 0u; + bool inv = false; + data_i &= UINT32_MAX; + for (unsigned int ix = 0u; ix < INV_HSIAO_COUNT; ix++) { + ecc <<= 1; + bool parity = (bool)__builtin_parityl( + data_i & INV_HSIAO[INV_HSIAO_COUNT - 1u - ix]); + ecc |= (uint64_t)(parity ^ inv); + inv = !inv; + } + return data_i | (ecc << 32u); +} + static void ot_rom_ctrl_unscramble(OtRomCtrlState *s, const uint64_t *src, - uint32_t *dst, unsigned size) + uint32_t *dst) { - unsigned scr_word_size = (size - OT_ROM_DIGEST_BYTES) / sizeof(uint32_t); - unsigned log_addr = 0; + unsigned scr_word_size = (s->size - OT_ROM_DIGEST_BYTES) / sizeof(uint32_t); + unsigned dword_count = (s->size * 2u) / sizeof(uint64_t); + unsigned log_addr = 0u; /* unscramble the whole ROM, except the trailing ROM digest bytes */ - s->recovered_error_count = 0; - s->unrecoverable_error_count = 0; + s->recovered_error_count = 0u; + s->unrecoverable_error_count = 0u; for (; log_addr < scr_word_size; log_addr++) { unsigned phy_addr = ot_rom_ctrl_addr_sp_enc(s, log_addr); - g_assert(phy_addr < size); - uint64_t srcdata = src[phy_addr]; - uint64_t clrdata = ot_rom_ctrl_unscramble_word(s, log_addr, srcdata); + g_assert(phy_addr < dword_count); + uint64_t scrdata = src[phy_addr]; + uint64_t clrdata = ot_rom_ctrl_unscramble_word(s, log_addr, scrdata); dst[log_addr] = (uint32_t)clrdata; unsigned err; uint32_t fixdata = ot_rom_ctrl_verify_ecc_39_32_u32(s, clrdata, &err); @@ -525,30 +446,135 @@ static void ot_rom_ctrl_unscramble(OtRomCtrlState *s, const uint64_t *src, } } /* recover the ROM digest bytes, which are not scrambled */ - for (unsigned wix = 0; wix < ROM_DIGEST_WORDS; wix++, log_addr++) { + for (unsigned wix = 0u; wix < ROM_DIGEST_WORDS; wix++, log_addr++) { unsigned phy_addr = ot_rom_ctrl_addr_sp_enc(s, log_addr); - g_assert(phy_addr < size); + g_assert(phy_addr < dword_count); s->regs[R_EXP_DIGEST_0 + wix] = (uint32_t)src[phy_addr]; /* note: ECC is not used for DIGEST words */ } } +static void ot_rom_ctrl_scramble(OtRomCtrlState *s, const uint32_t *src, + uint64_t *dst) +{ + if (!s->key_xstr || !s->nonce_xstr) { + trace_ot_rom_ctrl_missing(s->ot_id, + "missing key/nonce to fake scrambling"); + return; + } + + unsigned scr_word_size = (s->size - OT_ROM_DIGEST_BYTES) / sizeof(uint32_t); + for (unsigned log_addr = 0u; log_addr < scr_word_size; log_addr++) { + uint64_t clrdata = src[log_addr]; + uint64_t eclrdata = ot_rom_ctrl_add_ecc_39_32_u64(clrdata); + uint64_t scrdata = ot_rom_ctrl_scramble_word(s, log_addr, eclrdata); + dst[log_addr] = scrdata; + } +} + +static void ot_rom_ctrl_compare_and_notify(OtRomCtrlState *s) +{ + /* compare digests */ + bool rom_good = true; + for (unsigned ix = 0u; ix < ROM_DIGEST_WORDS; ix++) { + if (s->regs[R_EXP_DIGEST_0 + ix] != s->regs[R_DIGEST_0 + ix]) { + rom_good = false; + error_setg(&error_fatal, + "ot_rom_ctrl: %s: Digest mismatch (expected 0x%08x got " + "0x%08x) @ %u, errors: %u single-bit, %u double-bit\n", + s->ot_id, s->regs[R_EXP_DIGEST_0 + ix], + s->regs[R_DIGEST_0 + ix], ix, s->recovered_error_count, + s->unrecoverable_error_count); + } + } + + trace_ot_rom_ctrl_notify(s->ot_id, rom_good); + + /* notify end of check */ + ibex_irq_set(&s->pwrmgr_good, rom_good); + ibex_irq_set(&s->pwrmgr_done, true); +} + +static void ot_rom_ctrl_send_kmac_req(OtRomCtrlState *s) +{ + g_assert(s->scrambled.buffer); + g_assert(s->scrambled.word_pos <= s->scrambled.word_count); + + OtKMACAppReq req = { + .msg_len = OT_ROM_CTRL_WORD_BYTES, + }; + stq_le_p(req.msg_data, s->scrambled.buffer[s->scrambled.word_pos]); + req.last = ++s->scrambled.word_pos == s->scrambled.word_count; + + OtKMACClass *kc = OT_KMAC_GET_CLASS(s->kmac); + kc->app_request(s->kmac, s->kmac_app, &req); +} + +static void +ot_rom_ctrl_handle_kmac_response(void *opaque, const OtKMACAppRsp *rsp) +{ + OtRomCtrlState *s = OT_ROM_CTRL(opaque); + + if (!rsp->done) { + ot_rom_ctrl_send_kmac_req(s); + return; + } + g_assert(s->scrambled.word_pos == s->scrambled.word_count); + + qemu_vfree(s->scrambled.buffer); + s->scrambled.word_count = 0u; + + /* + * switch to ROMD mode if no unrecoverable ECC error has been detected. + * Note that real HW does this on a per 32-bit address basis, but as any + * error triggers an invalid digest and prevents the Ibex core from booting, + * this use case is mostly useless anyway. + */ + memory_region_rom_device_set_romd(&s->mem, + s->unrecoverable_error_count == 0u); + + /* retrieve digest */ + for (unsigned ix = 0u; ix < ROM_DIGEST_WORDS; ix++) { + uint32_t share0; + uint32_t share1; + memcpy(&share0, &rsp->digest_share0[ix * sizeof(uint32_t)], + sizeof(uint32_t)); + memcpy(&share1, &rsp->digest_share1[ix * sizeof(uint32_t)], + sizeof(uint32_t)); + s->regs[R_DIGEST_0 + ix] = share0 ^ share1; + if (s->scrambled.local_gen) { + s->regs[R_EXP_DIGEST_0 + ix] = s->regs[R_DIGEST_0 + ix]; + } + } + + if (trace_event_get_state(TRACE_OT_ROM_CTRL_COMPUTED_DIGEST)) { + uint8_t digest[OT_ROM_DIGEST_BYTES]; + for (unsigned ix = 0u; ix < OT_ROM_DIGEST_BYTES; ix++) { + digest[ix] = rsp->digest_share0[ix] ^ rsp->digest_share1[ix]; + } + trace_ot_rom_ctrl_computed_digest( + s->ot_id, ot_common_lhexdump(digest, OT_ROM_DIGEST_BYTES, true, + s->hexstr, OT_ROM_CTRL_HEXSTR_SIZE)); + } + + trace_ot_rom_ctrl_digest_mode(s->ot_id, "stored"); + + /* compare digests and send notification */ + ot_rom_ctrl_compare_and_notify(s); +} + static void ot_rom_ctrl_spawn_hash_calculation( - OtRomCtrlState *s, uintptr_t baseptr, bool scrambled_n_ecc) + OtRomCtrlState *s, uintptr_t scramble_ptr, bool generate) { - g_assert(baseptr % sizeof(uint64_t) == 0); + g_assert(scramble_ptr % sizeof(uint64_t) == 0ull); - s->scrambled_n_ecc = scrambled_n_ecc; - s->se_buffer = (uint64_t *)baseptr; + OtRomCtrlScrambled *scrambled = &s->scrambled; unsigned word_count = (s->size - OT_ROM_DIGEST_BYTES) / sizeof(uint32_t); - if (scrambled_n_ecc) { - s->se_word_bytes = OT_ROM_CTRL_WORD_BYTES; - s->se_last_pos = word_count * OT_ROM_CTRL_WORD_BYTES; - } else { - s->se_word_bytes = sizeof(uint64_t); - s->se_last_pos = word_count * sizeof(uint32_t); - } - s->se_pos = 0; + scrambled->buffer = (uint64_t *)scramble_ptr; + scrambled->word_count = word_count; + scrambled->word_pos = 0u; + scrambled->local_gen = generate; + ot_rom_ctrl_send_kmac_req(s); } @@ -575,7 +601,12 @@ static void ot_rom_ctrl_load_elf(OtRomCtrlState *s, const OtRomImg *ri) } uintptr_t hostptr = (uintptr_t)memory_region_get_ram_ptr(&s->mem); - ot_rom_ctrl_spawn_hash_calculation(s, hostptr, false); + + unsigned load_size = s->size * 2u; + uintptr_t tmpptr = (uintptr_t)qemu_memalign(sizeof(uint64_t), load_size); + ot_rom_ctrl_scramble(s, (const uint32_t *)hostptr, (uint64_t *)tmpptr); + + ot_rom_ctrl_spawn_hash_calculation(s, tmpptr, true); } static void ot_rom_ctrl_load_binary(OtRomCtrlState *s, const OtRomImg *ri) @@ -612,7 +643,11 @@ static void ot_rom_ctrl_load_binary(OtRomCtrlState *s, const OtRomImg *ri) memory_region_set_dirty(&s->mem, 0, ri->raw_size); - ot_rom_ctrl_spawn_hash_calculation(s, hostptr, false); + unsigned load_size = s->size * 2u; + uintptr_t tmpptr = (uintptr_t)qemu_memalign(sizeof(uint64_t), load_size); + ot_rom_ctrl_scramble(s, (const uint32_t *)hostptr, (uint64_t *)tmpptr); + + ot_rom_ctrl_spawn_hash_calculation(s, tmpptr, true); } static char *ot_rom_ctrl_read_text_file(OtRomCtrlState *s, const OtRomImg *ri) @@ -682,17 +717,17 @@ static void ot_rom_ctrl_load_vmem(OtRomCtrlState *s, const OtRomImg *ri, char *line; for (line = strtok_r(buffer, sep, &brks); line; line = strtok_r(NULL, sep, &brks)) { - if (strlen(line) == 0) { + if (strlen(line) == 0u) { continue; } gchar **items = g_strsplit_set(line, " ", 0); - if (items[0][0] != '@') { /* block address */ + if (items[0u][0u] != '@') { /* block address */ g_strfreev(items); continue; } - unsigned blk_addr = (unsigned)g_ascii_strtoull(&items[0][1], NULL, 16); + unsigned blk_addr = (unsigned)g_ascii_strtoull(&items[0u][1], NULL, 16); if (blk_addr < exp_addr) { g_strfreev(items); g_free(buffer); @@ -707,7 +742,7 @@ static void ot_rom_ctrl_load_vmem(OtRomCtrlState *s, const OtRomImg *ri, memptr += pad_size; } - unsigned blk_count = 0; + unsigned blk_count = 0u; while (items[1u + blk_count]) { blk_count++; } @@ -719,7 +754,7 @@ static void ot_rom_ctrl_load_vmem(OtRomCtrlState *s, const OtRomImg *ri, return; } - for (unsigned blk = 0; blk < blk_count; blk++) { + for (unsigned blk = 0u; blk < blk_count; blk++) { uint64_t value = g_ascii_strtoull(items[1u + blk], NULL, 16); if (!scrambled_n_ecc) { /* direct store to ROM controller memory */ @@ -736,19 +771,43 @@ static void ot_rom_ctrl_load_vmem(OtRomCtrlState *s, const OtRomImg *ri, } g_free(buffer); - if (memptr > baseptr) { - if (scrambled_n_ecc) { - uintptr_t dst = (uintptr_t)memory_region_get_ram_ptr(&s->mem); - g_assert((dst & 0x3u) == 0); - ot_rom_ctrl_unscramble(s, (const uint64_t *)baseptr, - (uint32_t *)dst, - s->size /* destination size */); - } + uintptr_t scrptr; - memory_region_set_dirty(&s->mem, 0, memptr - baseptr); + if (scrambled_n_ecc) { + uintptr_t dst = (uintptr_t)memory_region_get_ram_ptr(&s->mem); + g_assert((dst & 0x3u) == 0u); + ot_rom_ctrl_unscramble(s, (const uint64_t *)baseptr, (uint32_t *)dst); + + /* + * hash buffer needs to be in logicial order, input file is in physical + * order. Create an intermediate copy + */ + uint64_t *logbuf = qemu_memalign(sizeof(uint64_t), load_size); + const uint64_t *phybuf = (const uint64_t *)baseptr; + unsigned dword_count = load_size / sizeof(uint64_t); + for (unsigned log_addr = 0u; log_addr < dword_count; log_addr++) { + unsigned phy_addr = ot_rom_ctrl_addr_sp_enc(s, log_addr); + g_assert(phy_addr < dword_count); + logbuf[log_addr] = phybuf[phy_addr]; + } + /* physical buffer is not longer needed */ + qemu_vfree((void *)baseptr); - ot_rom_ctrl_spawn_hash_calculation(s, baseptr, scrambled_n_ecc); + /* hash completion discard temporary baseptr */ + scrptr = (uintptr_t)logbuf; + } else { + load_size = s->size * 2u; + uintptr_t tmpptr = + (uintptr_t)qemu_memalign(sizeof(uint64_t), load_size); + ot_rom_ctrl_scramble(s, (const uint32_t *)baseptr, (uint64_t *)tmpptr); + /* + * hash completion discard temporary tmpptr, + * baseptr points to the real QEMU host memory and should be kept. + */ + scrptr = tmpptr; } + + ot_rom_ctrl_spawn_hash_calculation(s, scrptr, !scrambled_n_ecc); } static void ot_rom_ctrl_load_hex(OtRomCtrlState *s, const OtRomImg *ri) @@ -776,7 +835,7 @@ static void ot_rom_ctrl_load_hex(OtRomCtrlState *s, const OtRomImg *ri) char *line; for (line = strtok_r(buffer, sep, &brks); line; line = strtok_r(NULL, sep, &brks)) { - if (strlen(line) == 0) { + if (strlen(line) == 0u) { continue; } @@ -801,9 +860,8 @@ static void ot_rom_ctrl_load_hex(OtRomCtrlState *s, const OtRomImg *ri) g_free(buffer); uintptr_t dst = (uintptr_t)memory_region_get_ram_ptr(&s->mem); - g_assert((dst & 0x3u) == 0); - ot_rom_ctrl_unscramble(s, (const uint64_t *)baseptr, (uint32_t *)dst, - s->size /* destination size */); + g_assert((dst & 0x3u) == 0u); + ot_rom_ctrl_unscramble(s, (const uint64_t *)baseptr, (uint32_t *)dst); if (memptr > baseptr) { memory_region_set_dirty(&s->mem, 0, memptr - baseptr); @@ -816,7 +874,23 @@ static void ot_rom_ctrl_load_hex(OtRomCtrlState *s, const OtRomImg *ri) return; } - ot_rom_ctrl_spawn_hash_calculation(s, baseptr, true); + /* + * hash buffer needs to be in logicial order, input file is in physical + * order. Create an intermediate copy + */ + uint64_t *logbuf = qemu_memalign(sizeof(uint64_t), load_size); + const uint64_t *phybuf = (const uint64_t *)baseptr; + unsigned dword_count = load_size / sizeof(uint64_t); + for (unsigned log_addr = 0u; log_addr < dword_count; log_addr++) { + unsigned phy_addr = ot_rom_ctrl_addr_sp_enc(s, log_addr); + g_assert(phy_addr < dword_count); + logbuf[log_addr] = phybuf[phy_addr]; + } + /* physical buffer is not longer needed */ + qemu_vfree((void *)baseptr); + + /* hash completion takes care of freeing the buffer */ + ot_rom_ctrl_spawn_hash_calculation(s, (uintptr_t)logbuf, false); } } @@ -829,8 +903,16 @@ static void ot_rom_ctrl_load_rom(OtRomCtrlState *s) obj = object_resolve_path_component(object_get_objects_root(), s->ot_id); if (!obj) { trace_ot_rom_ctrl_load_rom_no_image(s->ot_id); - uintptr_t hostptr = (uintptr_t)memory_region_get_ram_ptr(&s->mem); - ot_rom_ctrl_spawn_hash_calculation(s, hostptr, false); + /* + * when no ROM is present, fake an empty digest. This use case is not + * valid on real HW. In QEMU, it is a shortcut to run the platform + * for specific test cases. Note that the KeyManager may not produce + * valid results in such a case. + */ + memset(&s->regs[R_EXP_DIGEST_0], 0, + ROM_DIGEST_WORDS * sizeof(uint32_t)); + memset(&s->regs[R_DIGEST_0], 0, ROM_DIGEST_WORDS * sizeof(uint32_t)); + ot_rom_ctrl_compare_and_notify(s); return; } rom_img = (OtRomImg *)object_dynamic_cast(obj, TYPE_OT_ROM_IMG); @@ -905,12 +987,12 @@ static uint64_t ot_rom_ctrl_regs_read(void *opaque, hwaddr addr, unsigned size) qemu_log_mask(LOG_GUEST_ERROR, "%s: W/O register 0x%02" HWADDR_PRIx " (%s)\n", __func__, addr, REG_NAME(reg)); - val32 = 0; + val32 = 0u; break; default: qemu_log_mask(LOG_GUEST_ERROR, "%s: Bad offset 0x%" HWADDR_PRIx "\n", __func__, addr); - val32 = 0; + val32 = 0u; break; } @@ -1038,7 +1120,7 @@ static void ot_rom_ctrl_parse_hexstr(const char *name, uint8_t **buf, } uint8_t *out = g_new0(uint8_t, size); - for (unsigned ix = 0; ix < len; ix++) { + for (unsigned ix = 0u; ix < len; ix++) { if (!g_ascii_isxdigit(hexstr[ix])) { g_free(out); *buf = NULL; @@ -1098,8 +1180,8 @@ static void ot_rom_ctrl_reset_hold(Object *obj, ResetType type) memset(memory_region_get_ram_ptr(&s->mem), 0, s->size); memset(s->regs, 0, REGS_SIZE); } else { - s->regs[R_ALERT_TEST] = 0; - s->regs[R_FATAL_ALERT_CAUSE] = 0; + s->regs[R_ALERT_TEST] = 0u; + s->regs[R_FATAL_ALERT_CAUSE] = 0u; } ibex_irq_set(&s->pwrmgr_good, false); @@ -1163,13 +1245,11 @@ static void ot_rom_ctrl_realize(DeviceState *dev, Error **errp) * reads). */ s->first_reset = true; - s->se_buffer = NULL; - fifo8_reset(&s->hash_fifo); memory_region_rom_device_set_romd(&s->mem, false); - unsigned wsize = s->size / sizeof(uint32_t); + size_t wsize = (size_t)s->size / sizeof(uint32_t); unsigned addrbits = ctz32(wsize); - g_assert((wsize & ~(1ull << addrbits)) == 0); + g_assert((wsize & ~(1ull << addrbits)) == 0ull); uint8_t *bytes; @@ -1205,8 +1285,6 @@ static void ot_rom_ctrl_init(Object *obj) ibex_qdev_init_irq(obj, &s->alert, OT_DEVICE_ALERT); - fifo8_create(&s->hash_fifo, OT_KMAC_APP_MSG_BYTES); - object_property_add_bool(obj, "load", NULL, &ot_rom_ctrl_set_load); object_property_set_description(obj, "load", "Trigger initial ROM loading"); diff --git a/hw/opentitan/ot_rom_ctrl_img.c b/hw/opentitan/ot_rom_ctrl_img.c index f8d43b20e0607..8b8fe71b1961d 100644 --- a/hw/opentitan/ot_rom_ctrl_img.c +++ b/hw/opentitan/ot_rom_ctrl_img.c @@ -48,6 +48,7 @@ static OtRomImgFormat ot_rom_img_guess_image_format(const char *filename) uint8_t data[128u]; ssize_t len = read(fd, data, sizeof(data)); + data[sizeof(data) - 1u] = '\0'; close(fd); if (len < sizeof(data)) { @@ -58,10 +59,28 @@ static OtRomImgFormat ot_rom_img_guess_image_format(const char *filename) return OT_ROM_IMG_FORMAT_ELF; } - if (data[0] == '@') { /* likely a VMEM file */ + /* + * Discard comments; only cope with single-line comments: + * we do not need to handle more, since OT-generated files do not contain + * multi-line comments in VMEM files; keep it as simple as possible. + */ + if (data[0u] == '/' && (data[1u] == '*' || data[1u] == '/')) { + for (unsigned ix = 2u; ix < sizeof(data) - 1u; ix++) { + if (data[ix] == '\n') { + unsigned rem = sizeof(data) - 1u - ix; + memmove(&data[0], &data[ix + 1u], rem); + len = read(fd, &data[rem], sizeof(data) - rem); + (void)len; /* GCC unused result warning */ + data[sizeof(data) - 1u] = '\0'; + break; + } + } + } + + if (data[0u] == '@') { /* likely a VMEM file */ bool addr = true; - unsigned dlen = 0; - for (unsigned ix = 1; ix < sizeof(data); ix++) { + unsigned dlen = 0u; + for (unsigned ix = 1u; ix < sizeof(data); ix++) { if (data[ix] == ' ') { /* separator */ if (addr) { addr = false; @@ -87,9 +106,9 @@ static OtRomImgFormat ot_rom_img_guess_image_format(const char *filename) } bool hexa_only = true; - unsigned cr = 0; + unsigned cr = 0u; unsigned ix; - for (ix = 0; ix < sizeof(data); ix++) { + for (ix = 0u; ix < sizeof(data); ix++) { if (data[ix] == '\r') { cr = ix; continue; diff --git a/hw/opentitan/trace-events b/hw/opentitan/trace-events index 3ecbbd1291cdf..924d7910b6fbe 100644 --- a/hw/opentitan/trace-events +++ b/hw/opentitan/trace-events @@ -491,6 +491,7 @@ ot_rom_ctrl_load_rom_no_image(const char *id) "%s: ROM image not defined" ot_rom_ctrl_mem_read_out(const char *id, uint32_t addr, uint32_t val, uint32_t pc) "%s: addr=0x%04x, val=0x%08x, pc=0x%x" ot_rom_ctrl_mem_rejects(const char *id, uint32_t addr, bool is_write, uint32_t pc) "%s: addr=0x%04x, is_write=%u, pc=0x%x" ot_rom_ctrl_mem_write(const char *id, uint32_t addr, uint32_t val, uint32_t pc) "%s: addr=0x%04x, val=0x%08x, pc=0x%x" +ot_rom_ctrl_missing(const char *id, const char *error) "%s: %s" ot_rom_ctrl_notify(const char *id, bool rom_good) "%s: ROM good: %u" ot_rom_ctrl_reset(const char *id, const char *phase) "%s: %s" diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index 43558182055ed..e481485fc78f7 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -6,14 +6,27 @@ :author: Emmanuel Blot """ -from binascii import hexlify +from binascii import hexlify, unhexlify +from configparser import RawConfigParser from io import BytesIO from logging import getLogger -from typing import BinaryIO, Optional, Union +from functools import lru_cache +from os.path import basename +from typing import BinaryIO, Optional, TextIO, Union -from ..util.elf import ElfBlob -from ..util.file import guess_file_type -from ..util.prince import PrinceCipher +try: + from itertools import batched +except ImportError: + # workaround for old Python versions (<3.12) + def batched(seq, n_count): + for pos in range(0, len(seq), n_count): + yield seq[pos:pos+n_count] +import re + +from ot.util.elf import ElfBlob +from ot.util.file import guess_file_type +from ot.util.misc import classproperty +from ot.util.prince import PrinceCipher # ruff: noqa: E402 _CRYPTO_EXC: Optional[Exception] = None @@ -28,12 +41,18 @@ class ROMImage: - """ + """ROM controller image helper to manipulate ROM images + + Support scrambling, descrambling, file format conversions. + + :param name: an optional name for logging purposes + :param snp: use legacy substitute and permute stages """ ADDR_SUBST_PERM_ROUNDS = 2 DATA_SUBST_PERM_ROUNDS = 2 - PRINCE_HALF_ROUNDS = 2 + PRINCE_HALF_ROUNDS_SNP = 2 # legacy mode + PRINCE_HALF_ROUNDS = 3 # new mode w/o S&P DATA_BITS = 4 * 8 ECC_BITS = 7 WORD_BITS = DATA_BITS + ECC_BITS @@ -42,41 +61,138 @@ class ROMImage: DIGEST_WORDS = DIGEST_BYTES // 4 SBOX4 = [12, 5, 6, 11, 9, 0, 10, 13, 3, 14, 15, 8, 4, 7, 1, 2] + """PRESENT S-box permutation.""" SBOX4_INV = [5, 14, 15, 8, 12, 1, 2, 13, 11, 4, 6, 3, 0, 7, 9, 10] + """PRESENT S-box inverted permutation.""" + + ECC_39_32 = [0x2606bd25, 0xdeba8050, 0x413d89aa, 0x31234ed1, + 0xc2c1323b, 0x2dcc624c, 0x98505586] + """HSIAO constants for "inverted" 32-bit data with 7-bit SEC-DED.""" + + VMEM_LINE_WIDTH = 80 + """Number of max char per line on generated VMEM files.""" - def __init__(self, name: Union[str, int, None] = None): + def __init__(self, name: Union[str, int, None] = None, snp: bool = False): logname = 'romimg' if isinstance(name, (int, str)): logname = f'{logname}.{name}' self._log = getLogger(logname) + self._snp = snp + self._prince_half_rounds = (self.PRINCE_HALF_ROUNDS if not snp else + self.PRINCE_HALF_ROUNDS_SNP) self._name = name - self._data = b'' - self._key = 0 - self._nonce = 0 + self._clear_data = bytearray() + self._scrambled_words: list[int] = [] # in logical address order + self._key: Optional[int] = None + self._nonce: Optional[int] = None self._addr_nonce = 0 self._data_nonce = 0 self._addr_width = 0 self._khi = 0 self._klo = 0 - self._digest = bytes(32) + self._digest: Optional[bytes] = None def load(self, rfp: BinaryIO, size: Optional[int] = None): + """Load a ROM file. Can either be: + + - a so called 'HEX' file, which is assumed to be scrambled + - a pure raw binary file + - a RISC-V RV32 ELF file + + :param rfp: input binary stream + :param size: optional size, required for binary and ELF file format + """ ftype = guess_file_type(rfp) loader = getattr(self, f'_load_{ftype}', None) if not loader: raise ValueError(f'Unsupported ROM file type: {ftype}') + filename = basename(rfp.name) if rfp.name else '?' + self._log.info('loading ROM image %s as %s file', + filename, ftype.upper()) loader(rfp, size) + def save(self, rfp: BinaryIO, ftype: str) -> None: + """Save a ROM file. + + :param rfp: output binary stream + :param ftype: the output file format. Either 'hex', 'vmem' or 'svem' + """ + saver = getattr(self, f'_save_{ftype.lower()}', None) + if not saver: + raise ValueError(f'Unsupported ROM file type: {ftype}') + filename = basename(rfp.name) if isinstance(rfp.name, str) else '?' + self._log.info('storing ROM image %s as %s file', filename, ftype) + saver(rfp) + + @classproperty + def save_formats(self) -> set[str]: + """Supported output formats.""" + formats = set() + prefix = '_save_' + for item in dir(self): + if item.startswith(prefix): + formats.add(item.removeprefix(prefix).upper()) + return formats + + def load_config(self, config_file: TextIO, rom_idx: Optional[int]) -> None: + """Load ROM parameters from QEMU 'readconfig' file. + + :param config_file: the config file text stream + :param rom_idx: the ROM index (when several ROMs are defined) + """ + config = RawConfigParser() + config.read_file(config_file) + for section in config.sections(): + prefix = 'ot_device ' + if not section.startswith(prefix): + continue + devname = section.removeprefix(prefix).strip(' "') + devdescs = devname.split('.') + devtype = devdescs[0] + if devtype != 'ot-rom_ctrl': + continue + if len(devdescs) > 1: + devinst = devdescs[-1] + prefix = 'rom' + if not devinst.startswith(prefix): + raise ValueError(f'Invalid ROM instance name: {devinst}') + devidx_str = devinst.removeprefix(prefix) + else: + devidx_str = '' + if devidx_str: + devidx = int(devidx_str) + if devidx != rom_idx: + continue + elif rom_idx: + continue + for opt in config.options(section): + val = config.get(section, opt).strip('"') + setattr(self, opt, unhexlify(val)) + return + if rom_idx is None: + rom_idx = 'undefined' + raise ValueError(f"Unable to find configuration for ROM '{rom_idx}'") + @property - def digest(self): + def digest(self) -> bytes: + """Return the current digest of the ROM image. + + Digest is computed on-the-fly if not already known. + + :return: the digest + """ + if not self._digest: + self._make_digest() return self._digest @property - def key(self): - return self._key.to_bytes(16, 'big') + def key(self) -> Optional[int]: + """Key observer.""" + return None if self._key is None else self._key.to_bytes(16, 'big') @key.setter - def key(self, value: bytes): + def key(self, value: bytes) -> None: + """Key modifier.""" if not isinstance(value, bytes): raise TypeError('Key must be bytes') self._key = int.from_bytes(value, 'big') @@ -84,74 +200,22 @@ def key(self, value: bytes): self._klo = self._key & 0xFFFFFFFFFFFFFFFF @property - def nonce(self): - return self._nonce.to_bytes(8, 'big') + def nonce(self) -> Optional[int]: + """Nonce observer.""" + return None if self._nonce is None else self._nonce.to_bytes(8, 'big') @nonce.setter def nonce(self, value: bytes): + """Nonce modifier.""" if not isinstance(value, bytes): raise TypeError('Nonce must be bytes') self._nonce = int.from_bytes(value, 'big') self._addr_nonce = 0 self._data_nonce = 0 - def _load_hex(self, rfp: BinaryIO, size: Optional[int] = None) -> None: - data: list[int] = [] # 64-bit values - for lpos, line in enumerate(rfp.readlines(), start=1): - line = line.strip() - if len(line) != 10: - raise ValueError(f'Unsupported ROM HEX format at line {lpos}') - try: - data.append(int(line, 16)) - except ValueError as exc: - raise ValueError(f'Invalid HEX data at line {lpos}: {exc}') - word_size = lpos - addr_bits = self.ctz(word_size) - data_nonce_width = 64 - addr_bits - self._addr_nonce = self._nonce >> data_nonce_width - self._data_nonce = self._nonce & ((1 << data_nonce_width) - 1) - self._addr_width = addr_bits - self._log.info('data_nonce_width: %d', data_nonce_width) - self._log.info('addr_width: %d', self._addr_width) - self._log.info('addr_nonce: %06x', self._addr_nonce) - self._log.info('data_nonce: %012x', self._data_nonce) - self._log.info('key_hi: %016x', self._khi) - self._log.info('key_lo: %016x', self._klo) - digest = self._unscramble(data) - bndigest = bytes(reversed(digest)) - self._log.info('digest: %s', hexlify(bndigest).decode()) - self._digest = digest - - def _load_bin(self, rfp: BinaryIO, size: Optional[int] = None) -> None: - if not size: - raise ValueError('ROM size not specified') - if _CRYPTO_EXC: - raise ModuleNotFoundError('Crypto module not found') - data = bytearray(rfp.read()) - # digest storage is not included in digest computation - data_len = len(data) - size -= self.DIGEST_BYTES - if data_len > size: - raise ValueError('ROM size is too small') - if data_len < size: - data.extend(bytes(size - data_len)) - shake = cSHAKE256.new(custom=b'ROM_CTRL') - shake.update(data) - digest = shake.read(32) - self._log.info('size: %d bytes', size) - bndigest = bytes(reversed(digest)) - self._log.info('digest: %s', hexlify(bndigest).decode()) - self._digest = digest - self._data = data - - def _load_elf(self, rfp: BinaryIO, size: Optional[int] = None) -> None: - elf = ElfBlob() - elf.load(rfp) - bin_io = BytesIO(elf.blob) - self._load_bin(bin_io, size) - @classmethod - def ctz(cls, val): + def ctz(cls, val: int) -> int: + """Count trailing zero bit in an integer.""" if val == 0: raise ValueError('CTZ undefined') pos = 0 @@ -161,11 +225,13 @@ def ctz(cls, val): return pos @classmethod - def bitswap(cls, in_, mask, shift): + def bitswap(cls, in_: int, mask: int, shift: int) -> int: + """Bit swapping helper function.""" return ((in_ & mask) << shift) | ((in_ & ~mask) >> shift) @classmethod - def bitswap64(cls, val): + def bitswap64(cls, val: int) -> int: + """Swap (reverse) 64-bit integer.""" val = cls.bitswap(val, 0x5555555555555555, 1) val = cls.bitswap(val, 0x3333333333333333, 2) val = cls.bitswap(val, 0x0f0f0f0f0f0f0f0f, 4) @@ -176,7 +242,8 @@ def bitswap64(cls, val): return val @classmethod - def sbox(cls, in_, width, sbox): + def sbox(cls, in_: int, width: int, sbox: int) -> int: + """PRESENT S-box permutation.""" assert width < 64 full_mask = (1 << width) - 1 @@ -191,14 +258,16 @@ def sbox(cls, in_, width, sbox): return out @classmethod - def flip(cls, in_, width): + def flip(cls, in_: int, width: int) -> int: + """Reverse N bit in an integer.""" out = cls.bitswap64(in_) out >>= 64 - width return out @classmethod - def perm(cls, in_, width, invert): + def perm(cls, in_: int, width: int, invert: bool) -> int: + """PRESENT permutation.""" assert width < 64 full_mask = (1 << width) - 1 @@ -224,7 +293,9 @@ def perm(cls, in_, width, invert): return out @classmethod - def subst_perm_enc(cls, in_, key, width, num_round): + def subst_perm_enc(cls, in_: int, key: int, width: int, num_round: int) \ + -> int: + """Substitute-permute rounds.""" state = in_ while num_round: num_round -= 1 @@ -237,7 +308,9 @@ def subst_perm_enc(cls, in_, key, width, num_round): return state @classmethod - def subst_perm_dec(cls, val, key, width, num_round): + def subst_perm_dec(cls, val: int, key: int, width: int, num_round: int) \ + -> int: + """Substitute-permute rounds.""" state = val while num_round: num_round -= 1 @@ -249,54 +322,344 @@ def subst_perm_dec(cls, val, key, width, num_round): return state - def addr_sp_enc(self, addr): + def addr_sp_enc(self, addr: int) -> int: + """Encode a logical (CPU) address into a physical (ROM storage) address. + """ return self.subst_perm_enc(addr, self._addr_nonce, self._addr_width, self.ADDR_SUBST_PERM_ROUNDS) + def addr_sp_dec(self, addr: int) -> int: + """Decode a physical (ROM storage) address into a logical (CPU) address. + """ + return self.subst_perm_dec(addr, self._addr_nonce, self._addr_width, + self.ADDR_SUBST_PERM_ROUNDS) + + @classmethod + def add_ecc_inv_39_32(cls, data: int) -> int: + """Compute and add HSIAO SEC-DEC to a 32 bit value. + + :param data: 32-bit value + :return: 39-bit value (upper bits contain the ECC) + """ + ecc = 0 + inv = False + for mask in reversed(cls.ECC_39_32): + ecc <<= 1 + parity = (data & mask).bit_count() & 1 + ecc |= parity ^ int(inv) + inv = not inv + return (ecc << 32) | data + + @classmethod + @lru_cache + def data_sp_enc(cls, val: int) -> int: + """Encode (scramble) data.""" + return cls.subst_perm_enc(val, 0, cls.WORD_BITS, + cls.DATA_SUBST_PERM_ROUNDS) + @classmethod - def data_sp_dec(cls, val): + @lru_cache + def data_sp_dec(cls, val: int) -> int: + """Decode (unscramble) data.""" return cls.subst_perm_dec(val, 0, cls.WORD_BITS, cls.DATA_SUBST_PERM_ROUNDS) + def _load_hex(self, rfp: BinaryIO, size: Optional[int]) -> None: + words: list[int] = [] # 64-bit values + for lpos, line in enumerate(rfp.readlines(), start=1): + line = line.strip() + if len(line) != 10: + raise ValueError(f'Unsupported ROM HEX format at line {lpos}') + try: + words.append(int(line, 16)) + except ValueError as exc: + raise ValueError(f'Invalid HEX data at line {lpos}: {exc}') + if size is not None and size != len(words) * 4: + raise ValueError('HEX content does not match ROM size') + self._handle_scrambled_data(words) + + def _load_svmem(self, rfp: BinaryIO, size: Optional[int]) -> None: + # load VMEM handle both kinds of VMEM files + self._load_vmem(rfp, size) + + def _load_vmem(self, rfp: BinaryIO, size: Optional[int]) -> None: + words: list[int] = [] # 64-bit values + scrambled: Optional[bool] = None + next_addr = 0 + # note: address marker (@address) defines the index in the destination + # memory. For ROM images, each index represents a 32-bit memory address. + # Each location contains 32 bits of data, and optionally 7 bits of ECC. + # ECC extension is only supported in scrambled files. The ECC is stored + # in the MSB of each VMEM word. + for lpos, line in enumerate(rfp.readlines(), start=1): + line = line.strip() + if not line.startswith(b'@'): + continue + parts = re.split(r'\s+', line[1:]) + address_str = parts[0] + data_str = parts[1:] + scrambled_data = all(len(d) in (9, 10) for d in data_str) + plain_data = all(len(d) in (7, 8) for d in data_str) + if not scrambled_data ^ plain_data: + raise ValueError(f'Unknown VMEM format @ {lpos}') + if scrambled is None: + scrambled = scrambled_data + self._log.info('Identified %s as %s VMEM format', + rfp.name or '?', + 'scrambled' if scrambled else 'plain') + elif scrambled != scrambled_data: + raise ValueError(f'Incoherent VMEM format @ {lpos}') + try: + address = int(address_str, 16) + words.extend(int(d, 16) for d in data_str) + except (TypeError, ValueError) as exc: + raise ValueError(f'Invalid data in VMEM format @ ' + f'{lpos}') from exc + if address != next_addr: + raise ValueError(f'Incoherent next address @ {lpos}') + next_addr += len(data_str) + if scrambled is None: + self._log.error('No valid data found in VMEM file') + return + if scrambled: + if size is not None and size != len(words) * 4: + raise ValueError(f'ROM size ({size}) does not match the ' + f'scrambled content size ({len(words) * 4})') + self._handle_scrambled_data(words) + else: + if not size: + raise ValueError('ROM size not specified') + clrdata = bytearray() + for word in words: + clrdata.extend(word.to_bytes(4, 'little')) + data_len = len(clrdata) + if data_len < size: + clrdata.extend(bytes(size - data_len)) + self._clear_data = clrdata + bndigest = bytes(reversed(self.digest)) + self._log.info('computed digest: %s', hexlify(bndigest).decode()) + + def _load_bin(self, rfp: BinaryIO, size: Optional[int]) -> None: + if not size: + raise ValueError('ROM size not specified') + if _CRYPTO_EXC: + raise ModuleNotFoundError('Crypto module not found') + data = bytearray(rfp.read()) + data_len = len(data) + if data_len > size: + raise ValueError(f'Specified ROM size is too small to fit ' + f'{rfp.name or '?'}') + if data_len < size: + data.extend(bytes(size - data_len)) + self._clear_data = data + bndigest = bytes(reversed(self.digest)) + self._log.info('computed digest: %s', hexlify(bndigest).decode()) + + def _load_elf(self, rfp: BinaryIO, size: Optional[int]) -> None: + elf = ElfBlob() + elf.load(rfp) + bin_io = BytesIO(elf.blob) + self._load_bin(bin_io, size) + + def _save_hex(self, rfp: BinaryIO) -> None: + # assume a scrambled output image + self._prepare_scrambled_data() + scrwords = self._scrambled_words + rfp.write('\n'.join(f'{scrwords[self.addr_sp_dec(pa)]:010X}' + for pa in range(len(scrwords))).encode()) + rfp.write(b'\n') + + def _save_svmem(self, rfp: BinaryIO) -> None: + # create scrambled data if not yet available + self._prepare_scrambled_data() + scrwords = [self._scrambled_words[self.addr_sp_dec(pa)] + for pa in range(len(self._scrambled_words))] + word_char = 10 + addr_char = 8 + 1 + word_per_line = (self.VMEM_LINE_WIDTH - addr_char) // (word_char + 1) + self._print_vmem(rfp, scrwords, word_per_line, word_char) + + def _save_vmem(self, rfp: BinaryIO) -> None: + # scrambled output image + clrwords = [int.from_bytes(bs, 'little') + for bs in batched(self._clear_data, 4)] + word_char = 8 + addr_char = 8 + 1 + word_per_line = (self.VMEM_LINE_WIDTH - addr_char) // (word_char + 1) + self._print_vmem(rfp, clrwords, word_per_line, word_char) + + def _print_vmem(self, rfp: BinaryIO, words: list[int], + word_per_line: int, word_char: int) -> None: + pos = 0 + # note: address marker (@address) defines the index in the destination + # memory. See _load_vmem for details + for words in batched(words, word_per_line): + data = ' '.join(f'{w:0{word_char}X}' for w in words) + rfp.write(f'@{pos:08X} {data}\n'.encode()) + pos += len(words) + + def _save_bin(self, rfp: BinaryIO) -> None: + rfp.write(self._clear_data) + + def _prepare_scrambled_data(self): + if self._key is None: + raise RuntimeError('Key not defined, cannot scramble HEX file') + if self._nonce is None: + raise RuntimeError('Nonce not defined, cannot scramble HEX file') + if not self._clear_data: + self._log.warning('No data to save') + return + # ensure scrambled data and digest are generated + bndigest = bytes(reversed(self.digest)) + self._log.info('computed digest: %s', hexlify(bndigest).decode()) + + def _handle_scrambled_data(self, data: list[int]) -> None: + word_count = len(data) + addr_bits = self.ctz(word_count) + data_nonce_width = 64 - addr_bits + if self._key is None: + raise RuntimeError('Key not defined, cannot unscramble HEX file') + if self._nonce is None: + raise RuntimeError('Nonce not defined, cannot unscramble HEX file') + self._addr_nonce = self._nonce >> data_nonce_width + self._data_nonce = self._nonce & ((1 << data_nonce_width) - 1) + self._addr_width = addr_bits + self._log.debug('nonce_width: %d', data_nonce_width) + self._log.debug('addr_width: %d', self._addr_width) + self._log.debug('addr_nonce: %06x', self._addr_nonce) + self._log.debug('data_nonce: %012x', self._data_nonce) + self._log.debug('key_hi: %016x', self._khi) + self._log.debug('key_lo: %016x', self._klo) + self._unscramble(data) + bndigest = bytes(reversed(self._digest)) + self._log.info('stored digest: %s', hexlify(bndigest).decode()) + local_digest = self.digest + bndigest = bytes(reversed(local_digest)) + self._log.info('local digest: %s', hexlify(bndigest).decode()) + if local_digest != self._digest: + self._log.error('Digest mismatch') + def _get_keystream(self, addr: int): scramble = (self._data_nonce << self._addr_width) | addr stream = PrinceCipher.run(scramble, self._khi, self._klo, - self.PRINCE_HALF_ROUNDS) + self._prince_half_rounds) return stream & ((1 << self.WORD_BITS) - 1) + def _scramble_word(self, addr: int, value: int): + keystream = self._get_keystream(addr) + if not self._snp: + return keystream ^ value + return self.data_sp_enc(keystream ^ value) + def _unscramble_word(self, addr: int, value: int): keystream = self._get_keystream(addr) + if not self._snp: + return keystream ^ value spd = self.data_sp_dec(value) return keystream ^ spd - - def _unscramble(self, src: list[int]) -> bytes: + def _scramble(self, data: Union[bytes, bytearray]) -> None: + data_len = len(data) + if data_len & (data_len - 1): + self._log.warning('Unexpected data length: %d, not a 2^N value', + data_len) + word_count = data_len // 4 + addr_bits = self.ctz(word_count) + data_nonce_width = 64 - addr_bits + if self._nonce is None: + raise RuntimeError('Nonce not defined, cannot scramble data') + addr_nonce = self._nonce >> data_nonce_width + data_nonce = self._nonce & ((1 << data_nonce_width) - 1) + addr_width = addr_bits + if not self._addr_nonce: + self._addr_nonce = addr_nonce + elif self._addr_nonce != addr_nonce: + raise RuntimeError('Addr nonce discrepancy') + if not self._data_nonce: + self._data_nonce = data_nonce + elif self._data_nonce != data_nonce: + raise RuntimeError('Data nonce discrepancy') + if not self._addr_width: + self._addr_width = addr_width + elif self._addr_width != addr_width: + raise RuntimeError('Addr width discrepancy') + self._log.debug('nonce_width: %d', data_nonce_width) + self._log.debug('addr_width: %d', self._addr_width) + self._log.debug('addr_nonce: %06x', self._addr_nonce) + self._log.debug('data_nonce: %012x', self._data_nonce) + scrambled: list[int] = [] + word_count = len(data) // 4 + dig_addr = len(data) - self.DIGEST_BYTES + for log_addr in range(word_count): + assert 0 <= log_addr < word_count + byte_addr = log_addr << 2 + clrdata = int.from_bytes(data[byte_addr:byte_addr + 4], 'little') + if byte_addr < dig_addr: + clrdata = self.add_ecc_inv_39_32(clrdata) + assert 0 <= clrdata < (1 << self.WORD_BITS), "invalid data" + scrdata = self._scramble_word(log_addr, clrdata) + scrambled.append(scrdata) + else: + # digest is not scrambled and contains no ECC + scrambled.append(clrdata) + self._scrambled_words = scrambled + + def _unscramble(self, scr: list[int]) -> None: # do not attempt to detect or correct errors for now - size = len(src) - scr_word_size = (size - self.DIGEST_BYTES) // 4 + word_count = len(scr) # each slot is a 32-bit data value + ECC + if word_count & (word_count - 1): + self._log.warning('Unexpected word count: %d, not a 2^N value', + word_count) + scr_word_count = word_count - self.DIGEST_BYTES // 4 + self._log.debug('word_count: %d, scr_word_count %d', + word_count, scr_word_count) log_addr = 0 - dst: list[int] = [0] * size - while log_addr < scr_word_size: + dst: list[int] = [0] * word_count # 32-bit values + scrambled_words: list[int] = [] + while log_addr < scr_word_count: phy_addr = self.addr_sp_enc(log_addr) - assert(phy_addr < size) - - srcdata = src[phy_addr] - clrdata = self._unscramble_word(log_addr, srcdata) + assert phy_addr < word_count, "unexpected physical address" + scrdata = scr[phy_addr] + scrambled_words.append(scrdata) + clrdata = self._unscramble_word(log_addr, scrdata) dst[log_addr] = clrdata & 0xffffffff log_addr += 1 + # digest words are not scrambled wix = 0 - digest_parts: list[int] = [] + digest_parts: list[int] = [] # 32-bit values while wix < self.DIGEST_WORDS: phy_addr = self.addr_sp_enc(log_addr) - assert(phy_addr < size) - digest_parts.append(src[phy_addr] & 0xffffffff) + assert phy_addr < word_count, "unexpected physical address" + word = scr[phy_addr] & 0xffffffff + scrambled_words.append(word) + digest_parts.append(word) wix += 1 log_addr += 1 digest = b''.join((dp.to_bytes(4, 'little') for dp in digest_parts)) - for addr in range(0x20, 0x30): - self._log.debug('@ %06x: %08x', addr, dst[addr]) data = bytearray() for val in dst: data.extend(val.to_bytes(4, 'little')) - self._data = bytes(data) - return digest + self._clear_data = data + self._scrambled_words = scrambled_words + self._digest = digest + + def _compute_digest(self) -> None: + scrambled_data = bytearray() + word_bytes = len(self._clear_data) + scr_word_count = (word_bytes - self.DIGEST_BYTES) // 4 + for word in self._scrambled_words[:scr_word_count]: + scrambled_data.extend(word.to_bytes(self.WORD_BYTES, 'little')) + shake = cSHAKE256.new(custom=b'ROM_CTRL') + shake.update(scrambled_data) + self._digest = shake.read(self.DIGEST_BYTES) + if len(self._scrambled_words) > scr_word_count: + self._scrambled_words[:] = self._scrambled_words[:scr_word_count] + digest_words = [int.from_bytes(self._digest[a:a+4], 'little') + for a in range(0, len(self._digest), 4)] + self._scrambled_words.extend(digest_words) + + def _make_digest(self) -> bytes: + if not self._scrambled_words: + self._scramble(self._clear_data) + self._compute_digest() + return self._digest diff --git a/python/qemu/ot/util/file.py b/python/qemu/ot/util/file.py index 34946231dd96e..788221a12edd7 100644 --- a/python/qemu/ot/util/file.py +++ b/python/qemu/ot/util/file.py @@ -36,12 +36,21 @@ def guess_file_type(file: Union[str, BufferedReader]) -> str: return 'elf' if header[:4] == b'OTPT': return 'spiflash' - vmem_re = rb'(?i)^@[0-9A-F]{4,}\s[0-9A-F]{6,}' + vmem_re = rb'(?i)^@[0-9A-F]{4,}\s([0-9A-F]{6,})' for line in header.split(b'\n'): if line.startswith(b'/*') or line.startswith(b'#'): continue - if re.match(vmem_re, line): - return 'vmem' + vmo = re.match(vmem_re, line) + if vmo: + bcount = len(vmo.group(1)) // 2 + # heuristic for VMEM files: + # - plain VMEM contain 16 or 32 data bit per block + # - special VMEM (ECC, scrambled, ...) usually contain an extra byte + # we only need to distinguish this kinds of files for now, it is not + # intended to be used for any purpose outside QEMU & Verilator + # tooling. + reg = bcount & 1 == 0 + return 'vmem' if reg else 'svmem' hex_re = rb'(?i)^[0-9A-F]{6,}' count = 0 for line in header.split(b'\n'): diff --git a/python/qemu/ot/util/prince.py b/python/qemu/ot/util/prince.py index 8aced6c5c6c3a..d5ecf480b8b9c 100644 --- a/python/qemu/ot/util/prince.py +++ b/python/qemu/ot/util/prince.py @@ -4,6 +4,8 @@ """PRINCE cipher implementation. """ +from functools import lru_cache + # pylint: disable=missing-docstring class PrinceCipher: @@ -36,11 +38,6 @@ class PrinceCipher: 0xd3b5a399ca0c2399, 0xc0ac29b7c97c50dd ] - SHIFT_ROWS_CONSTS = [ - 0x7bde, 0xbde7, 0xde7b, 0xe7bd - ] - - @classmethod def sbox(cls, in_: int, width: int, sbox: list[int]) -> int: full_mask = 0 if (width >= 64) else (1 << width) - 1 @@ -56,6 +53,7 @@ def sbox(cls, in_: int, width: int, sbox: list[int]) -> int: return ret @classmethod + @lru_cache(maxsize=65536) def nibble_red16(cls, data: int) -> int: nib0 = (data >> 0) & 0xf nib1 = (data >> 4) & 0xf @@ -63,17 +61,33 @@ def nibble_red16(cls, data: int) -> int: nib3 = (data >> 12) & 0xf return nib0 ^ nib1 ^ nib2 ^ nib3 - @classmethod - def mult_prime(cls, data: int) -> int: - ret = 0 + @staticmethod + def mult_prim_const() -> list[tuple[int, tuple[int, int]]]: + bconsts = [] + shift_rows_consts = [ + 0x7bde, 0xbde7, 0xde7b, 0xe7bd + ] for blk_idx in range(4): - data_hw = (data >> (16 * blk_idx)) & 0xffff + consts = [] start_sr_idx = 0 if blk_idx in (0, 3) else 1 + blk_const = blk_idx * 16 for nibble_idx in range(4): sr_idx = (start_sr_idx + 3 - nibble_idx) & 0x3 - sr_const = cls.SHIFT_ROWS_CONSTS[sr_idx] - nibble = cls.nibble_red16(data_hw & sr_const) - ret |= nibble << (16 * blk_idx + 4 * nibble_idx) + sr_const = shift_rows_consts[sr_idx] + shift_const = blk_const + nibble_idx * 4 + consts.append((shift_const, sr_const)) + bconsts.append((blk_const, consts)) + return bconsts + + MULT_PRIM_CONST = mult_prim_const() + + @classmethod + def mult_prime(cls, data: int) -> int: + ret = 0 + for blk_const, consts in cls.MULT_PRIM_CONST: + data_hw = data >> blk_const + for sh, sr in consts: + ret |= cls.nibble_red16(data_hw & sr) << sh return ret @classmethod diff --git a/scripts/opentitan/romtool.py b/scripts/opentitan/romtool.py new file mode 100755 index 0000000000000..069ee7ae9d90f --- /dev/null +++ b/scripts/opentitan/romtool.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025 Rivos, Inc. +# SPDX-License-Identifier: Apache2 + +"""QEMU OT tool to generate a scrambled ROM image. + + :author: Emmanuel Blot +""" + +# pylint: disable=invalid-name +# pylint: enable=invalid-name + +from argparse import ArgumentParser, FileType +from os.path import dirname, join as joinpath, normpath +from traceback import format_exception +import sys + +QEMU_PYPATH = joinpath(dirname(dirname(dirname(normpath(__file__)))), + 'python', 'qemu') +sys.path.append(QEMU_PYPATH) + +# ruff: noqa: E402 +from ot.rom.image import ROMImage +from ot.util.arg import ArgError +from ot.util.log import configure_loggers +from ot.util.misc import HexInt + + +def main(): + """Main routine""" + debug = True + desc = sys.modules[__name__].__doc__.split('.', 1)[0].strip() + argparser = ArgumentParser(description=f'{desc}.') + out_formats = list(ROMImage.save_formats) + # make hex format, if supported, the first (default) one + out_formats.sort(key=lambda x: x if x != 'HEX' else '') + try: + + files = argparser.add_argument_group(title='Files') + files.add_argument('rom', nargs=1, type=FileType('rb'), + help='input ROM image file') + files.add_argument('-c', '--config', type=FileType('rt'), + metavar='CFG', required=True, + help='input QEMU OT config file') + files.add_argument('-o', '--output', + help='output ROM image file') + + params = argparser.add_argument_group(title='Parameters') + params.add_argument('-i', '--rom-id', type=int, + help='ROM image identifier') + params.add_argument('-z', '--rom-size', metavar='SIZE', + type=HexInt.xparse, + help='ROM image size in bytes (accepts Ki suffix)') + params.add_argument('-f', '--output-format', choices=out_formats, + default=out_formats[0], + help=f'Output file format ' + f'(default: {out_formats[0]})') + params.add_argument('-s', '--subst-perm', action='store_true', + help='Enable legacy mode with S&P mode') + + extra = argparser.add_argument_group(title='Extras') + extra.add_argument('-v', '--verbose', action='count', + help='increase verbosity') + extra.add_argument('-d', '--debug', action='store_true', + help='enable debug mode') + + args = argparser.parse_args() + debug = args.debug + + configure_loggers(args.verbose, 'romimg', -1, name_width=12) + + rom_img = ROMImage(None, args.subst_perm) + rom_img.load_config(args.config, args.rom_id) + rom_img.load(args.rom[0], args.rom_size) + + with open(args.output, 'wb') if args.output else \ + sys.stdout.buffer as wfp: + rom_img.save(wfp, args.output_format) + + except ArgError as exc: + argparser.error(str(exc)) + except (IOError, ValueError, ImportError) as exc: + print(f'\nError: {exc}', file=sys.stderr) + if debug: + print(''.join(format_exception(exc, chain=False)), + file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + sys.exit(2) + + +if __name__ == '__main__': + main()