From 6bc1dc6b7dc76fd9268820bb3eff13e8fe6d719c Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 26 Sep 2025 14:33:39 +0200 Subject: [PATCH 1/7] [ot] python/qemu: ot.util.arg: move ArgError from misc to arg module Now that there is a dedicated ot.util module for ArgumentParser extensions, move the ArgError class to where it better belongs. Signed-off-by: Emmanuel Blot --- python/qemu/ot/km/dpe.py | 2 +- python/qemu/ot/util/arg.py | 8 ++++++++ python/qemu/ot/util/misc.py | 4 ---- scripts/opentitan/autotop.py | 4 ++-- scripts/opentitan/cfggen.py | 3 ++- scripts/opentitan/keymgr-dpe.py | 3 ++- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/python/qemu/ot/km/dpe.py b/python/qemu/ot/km/dpe.py index 652eaf6a0bd0e..2ff153c4433a4 100644 --- a/python/qemu/ot/km/dpe.py +++ b/python/qemu/ot/km/dpe.py @@ -24,7 +24,7 @@ from ..otp import OtpImage, OtpLifecycleExtension, OtpMap from ..rom.image import ROMImage -from ..util.misc import ArgError +from ..util.arg import ArgError # ruff: noqa: E402 _CRYPTO_EXC: Optional[Exception] = None diff --git a/python/qemu/ot/util/arg.py b/python/qemu/ot/util/arg.py index 6be703caa55a8..209b85b62ae01 100644 --- a/python/qemu/ot/util/arg.py +++ b/python/qemu/ot/util/arg.py @@ -12,6 +12,14 @@ from sys import stderr +class ArgError(Exception): + """Argument error. + + ArgError may be used to signal an argument parser error from a call + stack back to the ArgumentParser instance. + """ + + class ArgumentParser(_ArgumentParser): """Report usage error first, before printing out the usage. This enables catching the first line as the main error message. diff --git a/python/qemu/ot/util/misc.py b/python/qemu/ot/util/misc.py index 3aab7d4043b61..eccb3c49abc48 100644 --- a/python/qemu/ot/util/misc.py +++ b/python/qemu/ot/util/misc.py @@ -80,10 +80,6 @@ def xparse(value: Union[None, int, str]) -> Optional['HexInt']: return HexInt(int(value.strip(), value.startswith('0x') and 16 or 10)) -class ArgError(Exception): - """Argument error.""" - - class EasyDict(dict): """Dictionary whose members can be accessed as instance members """ diff --git a/scripts/opentitan/autotop.py b/scripts/opentitan/autotop.py index 561625063e90c..16e51f864d4db 100755 --- a/scripts/opentitan/autotop.py +++ b/scripts/opentitan/autotop.py @@ -24,9 +24,9 @@ sys.path.append(QEMU_PYPATH) # ruff: noqa: E402 +from ot.util.arg import ArgError from ot.util.log import configure_loggers -from ot.util.misc import (ArgError, HexInt, camel_to_snake_uppercase, - classproperty) +from ot.util.misc import HexInt, camel_to_snake_uppercase, classproperty try: _HJSON_ERROR = None diff --git a/scripts/opentitan/cfggen.py b/scripts/opentitan/cfggen.py index b0112af2e8649..ce8b46be9f658 100755 --- a/scripts/opentitan/cfggen.py +++ b/scripts/opentitan/cfggen.py @@ -34,8 +34,9 @@ def hjload(*_, **__): # noqa: E301 from ot.lc_ctrl.const import LcCtrlConstants from ot.otp.const import OtpConstants from ot.otp.secret import OtpSecretConstants +from ot.util.arg import ArgError from ot.util.log import configure_loggers -from ot.util.misc import ArgError, alphanum_key, to_bool +from ot.util.misc import alphanum_key, to_bool OtParamRegex = str diff --git a/scripts/opentitan/keymgr-dpe.py b/scripts/opentitan/keymgr-dpe.py index cfef1fff33af6..8a3af68b4aad8 100755 --- a/scripts/opentitan/keymgr-dpe.py +++ b/scripts/opentitan/keymgr-dpe.py @@ -24,8 +24,9 @@ from ot.km.dpe import KeyManagerDpe from ot.km.engine import KeyManagerDpeEngine from ot.otp.image import OtpImage +from ot.util.arg import ArgError from ot.util.log import configure_loggers -from ot.util.misc import ArgError, HexInt +from ot.util.misc import HexInt def main(): From 201cc1b9d1ae332bc18e50d4d00d730b51d55a9f Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 12 Sep 2025 16:25:27 +0200 Subject: [PATCH 2/7] [ot] python/qemu: ot.otp: add support for zeroizable OTP feature Signed-off-by: Emmanuel Blot --- python/qemu/ot/otp/descriptor.py | 8 ++++ python/qemu/ot/otp/map.py | 21 +++++++++-- python/qemu/ot/otp/partition.py | 64 ++++++++++++++++++++++++++------ 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/python/qemu/ot/otp/descriptor.py b/python/qemu/ot/otp/descriptor.py index d9586e929389b..e113f5d8c560e 100644 --- a/python/qemu/ot/otp/descriptor.py +++ b/python/qemu/ot/otp/descriptor.py @@ -20,6 +20,7 @@ class OtpPartitionDesc: 'size': None, 'offset': None, 'digest_offset': None, + 'zer_offset': None, 'hw_digest': '', 'sw_digest': '', 'secret': '', @@ -27,6 +28,7 @@ class OtpPartitionDesc: 'write_lock': 'wlock', 'read_lock': 'rlock', 'integrity': '', + 'zeroizable': '', 'iskeymgr': '', 'iskeymgr_creator': '', 'iskeymgr_owner': '', @@ -51,6 +53,12 @@ def save(self, hjname: str, scriptname: str, cfp: TextIO) -> None: print(f' [OTP_PART_{part.name}] = {{', file=cfp) print(f' .size = {part.size}u,', file=cfp) print(f' .offset = {part.offset}u,', file=cfp) + if part.zer_offset is not None: + print(f' .zer_offset = {part.zer_offset}u,', + file=cfp) + else: + print(f' .zer_offset = UINT16_MAX,', # noqa: F541 + file=cfp) if part.digest_offset is not None: print(f' .digest_offset = {part.digest_offset}u,', file=cfp) diff --git a/python/qemu/ot/otp/map.py b/python/qemu/ot/otp/map.py index 718a0dd84ad0a..56a2999406ded 100644 --- a/python/qemu/ot/otp/map.py +++ b/python/qemu/ot/otp/map.py @@ -123,6 +123,9 @@ def _generate_partitions(self) -> None: has_digest = any(part.get(f'{k}w_digest') for k in 'sh') if has_digest: items_size += OtpPartition.DIGEST_SIZE + has_zero = part.get('zeroizable', False) + if has_zero: + items_size += OtpPartition.ZER_SIZE if part_size: assert items_size <= part_size else: @@ -136,9 +139,12 @@ def _generate_partitions(self) -> None: part[kmm[0]] = kmm[1] prefix = name.title().replace('_', '') partname = f'{prefix}Part' - newpart = type(partname, (OtpPartition,), + # create an OtpPartition class specialized for the current partition + partcls = type(partname, (OtpPartition,), {'name': name, '__doc__': desc}) - self._partitions.append(newpart(part)) + # instantiate a new partition of this type + partobj = partcls(part) + self._partitions.append(partobj) def _check_keymgr_materials(self, partname: str, items: dict[str, dict]) \ -> Optional[tuple[str, bool]]: @@ -186,12 +192,21 @@ def _compute_locations(self) -> None: for part in self._partitions: part_offset = 0 for part in self._partitions: + zeroizable = getattr(part, 'zeroizable', False) + if zeroizable: + zer_offset = part_offset + part.size - OtpPartition.ZER_SIZE + else: + zer_offset = None if part.sw_digest or part.hw_digest: - digest_offset = part_offset + part.size - 8 + digest_offset = (part_offset + part.size - + OtpPartition.DIGEST_SIZE) + if zeroizable: + digest_offset -= OtpPartition.ZER_SIZE else: digest_offset = None setattr(part, 'offset', part_offset) setattr(part, 'digest_offset', digest_offset) + setattr(part, 'zer_offset', zer_offset) part_offset += part.size assert part_offset == self._otp_size, "Unexpected partition offset" diff --git a/python/qemu/ot/otp/partition.py b/python/qemu/ot/otp/partition.py index 73b5b0a811888..94ed9fd92608b 100644 --- a/python/qemu/ot/otp/partition.py +++ b/python/qemu/ot/otp/partition.py @@ -60,6 +60,8 @@ class OtpPartition: DIGEST_SIZE = 8 # bytes + ZER_SIZE = 8 # bytes + MAX_DATA_WIDTH = 20 def __init__(self, params): @@ -68,6 +70,7 @@ def __init__(self, params): self._log = getLogger('otp.part') self._data = b'' self._digest_bytes: Optional[bytes] = None + self._zer_bytes: Optional[bytes] = None @property def has_digest(self) -> bool: @@ -85,11 +88,18 @@ def is_locked(self) -> bool: return (self.has_digest and self._digest_bytes and self._digest_bytes != bytes(self.DIGEST_SIZE)) + @property + def is_zeroizable(self) -> bool: + """Check if the partition supports zeroization.""" + return getattr(self, 'zeroizable', False) + @property def is_empty(self) -> bool: """Report if the partition is empty.""" if self._digest_bytes and sum(self._digest_bytes): return False + if self._zer_bytes and sum(self._zer_bytes): + return False return sum(self._data) == 0 def __repr__(self) -> str: @@ -100,6 +110,10 @@ def load(self, bfp: BinaryIO) -> None: data = bfp.read(self.size) if len(data) != self.size: raise IOError(f'{self.name} Cannot load {self.size} from stream') + zer_data = None + if self.is_zeroizable: + data, zer_data = data[:-self.ZER_SIZE], data[-self.ZER_SIZE:] + self._zer_bytes = zer_data if self.has_digest: data, digest = data[:-self.DIGEST_SIZE], data[-self.DIGEST_SIZE:] self._digest_bytes = digest @@ -110,6 +124,7 @@ def save(self, bfp: BinaryIO) -> None: pos = bfp.tell() bfp.write(self._data) bfp.write(self._digest_bytes) + bfp.write(self._zer_bytes) size = bfp.tell() - pos if size != self.size: raise RuntimeError(f"Failed to save partition {self.name} content") @@ -148,9 +163,9 @@ def compute_digest(cls, data: bytes, digest_iv: int, digest_constant: int) \ if Present is None: raise RuntimeError('Cannot check digest, Present module not found') block_sz = OtpMap.BLOCK_SIZE - assert block_sz == 8 # should be 64 bits for Present to work + assert block_sz == Present.BLOCK_BIT_SIZE // 8 if len(data) % block_sz != 0: - # this case is valid but not yet impplemented (paddding) + # this case is valid but not yet implemented (paddding) raise RuntimeError('Invalid partition size') block_count = len(data) // block_sz if block_count & 1: @@ -177,9 +192,11 @@ def decode(self, base: Optional[int], decode: bool = True, wide: int = 0, buf = BytesIO(self._data) if ofp: def emit(fmt, *args): - print(fmt % args, file=ofp) + print(f'%-52s %s {fmt}' % args, file=ofp) else: - emit = self._log.info + def emit(fmt, *args): + fmt = f'%-52s %s {fmt}' + self._log.info(fmt, *args) pname = self.name offset = 0 soff = 0 @@ -190,6 +207,8 @@ def emit(fmt, *args): filter_re = r'.*' for itname, itdef in self.items.items(): itsize = itdef['size'] + if not itsize: + self._log.error('Zero sized %s.%s', self.name, itname) itvalue = buf.read(itsize) soff = f'[{f"{base+offset:d}":>5s}]' if base is not None else '' offset += itsize @@ -205,31 +224,42 @@ def emit(fmt, *args): if decode and self._decoder: dval = self._decoder.decode(itname, sval) if dval is not None: - emit('%-48s %s (decoded) %s', name, soff, dval) + emit('(decoded) %s', name, soff, dval) continue ssize = f'{{{itsize}}}' if not sum(itvalue) and wide < 2: if decode: - emit('%-48s %s %5s (empty)', name, soff, ssize) + emit('%5s (empty)', name, soff, ssize) else: - emit('%-48s %s %5s 0...', name, soff, ssize) + emit('%5s 0...', name, soff, ssize) else: if not wide and itsize > self.MAX_DATA_WIDTH: sval = f'{sval[:self.MAX_DATA_WIDTH*2]}...' - emit('%-48s %s %5s %s', name, soff, ssize, sval) + emit('%5s %s', name, soff, ssize, sval) else: ival = int.from_bytes(itvalue, 'little') if decode: if itdef.get('ismubi'): - emit('%-48s %s (decoded) %s', + emit('(decoded) %s', name, soff, str(OtpMap.MUBI8_BOOLEANS.get(ival, ival))) continue if itsize == 4 and ival in OtpMap.HARDENED_BOOLEANS: - emit('%-48s %s (decoded) %s', + emit('(decoded) %s', name, soff, str(OtpMap.HARDENED_BOOLEANS[ival])) continue - emit('%-48s %s %x', name, soff, ival) + emit('%x', name, soff, ival) + offset = (offset + OtpMap.BLOCK_SIZE - 1) & ~(OtpMap.BLOCK_SIZE - 1) + rpos = self.size + dsoff = zsoff = f'[{"":5s}]' + if self.is_zeroizable: + rpos -= self.ZER_SIZE + if base is not None: + zsoff = f'[{f"{base+rpos:d}":>5s}]' + if self.has_digest: + rpos -= self.DIGEST_SIZE + if base is not None: + dsoff = f'[{f"{base+rpos:d}":>5s}]' if self._digest_bytes is not None: if match(filter_re, 'DIGEST', IGNORECASE): if not sum(self._digest_bytes) and decode: @@ -237,7 +267,17 @@ def emit(fmt, *args): else: val = hexlify(self._digest_bytes).decode() ssize = f'{{{len(self._digest_bytes)}}}' - emit('%-48s %s %5s %s', f'{pname}:DIGEST', soff, ssize, val) + emit('%5s %s', f'{pname}:DIGEST', dsoff, ssize, val) + if self._zer_bytes is not None: + if match(filter_re, 'ZER', IGNORECASE): + if not sum(self._zer_bytes) and decode: + val = '(empty)' + else: + val = hexlify(self._zer_bytes).decode() + ssize = f'{{{len(self._zer_bytes)}}}' + emit('%5s %s', f'{pname}:ZER', zsoff, ssize, val) + if offset != rpos: + self._log.warning('%s: offset %d, size %d', self.name, offset, rpos) def empty(self) -> None: """Empty the partition, including its digest if any.""" From 5b3deaec8e04965034a4608b814f2959bf60143c Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 26 Sep 2025 14:57:00 +0200 Subject: [PATCH 3/7] [ot] python/qemu: ot.otp: add support for absorb-able fields Signed-off-by: Emmanuel Blot --- python/qemu/ot/otp/map.py | 6 +++-- python/qemu/ot/otp/partition.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/python/qemu/ot/otp/map.py b/python/qemu/ot/otp/map.py index 56a2999406ded..a4b1e8a1c0d5f 100644 --- a/python/qemu/ot/otp/map.py +++ b/python/qemu/ot/otp/map.py @@ -110,7 +110,8 @@ def _generate_partitions(self) -> None: # assume name & size are always defined for each item item_name = item['name'] del item['name'] - item_size = int(item['size']) + # absorbing items may have no size + item_size = int(item.get('size', 0)) item['size'] = item_size assert item_name not in items items[item_name] = item @@ -187,8 +188,9 @@ def _compute_locations(self) -> None: if extra_blocks: part.size += self.BLOCK_SIZE extra_blocks -= 1 - self._log.info('Partition %s size augmented from %u to %u', + self._log.info('Partition %s size augmented from %u to %u bytes', part.name, psize, part.size) + part.dispatch_absorb() for part in self._partitions: part_offset = 0 for part in self._partitions: diff --git a/python/qemu/ot/otp/partition.py b/python/qemu/ot/otp/partition.py index 94ed9fd92608b..9f382dda9fc5d 100644 --- a/python/qemu/ot/otp/partition.py +++ b/python/qemu/ot/otp/partition.py @@ -93,6 +93,11 @@ def is_zeroizable(self) -> bool: """Check if the partition supports zeroization.""" return getattr(self, 'zeroizable', False) + @property + def is_absorb(self) -> bool: + """Check if the partition can use unassigned storage.""" + return getattr(self, 'absorb', False) + @property def is_empty(self) -> bool: """Report if the partition is empty.""" @@ -279,6 +284,43 @@ def emit(fmt, *args): if offset != rpos: self._log.warning('%s: offset %d, size %d', self.name, offset, rpos) + def dispatch_absorb(self) -> None: + """Request the partition to resize its fields.""" + if not self.is_absorb: + raise RuntimeError('Partition cannot absorb free space') + absorb_fields = {n: v for n, v in self.items.items() + if v.get('absorb', False)} + avail_size = self.size + if self.has_digest: + avail_size -= self.DIGEST_SIZE + if self.is_zeroizable: + avail_size -= self.ZER_SIZE + avail_blocks = avail_size // OtpMap.BLOCK_SIZE + if not absorb_fields: + # a version of OTP where absorb is defined as a partition property + # but not defined for fields. In such a case, it is expected that + # the partition contains a single field. + itemcnt = len(self.items) + if itemcnt != 1: + raise RuntimeError(f'No known absorption method with {itemcnt} ' + f'items in partition {self.name}') + # nothing to do here + return + absorb_count = len(absorb_fields) + blk_per_field = avail_blocks // absorb_count + extra_blocks = avail_blocks % absorb_count + self._log.info("%s: %d bytes (%d blocks) to absorb into %d field%s", + self.name, avail_size, avail_blocks, absorb_count, + 's' if absorb_count > 1 else '') + for itname, itfield in absorb_fields.items(): + fsize = itfield['size'] + itfield['size'] += OtpMap.BLOCK_SIZE * blk_per_field + if extra_blocks: + itfield['size'] += OtpMap.BLOCK_SIZE + extra_blocks -= 1 + self._log.info('%s.%s size augmented from %u to %u bytes', + self.name, itname, fsize, itfield['size']) + def empty(self) -> None: """Empty the partition, including its digest if any.""" self._data = bytes(len(self._data)) From ed04ed03fa9a3eaa46e835acece39a48504b562a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 29 Sep 2025 13:27:31 +0200 Subject: [PATCH 4/7] [ot] scripts/opentitan: otptool: improve code skeleton generation add basic support for baremetal test code generation Signed-off-by: Emmanuel Blot --- docs/opentitan/otptool.md | 15 ++- python/qemu/ot/otp/descriptor.py | 224 +++++++++++++++++++++++++++---- python/qemu/ot/otp/lifecycle.py | 13 +- scripts/opentitan/otptool.py | 41 +++--- 4 files changed, 242 insertions(+), 51 deletions(-) diff --git a/docs/opentitan/otptool.md b/docs/opentitan/otptool.md index a985ce7bbac34..d8c5a6fd247be 100644 --- a/docs/opentitan/otptool.md +++ b/docs/opentitan/otptool.md @@ -13,7 +13,8 @@ usage: otptool.py [-h] [-j HJSON] [-m VMEM] [-l SV] [-o FILE] [-r RAW] [-G PART] [--change PART:FIELD=VALUE] [--empty PARTITION] [--erase PART:FIELD] [--clear-bit CLEAR_BIT] [--set-bit SET_BIT] [--toggle-bit TOGGLE_BIT] - [--write ADDR/HEXBYTES] [--patch-token NAME=VALUE] [-v] [-d] + [--write ADDR/HEXBYTES] [--patch-token NAME=VALUE] + [--out-kind {qemu,bmtest}] [-v] [-d] QEMU OT tool to manage OTP files. @@ -47,7 +48,7 @@ Commands: -D, --digest check the OTP HW partition digest -U, --update update RAW file after ECC recovery or bit changes -g, --generate {LCVAL,LCTPL,PARTS,REGS} - generate C code, see doc for options + generate code, see doc for options -F, --fix-ecc rebuild ECC -G, --fix-digest PART rebuild HW digest @@ -65,6 +66,9 @@ Commands: write bytes at specified location --patch-token NAME=VALUE change a LC hashed token, using Rust file + --out-kind {qemu,bmtest} + select output format for code generation (default: + qemu) Extras: -v, --verbose increase verbosity @@ -129,9 +133,9 @@ Fuse RAW images only use the v1 type. * `-G` can be used to (re)build the HW digest of a partition after altering one or more of its fields, see `--change` option. -* `-g` can be used to generate C code for QEMU, from OTP and LifeCycle known definitions. See the +* `-g` can be used to generate skeleton files, from OTP and LifeCycle known definitions. See the [Generation](#generation) section for details. See option `-o` to specify the path to the file to - generate + generate. See also the `--out-kind` option for output formats. * `-i` specify the initialization vector for the Present scrambler used for partition digests. This value is "usually" found within the `hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv` OT file, @@ -206,6 +210,9 @@ Fuse RAW images only use the v1 type. * `--no-version` disable OTP image version reporting when `-s` is used. +* `--out-kind` define the output format for code generation for the `-g` option. Note that only a + subset of the generation types are available in some output kinds. + * `--patch-token` patch a Life Cycle hashed token. This feature is primary aimed at testing the Life Cycle controller. With this option, the partition to update is automatically found using the token `NAME`. If the partition containing the token to update is already locked, its digest is diff --git a/python/qemu/ot/otp/descriptor.py b/python/qemu/ot/otp/descriptor.py index e113f5d8c560e..547c108500bfc 100644 --- a/python/qemu/ot/otp/descriptor.py +++ b/python/qemu/ot/otp/descriptor.py @@ -7,12 +7,37 @@ """ from logging import getLogger -from typing import TYPE_CHECKING, TextIO +from typing import NamedTuple, TYPE_CHECKING, TextIO + +from ..util.misc import redent +from .partition import OtpPartition if TYPE_CHECKING: from .map import OtpMap +class OtpSlotDescriptor(NamedTuple): + """A location descriptor in OTP, either a whole partition or an item. + + It has no other purpose than storing intermediate info as a container. + """ + + name: str + """Name of the slot.""" + + offset: str + """Offset in bytes.""" + + size: str + """Size in bytes.""" + + gen: bool = False + """Whether this slot is generated by the script or defined.""" + + part: bool = False + """Whether the slot defines the whole partition or a single item.""" + + class OtpPartitionDesc: """OTP Partition descriptor generator.""" @@ -39,9 +64,18 @@ def __init__(self, otpmap: 'OtpMap'): self._log = getLogger('otp.partdesc') self._otpmap = otpmap - def save(self, hjname: str, scriptname: str, cfp: TextIO) -> None: - """Generate a C file with a static description for the partitions.""" + def save(self, kind: str, hjname: str, scriptname: str, cfp: TextIO) \ + -> None: + """Generate a source file with a static description for the partitions. + + :param kind: kind of generation output + :param hjname: the name of the input HJSON configuration file + :param scriptname: the name of the script that generates this output + :param cfp: the output text stream + """ # pylint: disable=f-string-without-interpolation + if kind != 'qemu': + raise NotImplementedError(f'No support for {kind}') attrs = {n: getattr(self, f'_convert_to_{k}') if k else lambda x: x for n, k in self.ATTRS.items() if k is not None} print(f'/* Generated from {hjname} with {scriptname} */', file=cfp) @@ -126,48 +160,190 @@ def __init__(self, otpmap: 'OtpMap'): self._log = getLogger('otp.reg') self._otpmap = otpmap - def save(self, hjname: str, scriptname: str, cfp: TextIO) -> None: - """Generate a C file with register definition for the partitions.""" - reg_offsets = [] - reg_sizes = [] - part_names = [] + def save(self, kind: str, hjname: str, scriptname: str, cfp: TextIO) \ + -> None: + """Generate a source file with register definition for the partitions. + + :param kind: kind of generation output + :param hjname: the name of the input HJSON configuration file + :param scriptname: the name of the script that generates this output + :param cfp: the output text stream + """ + try: + save = getattr(self, f'_save_{kind.lower()}') + except AttributeError as exc: + raise NotImplementedError(f'No support for {kind}') from exc + slots: list[OtpSlotDescriptor] = [] for part in self._otpmap.enumerate_partitions(): - part_names.append(f'OTP_PART_{part.name}') offset = part.offset - reg_sizes.append((f'{part.name}_SIZE', part.size)) + slots.append(OtpSlotDescriptor(part.name, offset, part.size, True, + True)) for itname, itdict in part.items.items(): size = itdict['size'] if not itname.startswith(f'{part.name}_'): name = f'{part.name}_{itname}'.upper() else: name = itname - reg_offsets.append((name, offset)) - reg_sizes.append((f'{name}_SIZE', size)) + slots.append(OtpSlotDescriptor(name, offset, size)) offset += size + zeroizable = getattr(part, 'zeroizable', False) + digest = any(getattr(part, f'{k}w_digest', False) for k in 'sh') + if digest: + offset = part.offset + part.size - OtpPartition.DIGEST_SIZE + if zeroizable: + offset -= OtpPartition.ZER_SIZE + slots.append(OtpSlotDescriptor(f'{part.name}_DIGEST', offset, + OtpPartition.DIGEST_SIZE, True)) + if zeroizable: + offset = part.offset + part.size - OtpPartition.DIGEST_SIZE + slots.append(OtpSlotDescriptor(f'{part.name}_ZER', offset, + OtpPartition.ZER_SIZE, True)) + + save(hjname, scriptname, cfp, slots) + + def _save_qemu(self, hjname: str, scriptname: str, cfp: TextIO, + slots: list[OtpSlotDescriptor]) -> None: print(f'/* Generated from {hjname} with {scriptname} */') print(file=cfp) - print('/* clang-format off */', file=cfp) - for reg, off in reg_offsets: - print(f'REG32({reg}, {off}u)', file=cfp) + for slot in slots: + if slot.part: + continue + print(f'REG32({slot.name}, {slot.offset}u)', file=cfp) print(file=cfp) - regwidth = max(len(r[0]) for r in reg_sizes) - for reg, size in reg_sizes: - print(f'#define {reg:{regwidth}s} {size}u', file=cfp) + + regwidth = max(len(s.name) for s in slots) + regwidth += len('_SIZE') + for slot in slots: + if slot.gen and not slot.part: + continue + name = f'{slot.name}_SIZE' + print(f'#define {name:{regwidth}s} {slot.size}u', file=cfp) print(file=cfp) + + part_names = [slot.name for slot in slots if slot.part] pcount = len(part_names) + part_names = [f'OTP_PART_{pn}' for pn in part_names] part_names.extend(( - '_OTP_PART_COUNT', - 'OTP_ENTRY_DAI = _OTP_PART_COUNT', - 'OTP_ENTRY_KDI', - '_OTP_ENTRY_COUNT')) + 'OTP_PART_COUNT', + 'OTP_ENTRY_DAI = OTP_PART_COUNT, ' + '/* Fake partitions for error (...) */', + 'OTP_ENTRY_KDI, /* Key derivation issue, not really OTP */', + 'OTP_ENTRY_COUNT')) print('typedef enum {', file=cfp) for pname in part_names: - print(f' {pname},', file=cfp) + print(f' {pname}{"," if "," not in pname else ""}', file=cfp) print('} OtOTPPartitionType;', file=cfp) print(file=cfp) + print('static const char *PART_NAMES[] = {', file=cfp) + print(' /* clang-format off */', file=cfp) for pname in part_names[:pcount]: print(f' OTP_NAME_ENTRY({pname}),', file=cfp) + print(' /* fake partitions */', file=cfp) + print(' OTP_NAME_ENTRY(OTP_ENTRY_DAI),', file=cfp) + print(' OTP_NAME_ENTRY(OTP_ENTRY_KDI),', file=cfp) + print(' /* clang-format on */', file=cfp) print('};', file=cfp) - print('/* clang-format on */', file=cfp) + print(file=cfp) + + cases: list[str] = [] + for slot in slots: + if slot.part: + continue + if slot.gen: + cases.append(f'CASE_WIDE({slot.name});') + elif slot.size > 4: + cases.append(f'CASE_RANGE({slot.name});') + elif slot.size == 1: + cases.append(f'CASE_BYTE({slot.name});') + elif slot.size < 4: + cases.append(f'CASE_SUB({slot.name}, {slot.size}u);') + else: + cases.append(f'CASE_REG({slot.name});') + + code = ''' + static const char *ot_otp_swcfg_reg_name(unsigned swreg) + { + #define CASE_BYTE(_reg_) \\ + case A_##_reg_: \\ + return stringify(_reg_) + #define CASE_SUB(_reg_, _sz_) \\ + case A_##_reg_...(A_##_reg_ + (_sz_)): \\ + return stringify(_reg_) + #define CASE_REG(_reg_) \\ + case A_##_reg_...(A_##_reg_ + 3u): \\ + return stringify(_reg_) + #define CASE_WIDE(_reg_) \\ + case A_##_reg_...(A_##_reg_ + 7u): \\ + return stringify(_reg_) + #define CASE_RANGE(_reg_) \\ + case A_##_reg_...(A_##_reg_ + (_reg_##_SIZE) - 1u): \\ + return stringify(_reg_) + + switch (swreg) { + _CASES_ + default: + return ""; + } + + #undef CASE_BYTE + #undef CASE_SUB + #undef CASE_REG + #undef CASE_RANGE + #undef CASE_DIGEST + } + ''' + code = redent(code) + code = code.replace('_CASES_', '\n '.join(cases)) + print(redent(code), '', file=cfp) + + def _save_bmtest(self, hjname: str, scriptname: str, cfp: TextIO, + slots: list[OtpSlotDescriptor]) -> None: + # pylint: disable=unused-argument + print(f'// Generated from {hjname} with {scriptname}', file=cfp) + print(file=cfp) + rec_p2 = 1 << (len(slots) - 1).bit_length() + print(f'#![recursion_limit = "{rec_p2}"]', file=cfp) + print(file=cfp) + print('#[derive(Clone, Copy, Debug, PartialEq)]', file=cfp) + print('pub enum Partition {', file=cfp) + part_names = [slot.name for slot in slots if slot.part] + for pname in part_names: + pname = pname.title().replace('_', '') + print(f' {pname},', file=cfp) + print('}', file=cfp) + print(file=cfp) + print('register_structs! {', file=cfp) + print(' pub OtpSwCfgRegs {', file=cfp) + slot = OtpSlotDescriptor('', 0, 0) # default slot if none is defined + end = 0 + rsv = 0 + for slot in slots: + if slot.part: + continue + if slot.offset > end: + missing = slot.offset - end + count = missing // 4 + if count > 1: + print(f' (0x{end:04x} => ' + f'_reserved{rsv}: [ReadOnly; {count}]),', + file=cfp) + else: + width = missing * 8 + print(f' (0x{end:04x} => ' + f'_reserved{rsv}: ReadOnly),', file=cfp) + rsv += 1 + if slot.size <= 4: + width = slot.size * 8 + print(f' (0x{slot.offset:04x} => ' + f'{slot.name.lower()}: ReadOnly),', file=cfp) + else: + count = slot.size // 4 + print(f' (0x{slot.offset:04x} => ' + f'{slot.name.lower()}: [ReadOnly; {count}]),', + file=cfp) + end = slot.offset + slot.size + print(f' (0x{slot.offset+slot.size:04x} => @END),', file=cfp) + print(' }', file=cfp) + print('}', file=cfp) print(file=cfp) diff --git a/python/qemu/ot/otp/lifecycle.py b/python/qemu/ot/otp/lifecycle.py index c9ccba25c376d..9e32d01fc6dba 100644 --- a/python/qemu/ot/otp/lifecycle.py +++ b/python/qemu/ot/otp/lifecycle.py @@ -9,7 +9,6 @@ from binascii import unhexlify from io import StringIO from logging import getLogger -from os.path import basename from textwrap import fill from typing import TextIO import re @@ -96,14 +95,16 @@ def load(self, svp: TextIO): seq = ''.join((f'{x:04x}'for x in map(codes.get, seq))) self._tables[mkind][seq] = conv(ref) - def save(self, cfp: TextIO, data_mode: bool) -> None: - """Save OTP life cycle definitions as a C file. + def save(self, kind: str, cfp: TextIO, data_mode: bool) -> None: + """Save OTP life cycle definitions as a source file. + :param kind: output format :param cfp: output text stream :param data_mode: whether to output data or template """ - print(f'/* Section auto-generated with {basename(__file__)} ' - f'script */', file=cfp) + if kind.lower() != 'qemu': + raise NotImplementedError(f'No support for {kind}') + print(f'/* Section auto-generated with {__name__} module */', file=cfp) if data_mode: self._save_data(cfp) else: @@ -143,7 +144,7 @@ def _save_data(self, cfp: TextIO) -> None: def _save_template(self, cfp: TextIO) -> None: print('/* clang-format off */', file=cfp) - states = self._sequences.get('st') or {} + states = self._sequences.get('lcst') or {} print('static const uint8_t', file=cfp) print('LC_STATES_TPL[LC_STATE_VALID_COUNT][LC_STATE_SLOT_COUNT] = {', file=cfp) diff --git a/scripts/opentitan/otptool.py b/scripts/opentitan/otptool.py index 79bfe20cbdc31..b8b07ad6a770d 100755 --- a/scripts/opentitan/otptool.py +++ b/scripts/opentitan/otptool.py @@ -54,6 +54,7 @@ def main(): """Main routine""" debug = True genfmts = 'LCVAL LCTPL PARTS REGS'.split() + outkinds = ('qemu', 'bmtest') try: desc = sys.modules[__name__].__doc__.split('.', 1)[0].strip() argparser = ArgumentParser(description=f'{desc}.') @@ -110,7 +111,7 @@ def main(): help='update RAW file after ECC recovery or bit ' 'changes') commands.add_argument('-g', '--generate', choices=genfmts, - help='generate C code, see doc for options') + help='generate code, see doc for options') commands.add_argument('-F', '--fix-ecc', action='store_true', help='rebuild ECC') commands.add_argument('-G', '--fix-digest', action='append', @@ -138,6 +139,10 @@ def main(): commands.add_argument('--patch-token', action='append', metavar='NAME=VALUE', default=[], help='change a LC hashed token, using Rust file') + commands.add_argument('--out-kind', choices=outkinds, + default=outkinds[0], + help=f'select output format for code generation' + f' (default: {outkinds[0]})') extra = argparser.add_argument_group(title='Extras') extra.add_argument('-v', '--verbose', action='count', help='increase verbosity') @@ -213,22 +218,24 @@ def main(): output = sys.stdout if not args.output else args.output - if not args.generate: - pass - elif args.generate == 'PARTS': - partdesc = OtpPartitionDesc(otpmap) - partdesc.save(basename(args.otp_map.name), basename(sys.argv[0]), - output) - elif args.generate == 'REGS': - regdef = OtpRegisterDef(otpmap) - regdef.save(basename(args.otp_map.name), basename(sys.argv[0]), - output) - elif args.generate == 'LCVAL': - lcext.save(output, True) - elif args.generate == 'LCTPL': - lcext.save(output, False) - else: - argparser.error(f'Unsupported generation: {args.generate}') + try: + if not args.generate: + pass + elif args.generate == 'PARTS': + partdesc = OtpPartitionDesc(otpmap) + partdesc.save(args.out_kind, basename(args.otp_map.name), + basename(sys.argv[0]), output) + elif args.generate == 'REGS': + regdef = OtpRegisterDef(otpmap) + regdef.save(args.out_kind, basename(args.otp_map.name), + basename(sys.argv[0]), output) + elif args.generate == 'LCVAL': + lcext.save(args.out_kind, output, True) + elif args.generate == 'LCTPL': + lcext.save(args.out_kind, output, False) + except NotImplementedError: + argparser.error(f'Cannot generate {args.generate} in ' + f'{args.out_kind}') if args.vmem: otp.load_vmem(args.vmem, args.kind) From 83e1ab4a98ff72d9d6d22e53be4620bc5c479142 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 29 Sep 2025 12:00:09 +0200 Subject: [PATCH 5/7] [ot] python/qemu: ot.top: add a new module to manage OT tops. For now, only enumerate supported top names. Signed-off-by: Emmanuel Blot --- python/qemu/ot/top/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 python/qemu/ot/top/__init__.py diff --git a/python/qemu/ot/top/__init__.py b/python/qemu/ot/top/__init__.py new file mode 100644 index 0000000000000..9dfa907f46a27 --- /dev/null +++ b/python/qemu/ot/top/__init__.py @@ -0,0 +1,27 @@ +# Copyright (c) 2025 2025 lowRISC contributors. +# SPDX-License-Identifier: Apache2 + +"""OpenTitan Tops.""" + +from ot.util.misc import classproperty +from typing import Optional + + +class OpenTitanTop: + """OpenTitan supported tops.""" + + SHORT_MAP = { + 'darjeeling': 'dj', + 'earlgrey': 'eg', + } + + @classproperty + def names(cls) -> list[str]: + """Supported top names.""" + # pylint: disable=no-self-argument + return list(cls.SHORT_MAP) + + @classmethod + def short_name(cls, topname: str) -> Optional[str]: + """Get the short form of a top.""" + return cls.SHORT_MAP.get(topname.lower()) From e6e1f5c98edd424cb896eb970fd2524fd1632448 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 29 Sep 2025 12:00:37 +0200 Subject: [PATCH 6/7] [ot] scripts/opentitan: cfggen.py: use the new ot.top module Signed-off-by: Emmanuel Blot --- scripts/opentitan/cfggen.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/scripts/opentitan/cfggen.py b/scripts/opentitan/cfggen.py index ce8b46be9f658..44486d8850395 100755 --- a/scripts/opentitan/cfggen.py +++ b/scripts/opentitan/cfggen.py @@ -34,6 +34,7 @@ def hjload(*_, **__): # noqa: E301 from ot.lc_ctrl.const import LcCtrlConstants from ot.otp.const import OtpConstants from ot.otp.secret import OtpSecretConstants +from ot.top import OpenTitanTop from ot.util.arg import ArgError from ot.util.log import configure_loggers from ot.util.misc import alphanum_key, to_bool @@ -510,10 +511,6 @@ def _generate_pwrmgr(self, cfg: ConfigParser, def main(): """Main routine""" debug = True - top_map = { - 'darjeeling': 'dj', - 'earlgrey': 'eg', - } actions = ['config', 'clock'] try: desc = sys.modules[__name__].__doc__.split('.', 1)[0].strip() @@ -521,7 +518,7 @@ def main(): files = argparser.add_argument_group(title='Files') files.add_argument('opentitan', nargs='?', metavar='OTDIR', help='OpenTitan root directory') - files.add_argument('-T', '--top', choices=top_map.keys(), + files.add_argument('-T', '--top', choices=OpenTitanTop.names, help='OpenTitan top name') files.add_argument('-o', '--out', metavar='CFG', help='Filename of the config file to generate') @@ -576,7 +573,7 @@ def main(): argparser.error('Top name is required if no top file is ' 'specified') top = f'top_{args.top}' - topvar = top_map[args.top] + topvar = OpenTitanTop.short_name(args.top) topcfg = joinpath(ot_dir, f'hw/{top}/data/autogen/{top}.gen.hjson') if not isfile(topcfg): argparser.error(f"No such file '{topcfg}'") @@ -589,9 +586,9 @@ def main(): ltop = cfg.top_name if not ltop: argparser.error('Unknown top name') - log.info("Top: '%s'", cfg.top_name) + log.info("Top: '%s'", ltop) ltop = ltop.lower() - topvar = {k.lower(): v for k, v in top_map.items()}.get(ltop) + topvar = OpenTitanTop.short_name(cfg.top_name) if not topvar: argparser.error(f'Unsupported top name: {cfg.top_name}') top = f'top_{ltop}' From 24b234c383a2a7e5badbd70be25d19961035a565 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 29 Sep 2025 13:04:09 +0200 Subject: [PATCH 7/7] [ot] scripts/opentitan: otptool.py: add optional top name for code generation Signed-off-by: Emmanuel Blot --- docs/opentitan/otptool.md | 6 ++++- python/qemu/ot/otp/descriptor.py | 27 +++++++++++++-------- scripts/opentitan/otptool.py | 40 +++++++++++++++++++++++++++++--- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/docs/opentitan/otptool.md b/docs/opentitan/otptool.md index d8c5a6fd247be..002c1a35faad1 100644 --- a/docs/opentitan/otptool.md +++ b/docs/opentitan/otptool.md @@ -14,7 +14,7 @@ usage: otptool.py [-h] [-j HJSON] [-m VMEM] [-l SV] [-o FILE] [-r RAW] [--erase PART:FIELD] [--clear-bit CLEAR_BIT] [--set-bit SET_BIT] [--toggle-bit TOGGLE_BIT] [--write ADDR/HEXBYTES] [--patch-token NAME=VALUE] - [--out-kind {qemu,bmtest}] [-v] [-d] + [--out-kind {qemu,bmtest}] [--top-name TOP_NAME] [-v] [-d] QEMU OT tool to manage OTP files. @@ -69,6 +69,7 @@ Commands: --out-kind {qemu,bmtest} select output format for code generation (default: qemu) + --top-name TOP_NAME optional top name for code generation (default: auto) Extras: -v, --verbose increase verbosity @@ -230,6 +231,9 @@ Fuse RAW images only use the v1 type. is only intended to corrupt the OTP content so that HW & SW behavior may be exercised should such a condition exists. See [Bit position syntax](#bit-syntax) for how to specify a bit. +* `--top-name` defines the OpenTitan top name for some code generation feature. It overrides any + top name that may be automatically retrieved from the configuration file hierarchy. + * `--write` overrides any data (not ECC). If can be combined with `--fix-ecc` to automatically rebuild the ECC of the data slot that have been altered. This option may be repeated. The argument should comply with the `offset/hexdata` syntax, where the _offset_ part is defined in the diff --git a/python/qemu/ot/otp/descriptor.py b/python/qemu/ot/otp/descriptor.py index 547c108500bfc..391e6af67b263 100644 --- a/python/qemu/ot/otp/descriptor.py +++ b/python/qemu/ot/otp/descriptor.py @@ -7,9 +7,10 @@ """ from logging import getLogger -from typing import NamedTuple, TYPE_CHECKING, TextIO +from typing import NamedTuple, Optional, TYPE_CHECKING, TextIO -from ..util.misc import redent +from ot.top import OpenTitanTop +from ot.util.misc import redent from .partition import OtpPartition if TYPE_CHECKING: @@ -160,11 +161,12 @@ def __init__(self, otpmap: 'OtpMap'): self._log = getLogger('otp.reg') self._otpmap = otpmap - def save(self, kind: str, hjname: str, scriptname: str, cfp: TextIO) \ - -> None: + def save(self, kind: str, topname: Optional[str], hjname: str, + scriptname: str, cfp: TextIO) -> None: """Generate a source file with register definition for the partitions. :param kind: kind of generation output + :param topname: the name of the OpenTitan top :param hjname: the name of the input HJSON configuration file :param scriptname: the name of the script that generates this output :param cfp: the output text stream @@ -199,11 +201,11 @@ def save(self, kind: str, hjname: str, scriptname: str, cfp: TextIO) \ slots.append(OtpSlotDescriptor(f'{part.name}_ZER', offset, OtpPartition.ZER_SIZE, True)) - save(hjname, scriptname, cfp, slots) + save(topname, hjname, scriptname, cfp, slots) - def _save_qemu(self, hjname: str, scriptname: str, cfp: TextIO, - slots: list[OtpSlotDescriptor]) -> None: - print(f'/* Generated from {hjname} with {scriptname} */') + def _save_qemu(self, topname: Optional[str], hjname: str, scriptname: str, + cfp: TextIO, slots: list[OtpSlotDescriptor]) -> None: + print(f'/* Generated from {hjname} with {scriptname} */', file=cfp) print(file=cfp) for slot in slots: if slot.part: @@ -293,12 +295,17 @@ def _save_qemu(self, hjname: str, scriptname: str, cfp: TextIO, #undef CASE_DIGEST } ''' + if topname: + tname = OpenTitanTop.short_name(topname) + if tname: + code = code.replace('ot_otp_swcfg_reg_name', + f'ot_otp_{tname}_swcfg_reg_name') code = redent(code) code = code.replace('_CASES_', '\n '.join(cases)) print(redent(code), '', file=cfp) - def _save_bmtest(self, hjname: str, scriptname: str, cfp: TextIO, - slots: list[OtpSlotDescriptor]) -> None: + def _save_bmtest(self, topname: Optional[str], hjname: str, scriptname: str, + cfp: TextIO, slots: list[OtpSlotDescriptor]) -> None: # pylint: disable=unused-argument print(f'// Generated from {hjname} with {scriptname}', file=cfp) print(file=cfp) diff --git a/scripts/opentitan/otptool.py b/scripts/opentitan/otptool.py index b8b07ad6a770d..ddc7b173bba92 100755 --- a/scripts/opentitan/otptool.py +++ b/scripts/opentitan/otptool.py @@ -10,10 +10,12 @@ from argparse import ArgumentParser, FileType from binascii import unhexlify -from os.path import basename, dirname, join as joinpath, normpath +from io import IOBase +from os.path import (basename, dirname, isfile, join as joinpath, normpath, + split as splitpath) from re import match as re_match from traceback import format_exception -from typing import Optional +from typing import Optional, Union import sys QEMU_PYPATH = joinpath(dirname(dirname(dirname(normpath(__file__)))), @@ -28,6 +30,7 @@ _EXC = exc from ot.otp import (OtpImage, OtpLifecycleExtension, OtpMap, OtpPartition, OtpPartitionDesc, OtpRegisterDef) +from ot.top import OpenTitanTop from ot.util.log import configure_loggers from ot.util.misc import HexInt, to_bool @@ -50,6 +53,31 @@ def parse_lc_token(tkdesc: str) -> tuple[str, 'LifeCycleTokenPair']: return token_name, tkeng.build_from_text(tktext) +def get_top_name(*filepaths: list[Union[str, IOBase]]) -> Optional[str]: + """Try to retrieve the top name from a list of configuration files. + + :param filepaths: list of file path or file objects + :return: the name of the identified top, if found + """ + for filepath in filepaths: + if not isinstance(filepath, str): + filepath = getattr(filepath, 'name', None) + if not filepath: + continue + if not isfile(filepath): + continue + fdir = dirname(filepath) + top_names = set(OpenTitanTop.names) + while fdir: + fdir, tail = splitpath(fdir) + top_prefix = 'top_' + if tail.startswith(top_prefix): + candidate = tail.removeprefix(top_prefix) + if candidate in top_names: + return candidate + return None + + def main(): """Main routine""" debug = True @@ -143,6 +171,9 @@ def main(): default=outkinds[0], help=f'select output format for code generation' f' (default: {outkinds[0]})') + commands.add_argument('--top-name', + help='optional top name for code generation ' + '(default: auto)') extra = argparser.add_argument_group(title='Extras') extra.add_argument('-v', '--verbose', action='count', help='increase verbosity') @@ -226,8 +257,11 @@ def main(): partdesc.save(args.out_kind, basename(args.otp_map.name), basename(sys.argv[0]), output) elif args.generate == 'REGS': + topname = args.top_name + if not topname: + topname = get_top_name(args.otp_map) regdef = OtpRegisterDef(otpmap) - regdef.save(args.out_kind, basename(args.otp_map.name), + regdef.save(args.out_kind, topname, basename(args.otp_map.name), basename(sys.argv[0]), output) elif args.generate == 'LCVAL': lcext.save(args.out_kind, output, True)