From 498193e7f9abb421337b7fed8553f9716599c2f7 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 6 Oct 2025 09:27:57 +0200 Subject: [PATCH 01/11] [ot] python/qemu: ot.rom.image: add scramble feature to ROM images Signed-off-by: Emmanuel Blot --- python/qemu/ot/rom/image.py | 372 +++++++++++++++++++++++++++--------- python/qemu/ot/util/file.py | 15 +- 2 files changed, 291 insertions(+), 96 deletions(-) diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index 43558182055ed..2a0c5f3c40c5f 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -9,6 +9,7 @@ from binascii import hexlify from io import BytesIO from logging import getLogger +from os.path import basename from typing import BinaryIO, Optional, Union from ..util.elf import ElfBlob @@ -28,7 +29,11 @@ class ROMImage: - """ + """ROM controller image helper to manipulate ROM images + + Support scrambling, descrambling, file format conversions. + + :param name: an optional name for logging purposes """ ADDR_SUBST_PERM_ROUNDS = 2 @@ -42,7 +47,13 @@ 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.""" def __init__(self, name: Union[str, int, None] = None): logname = 'romimg' @@ -50,33 +61,70 @@ def __init__(self, name: Union[str, int, None] = None): logname = f'{logname}.{name}' self._log = getLogger(logname) 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. Only 'HEX' scrambled file + format is supported for now + """ + saver = getattr(self, f'_save_{ftype}', 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) + @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 +132,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 +157,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 +174,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 +190,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 +225,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 +240,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 +254,235 @@ 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 + 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): + 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: + 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 + 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 _save_hex(self, rfp: BinaryIO) -> None: + # assume a scrambled output image + 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: + return + # ensure scrambled data and digest are generated + digest = self._make_digest() + bndigest = bytes(reversed(digest)) + self._log.info('computed digest: %s', hexlify(bndigest).decode()) + 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 _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 + + 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 _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) return stream & ((1 << self.WORD_BITS) - 1) + def _scramble_word(self, addr: int, value: int): + keystream = self._get_keystream(addr) + return self.data_sp_enc(keystream ^ value) + def _unscramble_word(self, addr: int, value: int): keystream = self._get_keystream(addr) 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'): From cc4c8c50df7a6bd6b19d8a96829b8a417fc44807 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 6 Oct 2025 10:06:02 +0200 Subject: [PATCH 02/11] [ot] python/qemu: ot.rom.image: add support for plain and scrambed VMEM files Signed-off-by: Emmanuel Blot --- python/qemu/ot/rom/image.py | 120 ++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 26 deletions(-) diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index 2a0c5f3c40c5f..8a8242a5eafe2 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -12,6 +12,8 @@ from os.path import basename from typing import BinaryIO, Optional, Union +import re + from ..util.elf import ElfBlob from ..util.file import guess_file_type from ..util.prince import PrinceCipher @@ -295,39 +297,16 @@ def data_sp_dec(cls, val: int) -> int: cls.DATA_SUBST_PERM_ROUNDS) def _load_hex(self, rfp: BinaryIO, size: Optional[int]) -> None: - data: list[int] = [] # 64-bit values + 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: - data.append(int(line, 16)) + words.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 - 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') + self._handle_scrambled_data(words) def _save_hex(self, rfp: BinaryIO) -> None: # assume a scrambled output image @@ -346,6 +325,67 @@ def _save_hex(self, rfp: BinaryIO) -> None: for pa in range(len(scrwords))).encode()) rfp.write(b'\n') + 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) == 10 for d in data_str) + plain_data = all(len(d) == 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') @@ -359,6 +399,8 @@ def _load_bin(self, rfp: BinaryIO, size: Optional[int]) -> None: 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() @@ -366,6 +408,32 @@ def _load_elf(self, rfp: BinaryIO, size: Optional[int]) -> None: bin_io = BytesIO(elf.blob) self._load_bin(bin_io, size) + 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, From 7368f68df1194520d22497d1c2b999e6c939f208 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 6 Oct 2025 10:05:34 +0200 Subject: [PATCH 03/11] [ot] python/qemu: ot.rom.image: add alternative output file formats Signed-off-by: Emmanuel Blot --- python/qemu/ot/rom/image.py | 108 +++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index 8a8242a5eafe2..16016a1f125e0 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -11,12 +11,19 @@ from logging import getLogger from os.path import basename from typing import BinaryIO, Optional, Union - +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 ..util.elf import ElfBlob -from ..util.file import guess_file_type -from ..util.prince import PrinceCipher +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 @@ -57,6 +64,9 @@ class ROMImage: 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): logname = 'romimg' if isinstance(name, (int, str)): @@ -97,16 +107,25 @@ def save(self, rfp: BinaryIO, ftype: str) -> None: """Save a ROM file. :param rfp: output binary stream - :param ftype: the output file format. Only 'HEX' scrambled file - format is supported for now + :param ftype: the output file format. Either 'hex', 'vmem' or 'svem' """ - saver = getattr(self, f'_save_{ftype}', None) + 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 + @property def digest(self) -> bytes: """Return the current digest of the ROM image. @@ -306,25 +325,10 @@ def _load_hex(self, rfp: BinaryIO, size: Optional[int]) -> None: 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 _save_hex(self, rfp: BinaryIO) -> None: - # assume a scrambled output image - 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: - return - # ensure scrambled data and digest are generated - digest = self._make_digest() - bndigest = bytes(reversed(digest)) - self._log.info('computed digest: %s', hexlify(bndigest).decode()) - 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 _load_svmem(self, rfp: BinaryIO, size: Optional[int]) -> None: # load VMEM handle both kinds of VMEM files self._load_vmem(rfp, size) @@ -345,8 +349,8 @@ def _load_vmem(self, rfp: BinaryIO, size: Optional[int]) -> None: parts = re.split(r'\s+', line[1:]) address_str = parts[0] data_str = parts[1:] - scrambled_data = all(len(d) == 10 for d in data_str) - plain_data = all(len(d) == 8 for d in data_str) + 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: @@ -408,6 +412,58 @@ def _load_elf(self, rfp: BinaryIO, size: Optional[int]) -> None: 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) From 8ef36de0ddaf484dd44688afb6aa5f2c4375744e Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 6 Oct 2025 09:30:48 +0200 Subject: [PATCH 04/11] [ot] python/qemu: ot.rom.image: load secrets from QEMU config file Signed-off-by: Emmanuel Blot --- python/qemu/ot/rom/image.py | 45 +++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index 16016a1f125e0..dbdb005f3fc4d 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -6,11 +6,13 @@ :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 os.path import basename -from typing import BinaryIO, Optional, Union +from typing import BinaryIO, Optional, TextIO, Union + try: from itertools import batched except ImportError: @@ -126,6 +128,45 @@ def save_formats(self) -> set[str]: 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) -> bytes: """Return the current digest of the ROM image. From 960522ba8714e5fbb68e35c51d28e373689ff4df Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 2 Oct 2025 20:13:51 +0200 Subject: [PATCH 05/11] [ot] python/qemu: ot.rom.image: add new scrambling mode without S&P Signed-off-by: Emmanuel Blot --- python/qemu/ot/rom/image.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index dbdb005f3fc4d..46783ca62f394 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -45,11 +45,13 @@ class ROMImage: 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 @@ -69,11 +71,14 @@ class ROMImage: 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._clear_data = bytearray() self._scrambled_words: list[int] = [] # in logical address order @@ -534,15 +539,19 @@ def _handle_scrambled_data(self, data: list[int]) -> None: 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 From 4f6d1402e3f4ffd2e69c6620f7df2b911656684f Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 3 Oct 2025 17:33:55 +0200 Subject: [PATCH 06/11] [ot] scripts/opentitan: romtool.py: add a tiny helper script to scramble ROM images Signed-off-by: Emmanuel Blot --- scripts/opentitan/romtool.py | 94 ++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100755 scripts/opentitan/romtool.py 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() From ae547060bb59f7fc0e3f48f6ce6491c11e538cd9 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 2 Oct 2025 19:58:57 +0200 Subject: [PATCH 07/11] [ot] docs/opentitan: romtool: add initial documentation Signed-off-by: Emmanuel Blot --- docs/opentitan/romtool.md | 80 +++++++++++++++++++++++++++++++++++++++ docs/opentitan/tools.md | 1 + 2 files changed, 81 insertions(+) create mode 100644 docs/opentitan/romtool.md 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 From 332465237ab2b4dc700cae67c3c4197832fda9aa Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 4 Sep 2025 15:16:18 +0200 Subject: [PATCH 08/11] [ot] python/qemu: ot.util.prince: improve execution time - cache computation results when it makes sense - avoid computing constants more than once Signed-off-by: Emmanuel Blot --- python/qemu/ot/util/prince.py | 38 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 12 deletions(-) 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 From a606f35070d031219a62b87c5b284457c60e0947 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 4 Sep 2025 15:17:09 +0200 Subject: [PATCH 09/11] [ot] python/qemu: ot.rom.image: improve execution time - cache some computation results Signed-off-by: Emmanuel Blot --- python/qemu/ot/rom/image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/qemu/ot/rom/image.py b/python/qemu/ot/rom/image.py index 46783ca62f394..e481485fc78f7 100644 --- a/python/qemu/ot/rom/image.py +++ b/python/qemu/ot/rom/image.py @@ -10,6 +10,7 @@ from configparser import RawConfigParser from io import BytesIO from logging import getLogger +from functools import lru_cache from os.path import basename from typing import BinaryIO, Optional, TextIO, Union @@ -350,12 +351,14 @@ def add_ecc_inv_39_32(cls, data: int) -> int: 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 + @lru_cache def data_sp_dec(cls, val: int) -> int: """Decode (unscramble) data.""" return cls.subst_perm_dec(val, 0, cls.WORD_BITS, From 6c04f5f70018e5bde75e02f4e83c5d20c0c345c9 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 2 Oct 2025 20:06:34 +0200 Subject: [PATCH 10/11] [ot] hw/opentitan: ot_rom_ctrl: implement local address scrambling and ECC This should enable support for loading ELF, binary and plain text VMEM files as ROM image file while preserving proper KeyManager DPE computations, which is expected to be performed on scrambled address & ECC ROM content. Signed-off-by: Emmanuel Blot --- hw/opentitan/ot_rom_ctrl.c | 468 +++++++++++++++++++++---------------- hw/opentitan/trace-events | 1 + 2 files changed, 274 insertions(+), 195 deletions(-) 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/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" From c6e73ddc9b8bd19f9d06ce5a1e95eb89934ba294 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 2 Oct 2025 20:12:07 +0200 Subject: [PATCH 11/11] [ot] hw/opentitan: ot_rom_ctrl_img: improve VMEM detection resilience srecord can't help adding a useless comment before the generated VMEM file Skip what may look like a C comment to better detect text file format. Signed-off-by: Emmanuel Blot --- hw/opentitan/ot_rom_ctrl_img.c | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) 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;