[sigverify] Add an automatic check for OTBN instruction count ranges.
This adds a custom Bazel test that can run in CI to check the min/max
instruction count range in mod_exp_otbn.h against the values calculated
by the new static analysis script. Adding a test in this way seems
preferable to automatically generating the numbers, because we should in
general be double-checking the script's answers with manual analysis to
be extra certain.
Signed-off-by: Jade Philipoom <jadep@google.com>
diff --git a/hw/ip/otbn/util/BUILD b/hw/ip/otbn/util/BUILD
index f6deb23..54ca0df 100644
--- a/hw/ip/otbn/util/BUILD
+++ b/hw/ip/otbn/util/BUILD
@@ -49,3 +49,12 @@
requirement("pyelftools"),
],
)
+
+py_binary(
+ name = "get_instruction_count_range",
+ srcs = ["get_instruction_count_range.py"],
+ deps = [
+ "//hw/ip/otbn/util/shared:decode",
+ "//hw/ip/otbn/util/shared:instruction_count_range",
+ ],
+)
diff --git a/rules/otbn.bzl b/rules/otbn.bzl
index c9bbedb..0c15a71 100644
--- a/rules/otbn.bzl
+++ b/rules/otbn.bzl
@@ -230,6 +230,27 @@
runfiles = runfiles.merge(ctx.attr._checker[DefaultInfo].default_runfiles)
return [DefaultInfo(runfiles = runfiles)]
+def _otbn_insn_count_range(ctx):
+ """This rule gets min/max possible instruction counts for an OTBN program.
+ """
+
+ # Extract the .elf file to check from the dependency list.
+ elf = [f for t in ctx.attr.deps for f in t[OutputGroupInfo].elf.to_list()]
+ if len(elf) != 1:
+ fail("Expected only one .elf file in dependencies, got: " + str(elf))
+ elf = elf[0]
+
+ # Command to run the counter script and extract the min/max values.
+ out = ctx.actions.declare_file(ctx.attr.name + ".txt")
+ ctx.actions.run_shell(
+ outputs = [out],
+ inputs = [ctx.file._counter, elf],
+ command = "{} {} > {}".format(ctx.file._counter.path, elf.path, out.path),
+ )
+
+ runfiles = ctx.runfiles(files = ([out]))
+ return [DefaultInfo(files = depset([out]), runfiles = runfiles)]
+
otbn_library = rv_rule(
implementation = _otbn_library,
attrs = {
@@ -320,3 +341,14 @@
),
},
)
+
+otbn_insn_count_range = rule(
+ implementation = _otbn_insn_count_range,
+ attrs = {
+ "deps": attr.label_list(providers = [OutputGroupInfo]),
+ "_counter": attr.label(
+ default = "//hw/ip/otbn/util:get_instruction_count_range.py",
+ allow_single_file = True,
+ ),
+ },
+)
diff --git a/sw/device/silicon_creator/lib/sigverify/BUILD b/sw/device/silicon_creator/lib/sigverify/BUILD
index bc5cace..321ec1e 100644
--- a/sw/device/silicon_creator/lib/sigverify/BUILD
+++ b/sw/device/silicon_creator/lib/sigverify/BUILD
@@ -10,6 +10,7 @@
"opentitan_functest",
"verilator_params",
)
+load("//rules:otbn.bzl", "otbn_insn_count_range")
package(default_visibility = ["//visibility:public"])
@@ -159,6 +160,30 @@
],
)
+# This rule runs the instruction-counting script for OTBN and gets the expected
+# min/max instruction counts.
+otbn_insn_count_range(
+ name = "mod_exp_otbn_insn_count_range",
+ deps = [
+ "//sw/otbn/crypto:run_rsa_verify_3072_rr_modexp",
+ ],
+)
+
+# Check the OTBN instruction count in mod_exp_otbn.h
+sh_test(
+ name = "mod_exp_otbn_insn_count_check",
+ size = "small",
+ srcs = ["mod_exp_otbn_insn_count_check.sh"],
+ args = [
+ "$(location :mod_exp_otbn_insn_count_range)",
+ "$(location mod_exp_otbn.h)",
+ ],
+ data = [
+ "mod_exp_otbn.h",
+ ":mod_exp_otbn_insn_count_range",
+ ],
+)
+
# Common inputs for sigverify.c; we publish two variants of this
# library, so factoring this out simplifies the build.
cc_library(
diff --git a/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn.h b/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn.h
index b0c311c..55c143a 100644
--- a/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn.h
+++ b/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn.h
@@ -16,14 +16,19 @@
#endif // __cplusplus
/**
- * Manually-calculated instruction count range.
+ * Possible range of instruction counts for modexp.
*
- * The OTBN modexp implementation is not constant-time (which is okay because
- * it has no secret inputs). However, all valid control-flow paths should fall
- * within this range, which is calculated by changing the code to either take
- * all branches or skip them all.
+ * This range should represent the theoretical minimum/maximum instruction
+ * counts for any input to the program; if the instruction count recorded by
+ * OTBN is different, we will suspect a fault injection attack.
*
- * IMPORTANT: This may need to be modified if the modexp routine is changed!
+ * The OTBN modexp implementation is not constant-time, but that is okay
+ * because it has no secret inputs and therefore can't leak secret information.
+ *
+ * IMPORTANT: This may need to be modified if the modexp routine is changed! If
+ * updating this value, please both use the automatic script
+ * (get_instruction_count_range.py) and also double-check by manually
+ * modifying the code to skip/take all branches.
*/
enum {
kModExpOtbnInsnCountMin = 181147,
diff --git a/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn_insn_count_check.sh b/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn_insn_count_check.sh
new file mode 100755
index 0000000..5a01fd0
--- /dev/null
+++ b/sw/device/silicon_creator/lib/sigverify/mod_exp_otbn_insn_count_check.sh
@@ -0,0 +1,26 @@
+#!/bin/sh
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+# Usage: mod_exp_otbn_insn_count_check.sh COUNTS_FILE HEADER_FILE
+#
+# COUNTS_FILE: file including min/max instruction counts
+# HEADER_FILE: header file that should contain matching counts
+
+set -e
+
+counts_file="$1"
+header_file="$2"
+
+# Get the minimum/maximum instruction counts from the `counts_file`.
+min=$(grep "Minimum instruction count: " "${counts_file}" | sed -e "s/Minimum instruction count: //")
+max=$(grep "Maximum instruction count: " "${counts_file}" | sed -e "s/Maximum instruction count: //")
+echo "Expected minimum count: ${min}"
+echo "Expected maximum count: ${max}"
+
+echo "If this test fails, double check that the instruction count range above matches the one in ${header_file}."
+
+# Check that these numbers match the ones in the header file.
+grep "kModExpOtbnInsnCountMin = ${min}," "${header_file}"
+grep "kModExpOtbnInsnCountMax = ${max}," "${header_file}"