Skip to content

Commit

Permalink
[ot] scripts/opentitan: cfggen.py: new tool to generate QEMU OT confi…
Browse files Browse the repository at this point in the history
…g file

This tool may be used to parse an OpenTitan repository and generate a QEMU
configuration file that can be used to initialize sensitive data such as
the keys, nonce, tokens, etc.

Signed-off-by: Emmanuel Blot <[email protected]>
  • Loading branch information
rivos-eblot authored and loiclefort committed Sep 16, 2024
1 parent ae1a292 commit efb3e3d
Show file tree
Hide file tree
Showing 12 changed files with 602 additions and 108 deletions.
76 changes: 76 additions & 0 deletions docs/opentitan/cfggen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# `cfggen.py`

`cfggen.py` is a helper tool that can generate a QEMU OT configuration file,
for use with QEMU's `-readconfig` option, populated with sensitive data for
the ROM controller(s), the OTP controller, the Life Cycle controller, etc.

It heurastically parses configuration and generated RTL files to extract from
them the required keys, seeds, nonces and other tokens that are not stored in
the QEMU binary.

## Usage

````text
usage: cfggen.py [-h] [-o CFG] [-T TOP] [-c SV] [-l SV] [-t HJSON] [-s SOCID]
[-C COUNT] [-v] [-d]
TOPDIR
OpenTitan QEMU configuration file generator.
options:
-h, --help show this help message and exit
Files:
TOPDIR OpenTitan top directory
-o CFG, --out CFG Filename of the config file to generate
-T TOP, --top TOP OpenTitan Top name (default: darjeeling)
-c SV, --otpconst SV OTP Constant SV file (default: auto)
-l SV, --lifecycle SV
LifeCycle SV file (default: auto)
-t HJSON, --topcfg HJSON
OpenTitan top HJSON config file (default: auto)
Modifiers:
-s SOCID, --socid SOCID
SoC identifier, if any
-C COUNT, --count COUNT
SoC count (default: 1)
Extras:
-v, --verbose increase verbosity
-d, --debug enable debug mode
````


### Arguments

`TOPDIR` is a required positional argument which should point to the top-level directory of the
OpenTitan repository to analyze. It is used to generate the path towards the required files to
parse, each of which can be overidden with options `-c`, `-l` and `-t`.

* `-C` specify how many SoCs are used on the platform

* `-c` alternative path to the `otp_ctrl_part_pkg.sv` file

* `-d` only useful to debug the script, reports any Python traceback to the standard error stream.

* `-l` alternative path to the `lc_ctrl_state_pkg.sv.sv` file

* `-o` the filename of the configuration file to generate. It not specified, the generated content
is printed out to the standard output.

* `-s` specify a SoC identifier for OT platforms with mulitple SoCs

* `-T` specify the OpenTitan _top_ name, such as `Darjeeling`, `EarlGrey`, ... This option is
case-insensitive.

* `-t` alternative path to the `top_<top>.gen.hjson` file

* `-v` can be repeated to increase verbosity of the script, mostly for debug purpose.


### Examples

````sh
./scripts/opentitan/cfggen.py ../opentitan-integrated -o opentitan.cfg
````
5 changes: 5 additions & 0 deletions docs/opentitan/otcfg.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ an_integer = 0x1234
a_boolean = true
```

## Generation

It is possible to delegate the generation of an OpenTitan configuration file to the [`cfggen.py`](cfggen.md)
script, using an existing OpenTitan repository.

## Configurable constants

Constants can usually be retrieved from the OpenTitan autogenerated "top" HSJON file, from the
Expand Down
2 changes: 2 additions & 0 deletions docs/opentitan/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ directory to help with these tasks.

## Companion file management

* [`cfggen.py`](cfggen.md) can be used to generate an OpenTitan [configuration file](otcfg.md) from
an existing OpenTitan repository.
* [`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
Expand Down
271 changes: 271 additions & 0 deletions scripts/opentitan/cfggen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
#!/usr/bin/env python3

# Copyright (c) 2024 Rivos, Inc.
# SPDX-License-Identifier: Apache2

"""OpenTitan QEMU configuration file generator.
:author: Emmanuel Blot <[email protected]>
"""

from argparse import ArgumentParser
from configparser import ConfigParser
from logging import getLogger
from os.path import isdir, isfile, join as joinpath, normpath
from re import match, search
from sys import exit as sysexit, modules, stderr
from traceback import format_exc
from typing import Optional

try:
_HJSON_ERROR = None
from hjson import load as hjload
except ImportError as hjson_exc:
_HJSON_ERROR = str(hjson_exc)

from ot.util.log import configure_loggers
from ot.util.misc import camel_to_snake_case
from ot.otp.const import OtpConstants
from ot.otp.lifecycle import OtpLifecycle


OtParamRegex = str
"""Definition of a parameter to seek and how to shorten it."""


class OtConfiguration:
"""QEMU configuration file generator."""

def __init__(self):
self._log = getLogger('cfggen.cfg')
self._lc_states: tuple[str, str] = ('', '')
self._lc_transitions: tuple[str, str] = ('', '')
self._roms: dict[Optional[int], dict[str, str]] = {}
self._otp: dict[str, str] = {}
self._lc: dict[str, str] = {}

def load_top_config(self, toppath: str) -> None:
"""Load data from HJSON top configuration file."""
with open(toppath, 'rt') as tfp:
cfg = hjload(tfp)
for module in cfg.get('module') or []:
modtype = module.get('type')
if modtype == 'rom_ctrl':
self._load_top_values(module, self._roms, True,
r'RndCnstScr(.*)')
continue
if modtype == 'otp_ctrl':
self._load_top_values(module, self._otp, False,
r'RndCnst(.*)Init')
continue

def load_lifecycle(self, lcpath: str) -> None:
"""Load LifeCycle data from RTL file."""
lcext = OtpLifecycle()
with open(lcpath, 'rt') as lfp:
lcext.load(lfp)
states = lcext.get_configuration('LC_STATE')
if not states:
raise ValueError('Cannot obtain LifeCycle states')
for raw in {s for s in states if int(s, 16) == 0}:
del states[raw]
ostates = list(states)
self._lc_states = ostates[0], ostates[-1]
self._log.info("States first: '%s', last '%s'",
states[self._lc_states[0]], states[self._lc_states[1]])
trans = lcext.get_configuration('LC_TRANSITION_CNT')
if not trans:
raise ValueError('Cannot obtain LifeCycle transitions')
for raw in {s for s in trans if int(s, 16) == 0}:
del trans[raw]
otrans = list(trans)
self._lc_transitions = otrans[0], otrans[-1]
self._log.info('Transitions first : %d, last %d',
int(trans[self._lc_transitions[0]]),
int(trans[self._lc_transitions[1]]))
self._lc.update(lcext.get_tokens(False, False))

def load_otp_constants(self, otppath: str) -> None:
"""Load OTP data from RTL file."""
otpconst = OtpConstants()
with open(otppath, 'rt') as cfp:
otpconst.load(cfp)
self._otp.update(otpconst.get_digest_pair('cnsty_digest', 'digest'))
self._otp.update(otpconst.get_digest_pair('sram_data_key', 'sram'))

def save(self, socid: Optional[str] = None, count: Optional[int] = 1,
outpath: Optional[str] = None) \
-> None:
"""Save QEMU configuration file using a INI-like file format,
compatible with the `-readconfig` option of QEMU.
"""
cfg = ConfigParser()
self._generate_roms(cfg, socid, count)
self._generate_otp(cfg, socid)
self._generate_life_cycle(cfg, socid)
if outpath:
with open(outpath, 'wt') as ofp:
cfg.write(ofp)
else:
cfg.write(stderr)

@classmethod
def add_pair(cls, data: dict[str, str], kname: str, value: str) -> None:
"""Helper to create key, value pair entries."""
data[f' {kname}'] = f'"{value}"'

def _load_top_values(self, module: dict, odict: dict, multi: bool,
*regexes: list[OtParamRegex]) -> None:
modname = module.get('name')
if not modname:
return
for params in module.get('param_list', []):
if not isinstance(params, dict):
continue
for regex in regexes: # TODO: camelcase to lower snake case
pmo = match(regex, params['name'])
if not pmo:
continue
value = params.get('default')
if not value:
continue
if value.startswith('0x'):
value = value[2:]
kname = camel_to_snake_case(pmo.group(1))
if multi:
imo = search(r'(\d+)$', modname)
idx = int(imo.group(1)) if imo else 'None'
if idx not in odict:
odict[idx] = {}
odict[idx][kname] = value
else:
odict[kname] = value

def _generate_roms(self, cfg: ConfigParser, socid: Optional[str] = None,
count: int = 1) -> None:
for cnt in range(count):
for rom, data in self._roms.items():
nameargs = ['ot-rom_ctrl']
if socid:
if count > 1:
nameargs.append(f'{socid}{cnt}')
else:
nameargs.append(socid)
if rom is not None:
nameargs.append(f'rom{rom}')
romname = '.'.join(nameargs)
romdata = {}
for kname, val in data.items():
self.add_pair(romdata, kname, val)
cfg[f'ot_device "{romname}"'] = romdata

def _generate_otp(self, cfg: ConfigParser, socid: Optional[str] = None) \
-> None:
nameargs = ['ot-otp-dj']
if socid:
nameargs.append(socid)
otpname = '.'.join(nameargs)
otpdata = {}
self.add_pair(otpdata, 'lc_state_first', self._lc_states[0])
self.add_pair(otpdata, 'lc_state_last', self._lc_states[-1])
self.add_pair(otpdata, 'lc_trscnt_first', self._lc_transitions[0])
self.add_pair(otpdata, 'lc_trscnt_last', self._lc_transitions[-1])
for kname, val in self._otp.items():
self.add_pair(otpdata, kname, val)
cfg[f'ot_device "{otpname}"'] = otpdata

def _generate_life_cycle(self, cfg: ConfigParser,
socid: Optional[str] = None) -> None:
nameargs = ['ot-lc_ctrl']
if socid:
nameargs.append(socid)
lcname = '.'.join(nameargs)
lcdata = {}
for kname, value in self._lc.items():
self.add_pair(lcdata, kname, value)
cfg[f'ot_device "{lcname}"'] = lcdata


def main():
"""Main routine"""
debug = True
default_top = 'darjeeling'
try:
desc = modules[__name__].__doc__.split('.', 1)[0].strip()
argparser = ArgumentParser(description=f'{desc}.')
files = argparser.add_argument_group(title='Files')
files.add_argument('opentitan', nargs=1, metavar='TOPDIR',
help='OpenTitan top directory')
files.add_argument('-o', '--out', metavar='CFG',
help='Filename of the config file to generate')
files.add_argument('-T', '--top', default=default_top,
help=f'OpenTitan Top name (default: {default_top})')
files.add_argument('-c', '--otpconst', metavar='SV',
help='OTP Constant SV file (default: auto)')
files.add_argument('-l', '--lifecycle', metavar='SV',
help='LifeCycle SV file (default: auto)')
files.add_argument('-t', '--topcfg', metavar='HJSON',
help='OpenTitan top HJSON config file '
'(default: auto)')
mods = argparser.add_argument_group(title='Modifiers')
mods.add_argument('-s', '--socid',
help='SoC identifier, if any')
mods.add_argument('-C', '--count', default=1, type=int,
help='SoC count (default: 1)')
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, 'cfggen', 'otp')

if _HJSON_ERROR:
argparser.error('Missing HSJON module: {_HJSON_ERROR}')

topdir = args.opentitan[0]
if not isdir(topdir):
argparser.error('Invalid OpenTitan top directory')
ot_dir = normpath(topdir)
top = f'top_{args.top.lower()}'

if not args.topcfg:
cfgpath = joinpath(ot_dir, f'hw/{top}/data/autogen/{top}.gen.hjson')
else:
cfgpath = args.topcfg
if not isfile(cfgpath):
argparser.error(f"No such file '{cfgpath}'")

if not args.lifecycle:
lcpath = joinpath(ot_dir, 'hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv')
else:
lcpath = args.lifecycle
if not isfile(lcpath):
argparser.error(f"No such file '{lcpath}'")

if not args.otpconst:
ocpath = joinpath(ot_dir, 'hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv')
else:
ocpath = args.otpconst
if not isfile(lcpath):
argparser.error(f"No such file '{ocpath}'")

cfg = OtConfiguration()
cfg.load_top_config(cfgpath)
cfg.load_lifecycle(lcpath)
cfg.load_otp_constants(ocpath)
cfg.save(args.socid, args.count, args.out)

except (IOError, ValueError, ImportError) as exc:
print(f'\nError: {exc}', file=stderr)
if debug:
print(format_exc(chain=False), file=stderr)
sysexit(1)
except KeyboardInterrupt:
sysexit(2)


if __name__ == '__main__':
main()
Loading

0 comments on commit efb3e3d

Please sign in to comment.