[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}"