-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ot] scripts/opentitan: cfggen.py: new tool to generate QEMU OT confi…
…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
1 parent
ae1a292
commit efb3e3d
Showing
12 changed files
with
602 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
```` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.