[util, reggen] Autogen countermeasures testplan
This update auto-generates the security countermeasures testplan for all
IP blocks that have countermeasures listed in the IP Hjson spec.
The testplan is aurtogenerated initially. On subsequent invocations, it
checks whether the generated testplan is stale. This allows users to
hand-edit this testplan once generated.
Signed-off-by: Srikrishna Iyer <sriyer@google.com>
diff --git a/util/reggen/gen_sec_cm_testplan.py b/util/reggen/gen_sec_cm_testplan.py
new file mode 100644
index 0000000..c91bb14
--- /dev/null
+++ b/util/reggen/gen_sec_cm_testplan.py
@@ -0,0 +1,65 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+"""Generate the initial testplan for the listed countermeasures."""
+
+import logging as log
+from pathlib import Path
+
+import hjson # type: ignore
+from mako import exceptions # type: ignore
+from mako.lookup import TemplateLookup # type: ignore
+from pkg_resources import resource_filename
+
+from .ip_block import IpBlock
+
+
+def gen_sec_cm_testplan(block: IpBlock, outdir: str) -> int:
+ """Generate the security countermeasures testplan.
+
+ A new testplan is created only if it does not exist yet. If it already
+ exists, then it checks if the list of countermeasures match the list
+ of countermeasures in the design specification Hjson. If not, it throws
+ an error to prompt the user to keep the testplan up-to-date manually.
+ """
+ if not block.countermeasures:
+ return 0
+
+ outfile = Path(outdir) / f"{block.name.lower()}_sec_cm_testplan.hjson"
+ if outfile.exists():
+ names_from_testplan = []
+ with open(outfile, "r", encoding='UTF-8') as f:
+ data = hjson.load(f)
+ try:
+ names_from_testplan = [tp["name"] for tp in data["testpoints"]]
+ except KeyError as e:
+ raise KeyError(f"Malformed testplan {outfile}:\n{e}")
+
+ # Check if the testpoint names match the list in the design spec.
+ names_from_spec = [
+ "sec_cm_{}".format(str(cm).lower().replace(".", "_"))
+ for cm in block.countermeasures
+ ]
+
+ if sorted(names_from_spec) != sorted(names_from_testplan):
+ log.error("The generated security countermeasures testplan "
+ f"{outfile} is stale. Please manually update it "
+ "with the newly added (or removed) countermeasures.\n"
+ f"Deltas:\nSpec: {names_from_spec}\n"
+ f"Testplan: {names_from_testplan}.")
+ return 1
+
+ return 0
+
+ lookup = TemplateLookup(directories=[resource_filename('reggen', '.')])
+ sec_cm_testplan_tpl = lookup.get_template('sec_cm_testplan.hjson.tpl')
+ with open(outfile, 'w', encoding='UTF-8') as f:
+ try:
+ f.write(
+ sec_cm_testplan_tpl.render(block=block,
+ block_name=block.name.lower()))
+ except: # noqa F722 for template Exception handling
+ log.error(exceptions.text_error_template().render())
+ return 1
+
+ return 0
diff --git a/util/reggen/sec_cm_testplan.hjson.tpl b/util/reggen/sec_cm_testplan.hjson.tpl
new file mode 100644
index 0000000..6dc6fa5
--- /dev/null
+++ b/util/reggen/sec_cm_testplan.hjson.tpl
@@ -0,0 +1,40 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+// Security countermeasures testplan extracted from the IP Hjson using reggen.
+//
+// This testplan is auto-generated only the first time it is created. This is
+// because this testplan needs to be hand-editable. It is possible that these
+// testpoints can go out of date if the spec is updated with new
+// countermeasures. When `reggen` is invoked when this testplan already exists,
+// It checks if the list of testpoints is up-to-date and enforces the user to
+// make further manual updates.
+//
+// These countermeasures and their descriptions can be found here:
+// .../${block_name}/data/${block_name}.hjson
+//
+// It is possible that the testing of some of these countermeasures may already
+// be covered as a testpoint in a different testplan. This duplication is ok -
+// the test would have likely already been developed. We simply map those tests
+// to the testpoints below using the `tests` key.
+//
+// Please ensure that this testplan is imported in:
+// .../${block_name}/data/${block_name}_testplan.hjson
+{
+<%
+def get_sec_cm_testpoint_name(cm):
+ cm_name = str(cm).lower().replace(".", "_")
+ return f"sec_cm_{cm_name}"
+%>\
+ testpoints: [
+% for cm in block.countermeasures:
+ {
+ name: ${get_sec_cm_testpoint_name(cm)}
+ desc: "Verify the countermeasure(s) ${str(cm)}."
+ milestone: V2S
+ tests: []
+ }
+% endfor
+ ]
+}
diff --git a/util/regtool.py b/util/regtool.py
index 6bf359f..d5ecf35 100755
--- a/util/regtool.py
+++ b/util/regtool.py
@@ -11,10 +11,10 @@
import sys
from pathlib import Path
-from reggen import (gen_cheader, gen_dv, gen_fpv, gen_html,
- gen_json, gen_rtl, gen_rust, gen_selfdoc, version)
-from reggen.ip_block import IpBlock
+from reggen import (gen_cheader, gen_dv, gen_fpv, gen_html, gen_json, gen_rtl,
+ gen_rust, gen_sec_cm_testplan, gen_selfdoc, version)
from reggen.countermeasure import CounterMeasure
+from reggen.ip_block import IpBlock
DESC = """regtool, generate register info from Hjson source"""
@@ -61,6 +61,9 @@
parser.add_argument('-r',
action='store_true',
help='Output as SystemVerilog RTL')
+ parser.add_argument('--sec-cm-testplan',
+ action='store_true',
+ help='Generate security countermeasures testplan.')
parser.add_argument('-s',
action='store_true',
help='Output as UVM Register class')
@@ -123,6 +126,7 @@
('d', ('html', None)), ('doc', ('doc', None)),
('r', ('rtl', 'rtl')), ('s', ('dv', 'dv')),
('f', ('fpv', 'fpv/vip')), ('cdefines', ('cdh', None)),
+ ('sec_cm_testplan', ('sec_cm_testplan', 'data')),
('rust', ('rs', None))]
format = None
dirspec = None
@@ -146,8 +150,7 @@
if len(tokens) != 2:
raise ValueError('Entry {} in list of parameter defaults to '
'apply is {!r}, which is not of the form '
- 'param=value.'
- .format(idx, raw_param))
+ 'param=value.'.format(idx, raw_param))
params.append((tokens[0], tokens[1]))
# Define either outfile or outdir (but not both), depending on the output
@@ -207,6 +210,8 @@
else:
if format == 'rtl':
return gen_rtl.gen_rtl(obj, outdir)
+ if format == 'sec_cm_testplan':
+ return gen_sec_cm_testplan.gen_sec_cm_testplan(obj, outdir)
if format == 'dv':
return gen_dv.gen_dv(obj, args.dv_base_names, outdir)
if format == 'fpv':
@@ -237,7 +242,8 @@
if format == 'html':
return gen_html.gen_html(obj, outfile)
elif format == 'cdh':
- return gen_cheader.gen_cdefines(obj, outfile, src_lic, src_copy)
+ return gen_cheader.gen_cdefines(obj, outfile, src_lic,
+ src_copy)
elif format == 'rs':
return gen_rust.gen_rust(obj, outfile, src_lic, src_copy)
else: