Skip to content

Commit a4be24b

Browse files
committed
[ot] scripts/opentitan: cfggen.py: new tool to generate QEMU OT config 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 <eblot@rivosinc.com>
1 parent c17e36e commit a4be24b

File tree

12 files changed

+602
-108
lines changed

12 files changed

+602
-108
lines changed

docs/opentitan/cfggen.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# `cfggen.py`
2+
3+
`cfggen.py` is a helper tool that can generate a QEMU OT configuration file,
4+
for use with QEMU's `-readconfig` option, populated with sensitive data for
5+
the ROM controller(s), the OTP controller, the Life Cycle controller, etc.
6+
7+
It heurastically parses configuration and generated RTL files to extract from
8+
them the required keys, seeds, nonces and other tokens that are not stored in
9+
the QEMU binary.
10+
11+
## Usage
12+
13+
````text
14+
usage: cfggen.py [-h] [-o CFG] [-T TOP] [-c SV] [-l SV] [-t HJSON] [-s SOCID]
15+
[-C COUNT] [-v] [-d]
16+
TOPDIR
17+
18+
OpenTitan QEMU configuration file generator.
19+
20+
options:
21+
-h, --help show this help message and exit
22+
23+
Files:
24+
TOPDIR OpenTitan top directory
25+
-o CFG, --out CFG Filename of the config file to generate
26+
-T TOP, --top TOP OpenTitan Top name (default: darjeeling)
27+
-c SV, --otpconst SV OTP Constant SV file (default: auto)
28+
-l SV, --lifecycle SV
29+
LifeCycle SV file (default: auto)
30+
-t HJSON, --topcfg HJSON
31+
OpenTitan top HJSON config file (default: auto)
32+
33+
Modifiers:
34+
-s SOCID, --socid SOCID
35+
SoC identifier, if any
36+
-C COUNT, --count COUNT
37+
SoC count (default: 1)
38+
39+
Extras:
40+
-v, --verbose increase verbosity
41+
-d, --debug enable debug mode
42+
````
43+
44+
45+
### Arguments
46+
47+
`TOPDIR` is a required positional argument which should point to the top-level directory of the
48+
OpenTitan repository to analyze. It is used to generate the path towards the required files to
49+
parse, each of which can be overidden with options `-c`, `-l` and `-t`.
50+
51+
* `-C` specify how many SoCs are used on the platform
52+
53+
* `-c` alternative path to the `otp_ctrl_part_pkg.sv` file
54+
55+
* `-d` only useful to debug the script, reports any Python traceback to the standard error stream.
56+
57+
* `-l` alternative path to the `lc_ctrl_state_pkg.sv.sv` file
58+
59+
* `-o` the filename of the configuration file to generate. It not specified, the generated content
60+
is printed out to the standard output.
61+
62+
* `-s` specify a SoC identifier for OT platforms with mulitple SoCs
63+
64+
* `-T` specify the OpenTitan _top_ name, such as `Darjeeling`, `EarlGrey`, ... This option is
65+
case-insensitive.
66+
67+
* `-t` alternative path to the `top_<top>.gen.hjson` file
68+
69+
* `-v` can be repeated to increase verbosity of the script, mostly for debug purpose.
70+
71+
72+
### Examples
73+
74+
````sh
75+
./scripts/opentitan/cfggen.py ../opentitan-integrated -o opentitan.cfg
76+
````

docs/opentitan/otcfg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ an_integer = 0x1234
4848
a_boolean = true
4949
```
5050

51+
## Generation
52+
53+
It is possible to delegate the generation of an OpenTitan configuration file to the [`cfggen.py`](cfggen.md)
54+
script, using an existing OpenTitan repository.
55+
5156
## Configurable constants
5257

5358
Constants can usually be retrieved from the OpenTitan autogenerated "top" HSJON file, from the

docs/opentitan/tools.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ directory to help with these tasks.
1717

1818
## Companion file management
1919

20+
* [`cfggen.py`](cfggen.md) can be used to generate an OpenTitan [configuration file](otcfg.md) from
21+
an existing OpenTitan repository.
2022
* [`otpdm.py`](otpdm.md) can be used to access the OTP Controller over a JTAG/DTM/DM link. It reads
2123
out partition's item values and can update those items.
2224
* [`otptool.py`](otptool.md) can be used to generate an OTP image from a OTP VMEM file and can be

scripts/opentitan/cfggen.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright (c) 2024 Rivos, Inc.
4+
# SPDX-License-Identifier: Apache2
5+
6+
"""OpenTitan QEMU configuration file generator.
7+
8+
:author: Emmanuel Blot <eblot@rivosinc.com>
9+
"""
10+
11+
from argparse import ArgumentParser
12+
from configparser import ConfigParser
13+
from logging import getLogger
14+
from os.path import isdir, isfile, join as joinpath, normpath
15+
from re import match, search
16+
from sys import exit as sysexit, modules, stderr
17+
from traceback import format_exc
18+
from typing import Optional
19+
20+
try:
21+
_HJSON_ERROR = None
22+
from hjson import load as hjload
23+
except ImportError as hjson_exc:
24+
_HJSON_ERROR = str(hjson_exc)
25+
26+
from ot.util.log import configure_loggers
27+
from ot.util.misc import camel_to_snake_case
28+
from ot.otp.const import OtpConstants
29+
from ot.otp.lifecycle import OtpLifecycle
30+
31+
32+
OtParamRegex = str
33+
"""Definition of a parameter to seek and how to shorten it."""
34+
35+
36+
class OtConfiguration:
37+
"""QEMU configuration file generator."""
38+
39+
def __init__(self):
40+
self._log = getLogger('cfggen.cfg')
41+
self._lc_states: tuple[str, str] = ('', '')
42+
self._lc_transitions: tuple[str, str] = ('', '')
43+
self._roms: dict[Optional[int], dict[str, str]] = {}
44+
self._otp: dict[str, str] = {}
45+
self._lc: dict[str, str] = {}
46+
47+
def load_top_config(self, toppath: str) -> None:
48+
"""Load data from HJSON top configuration file."""
49+
with open(toppath, 'rt') as tfp:
50+
cfg = hjload(tfp)
51+
for module in cfg.get('module') or []:
52+
modtype = module.get('type')
53+
if modtype == 'rom_ctrl':
54+
self._load_top_values(module, self._roms, True,
55+
r'RndCnstScr(.*)')
56+
continue
57+
if modtype == 'otp_ctrl':
58+
self._load_top_values(module, self._otp, False,
59+
r'RndCnst(.*)Init')
60+
continue
61+
62+
def load_lifecycle(self, lcpath: str) -> None:
63+
"""Load LifeCycle data from RTL file."""
64+
lcext = OtpLifecycle()
65+
with open(lcpath, 'rt') as lfp:
66+
lcext.load(lfp)
67+
states = lcext.get_configuration('LC_STATE')
68+
if not states:
69+
raise ValueError('Cannot obtain LifeCycle states')
70+
for raw in {s for s in states if int(s, 16) == 0}:
71+
del states[raw]
72+
ostates = list(states)
73+
self._lc_states = ostates[0], ostates[-1]
74+
self._log.info("States first: '%s', last '%s'",
75+
states[self._lc_states[0]], states[self._lc_states[1]])
76+
trans = lcext.get_configuration('LC_TRANSITION_CNT')
77+
if not trans:
78+
raise ValueError('Cannot obtain LifeCycle transitions')
79+
for raw in {s for s in trans if int(s, 16) == 0}:
80+
del trans[raw]
81+
otrans = list(trans)
82+
self._lc_transitions = otrans[0], otrans[-1]
83+
self._log.info('Transitions first : %d, last %d',
84+
int(trans[self._lc_transitions[0]]),
85+
int(trans[self._lc_transitions[1]]))
86+
self._lc.update(lcext.get_tokens(False, False))
87+
88+
def load_otp_constants(self, otppath: str) -> None:
89+
"""Load OTP data from RTL file."""
90+
otpconst = OtpConstants()
91+
with open(otppath, 'rt') as cfp:
92+
otpconst.load(cfp)
93+
self._otp.update(otpconst.get_digest_pair('cnsty_digest', 'digest'))
94+
self._otp.update(otpconst.get_digest_pair('sram_data_key', 'sram'))
95+
96+
def save(self, socid: Optional[str] = None, count: Optional[int] = 1,
97+
outpath: Optional[str] = None) \
98+
-> None:
99+
"""Save QEMU configuration file using a INI-like file format,
100+
compatible with the `-readconfig` option of QEMU.
101+
"""
102+
cfg = ConfigParser()
103+
self._generate_roms(cfg, socid, count)
104+
self._generate_otp(cfg, socid)
105+
self._generate_life_cycle(cfg, socid)
106+
if outpath:
107+
with open(outpath, 'wt') as ofp:
108+
cfg.write(ofp)
109+
else:
110+
cfg.write(stderr)
111+
112+
@classmethod
113+
def add_pair(cls, data: dict[str, str], kname: str, value: str) -> None:
114+
"""Helper to create key, value pair entries."""
115+
data[f' {kname}'] = f'"{value}"'
116+
117+
def _load_top_values(self, module: dict, odict: dict, multi: bool,
118+
*regexes: list[OtParamRegex]) -> None:
119+
modname = module.get('name')
120+
if not modname:
121+
return
122+
for params in module.get('param_list', []):
123+
if not isinstance(params, dict):
124+
continue
125+
for regex in regexes: # TODO: camelcase to lower snake case
126+
pmo = match(regex, params['name'])
127+
if not pmo:
128+
continue
129+
value = params.get('default')
130+
if not value:
131+
continue
132+
if value.startswith('0x'):
133+
value = value[2:]
134+
kname = camel_to_snake_case(pmo.group(1))
135+
if multi:
136+
imo = search(r'(\d+)$', modname)
137+
idx = int(imo.group(1)) if imo else 'None'
138+
if idx not in odict:
139+
odict[idx] = {}
140+
odict[idx][kname] = value
141+
else:
142+
odict[kname] = value
143+
144+
def _generate_roms(self, cfg: ConfigParser, socid: Optional[str] = None,
145+
count: int = 1) -> None:
146+
for cnt in range(count):
147+
for rom, data in self._roms.items():
148+
nameargs = ['ot-rom_ctrl']
149+
if socid:
150+
if count > 1:
151+
nameargs.append(f'{socid}{cnt}')
152+
else:
153+
nameargs.append(socid)
154+
if rom is not None:
155+
nameargs.append(f'rom{rom}')
156+
romname = '.'.join(nameargs)
157+
romdata = {}
158+
for kname, val in data.items():
159+
self.add_pair(romdata, kname, val)
160+
cfg[f'ot_device "{romname}"'] = romdata
161+
162+
def _generate_otp(self, cfg: ConfigParser, socid: Optional[str] = None) \
163+
-> None:
164+
nameargs = ['ot-otp-dj']
165+
if socid:
166+
nameargs.append(socid)
167+
otpname = '.'.join(nameargs)
168+
otpdata = {}
169+
self.add_pair(otpdata, 'lc_state_first', self._lc_states[0])
170+
self.add_pair(otpdata, 'lc_state_last', self._lc_states[-1])
171+
self.add_pair(otpdata, 'lc_trscnt_first', self._lc_transitions[0])
172+
self.add_pair(otpdata, 'lc_trscnt_last', self._lc_transitions[-1])
173+
for kname, val in self._otp.items():
174+
self.add_pair(otpdata, kname, val)
175+
cfg[f'ot_device "{otpname}"'] = otpdata
176+
177+
def _generate_life_cycle(self, cfg: ConfigParser,
178+
socid: Optional[str] = None) -> None:
179+
nameargs = ['ot-lc_ctrl']
180+
if socid:
181+
nameargs.append(socid)
182+
lcname = '.'.join(nameargs)
183+
lcdata = {}
184+
for kname, value in self._lc.items():
185+
self.add_pair(lcdata, kname, value)
186+
cfg[f'ot_device "{lcname}"'] = lcdata
187+
188+
189+
def main():
190+
"""Main routine"""
191+
debug = True
192+
default_top = 'darjeeling'
193+
try:
194+
desc = modules[__name__].__doc__.split('.', 1)[0].strip()
195+
argparser = ArgumentParser(description=f'{desc}.')
196+
files = argparser.add_argument_group(title='Files')
197+
files.add_argument('opentitan', nargs=1, metavar='TOPDIR',
198+
help='OpenTitan top directory')
199+
files.add_argument('-o', '--out', metavar='CFG',
200+
help='Filename of the config file to generate')
201+
files.add_argument('-T', '--top', default=default_top,
202+
help=f'OpenTitan Top name (default: {default_top})')
203+
files.add_argument('-c', '--otpconst', metavar='SV',
204+
help='OTP Constant SV file (default: auto)')
205+
files.add_argument('-l', '--lifecycle', metavar='SV',
206+
help='LifeCycle SV file (default: auto)')
207+
files.add_argument('-t', '--topcfg', metavar='HJSON',
208+
help='OpenTitan top HJSON config file '
209+
'(default: auto)')
210+
mods = argparser.add_argument_group(title='Modifiers')
211+
mods.add_argument('-s', '--socid',
212+
help='SoC identifier, if any')
213+
mods.add_argument('-C', '--count', default=1, type=int,
214+
help='SoC count (default: 1)')
215+
extra = argparser.add_argument_group(title='Extras')
216+
extra.add_argument('-v', '--verbose', action='count',
217+
help='increase verbosity')
218+
extra.add_argument('-d', '--debug', action='store_true',
219+
help='enable debug mode')
220+
args = argparser.parse_args()
221+
debug = args.debug
222+
223+
configure_loggers(args.verbose, 'cfggen', 'otp')
224+
225+
if _HJSON_ERROR:
226+
argparser.error('Missing HSJON module: {_HJSON_ERROR}')
227+
228+
topdir = args.opentitan[0]
229+
if not isdir(topdir):
230+
argparser.error('Invalid OpenTitan top directory')
231+
ot_dir = normpath(topdir)
232+
top = f'top_{args.top.lower()}'
233+
234+
if not args.topcfg:
235+
cfgpath = joinpath(ot_dir, f'hw/{top}/data/autogen/{top}.gen.hjson')
236+
else:
237+
cfgpath = args.topcfg
238+
if not isfile(cfgpath):
239+
argparser.error(f"No such file '{cfgpath}'")
240+
241+
if not args.lifecycle:
242+
lcpath = joinpath(ot_dir, 'hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv')
243+
else:
244+
lcpath = args.lifecycle
245+
if not isfile(lcpath):
246+
argparser.error(f"No such file '{lcpath}'")
247+
248+
if not args.otpconst:
249+
ocpath = joinpath(ot_dir, 'hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv')
250+
else:
251+
ocpath = args.otpconst
252+
if not isfile(lcpath):
253+
argparser.error(f"No such file '{ocpath}'")
254+
255+
cfg = OtConfiguration()
256+
cfg.load_top_config(cfgpath)
257+
cfg.load_lifecycle(lcpath)
258+
cfg.load_otp_constants(ocpath)
259+
cfg.save(args.socid, args.count, args.out)
260+
261+
except (IOError, ValueError, ImportError) as exc:
262+
print(f'\nError: {exc}', file=stderr)
263+
if debug:
264+
print(format_exc(chain=False), file=stderr)
265+
sysexit(1)
266+
except KeyboardInterrupt:
267+
sysexit(2)
268+
269+
270+
if __name__ == '__main__':
271+
main()

0 commit comments

Comments
 (0)