[otbn,util] Add initial constants to constant-time checker.

Allows the constant-time checker to require certain constant values at
the start of subroutines. This allows us to check p384_proj_add where
we couldn't before, since that subroutine requires certain GPRs to have
specific values.

Signed-off-by: Jade Philipoom <jadep@google.com>
diff --git a/hw/ip/otbn/util/check_const_time.py b/hw/ip/otbn/util/check_const_time.py
index 668cc8e..f34fd1f 100755
--- a/hw/ip/otbn/util/check_const_time.py
+++ b/hw/ip/otbn/util/check_const_time.py
@@ -6,6 +6,8 @@
 import argparse
 import sys
 
+from typing import Dict, List
+
 from shared.check import CheckResult
 from shared.control_flow import program_control_graph, subroutine_control_graph
 from shared.decode import decode_elf
@@ -13,6 +15,45 @@
                                               get_subroutine_iflow,
                                               stringify_control_deps)
 
+# GPR maximum value.
+GPR_MAX = (1 << 32) - 1
+
+def is_gpr_name(name: str):
+    return name in [f'x{i}' for i in range(32)]
+
+def parse_required_constants(constants: List[str]) -> Dict[str,int]:
+    '''Parses required initial constants.
+
+    Constants are expected to be provided in the form <reg>:<value>, e.g.
+    x5:0xfffffff or x22:0. The value can be expressed in decimal or integer
+    form. Only GPRs are accepted as required constants (not wide registers or
+    special registers).
+    '''
+    out = {}
+    for token in constants:
+        reg_and_value = token.split(':')
+        if len(reg_and_value) != 2:
+            raise ValueError(
+                    f'Could not parse required constant {token}. Please '
+                    'provide required constants in the form <reg>:<value>, '
+                    'e.g. x5:3.')
+        reg, value = reg_and_value
+        if not is_gpr_name(reg):
+            raise ValueError(
+                    f'Cannot parse required constant {token}: {reg} is not a '
+                    'valid GPR name.')
+        if not value.isnumeric():
+            raise ValueError(
+                    f'Cannot parse required constant {token}: {value} is not '
+                    'a recognized numeric value.')
+        value = int(value)
+        if value < 0 or value > GPR_MAX:
+            raise ValueError(
+                    f'Cannot parse required constant {token}: {value} is out '
+                    'of range [0, GPR_MAX].')
+        out[reg] = value
+    return out
+
 
 def main() -> int:
     parser = argparse.ArgumentParser(
@@ -26,6 +67,15 @@
         help=('The specific subroutine to check. If not provided, the start '
               'point is _imem_start (whole program).'))
     parser.add_argument(
+        '--constants',
+        nargs='+',
+        type=str,
+        required=False,
+        help=('Registers which are required to be constant at the start of the '
+              'subroutine. Only valid if `--subroutine` is passed. Write '
+              'in the form "reg:value", e.g. x3:5. Only GPRs are accepted as '
+              'required constants.'))
+    parser.add_argument(
         '--secrets',
         nargs='+',
         type=str,
@@ -36,6 +86,16 @@
               'of input.'))
     args = parser.parse_args()
 
+    # Parse initial constants.
+    if args.constants is None:
+        constants = {}
+    else:
+        if args.subroutine is None:
+            raise ValueError('Cannot require initial constants for a whole '
+                             'program; use --subroutine to analyze a specific '
+                             'subroutine.')
+        constants = parse_required_constants(args.constants)
+
     # Compute control graph and get all nodes that influence control flow.
     program = decode_elf(args.elf)
     if args.subroutine is None:
@@ -46,7 +106,7 @@
         graph = subroutine_control_graph(program, args.subroutine)
         to_analyze = 'subroutine {}'.format(args.subroutine)
         _, _, control_deps = get_subroutine_iflow(program, graph,
-                                                  args.subroutine)
+                                                  args.subroutine, constants)
 
     if args.secrets is None:
         if args.verbose:
@@ -56,8 +116,8 @@
         secret_control_deps = control_deps
     else:
         if args.verbose:
-            print('Analyzing {} with initial secrets {}'.format(
-                to_analyze, args.secrets))
+            print('Analyzing {} with initial secrets {} and initial constants {}'.format(
+                to_analyze, args.secrets, constants))
         # If secrets were provided, only show the ways in which those specific
         # nodes could influence control flow.
         secret_control_deps = {
diff --git a/hw/ip/otbn/util/shared/information_flow_analysis.py b/hw/ip/otbn/util/shared/information_flow_analysis.py
index 18d7735..fb3027f 100755
--- a/hw/ip/otbn/util/shared/information_flow_analysis.py
+++ b/hw/ip/otbn/util/shared/information_flow_analysis.py
@@ -528,7 +528,7 @@
 
 
 def get_subroutine_iflow(program: OTBNProgram, graph: ControlGraph,
-                         subroutine_name: str) -> SubroutineIFlow:
+        subroutine_name: str, start_constants: Dict[str,int]) -> SubroutineIFlow:
     '''Gets the information-flow graphs for the subroutine.
 
     Returns three items:
@@ -540,9 +540,14 @@
     3. The information-flow nodes whose values at the start of the subroutine
        influence its control flow.
     '''
+    if 'x0' in start_constants and start_constants['x0'] != 0:
+        raise ValueError('The x0 register is always 0; cannot require '
+                f'x0={start_constants["x0"]}')
+    start_constants['x0'] = 0
+    constants = ConstantContext(start_constants)
     start_pc = program.get_pc_at_symbol(subroutine_name)
     _, ret_iflow, end_iflow, _, cycles, control_deps = _get_iflow(
-        program, graph, start_pc, ConstantContext.empty(), None, IFlowCache())
+        program, graph, start_pc, constants, None, IFlowCache())
     if cycles:
         for pc in cycles:
             print(cycles[pc].pretty())
diff --git a/rules/otbn.bzl b/rules/otbn.bzl
index 1b3fb96..c9bbedb 100644
--- a/rules/otbn.bzl
+++ b/rules/otbn.bzl
@@ -216,6 +216,8 @@
         script_content += " --subroutine {}".format(ctx.attr.subroutine)
     if ctx.attr.secrets:
         script_content += " --secrets {}".format(" ".join(ctx.attr.secrets))
+    if ctx.attr.initial_constants:
+        script_content += " --constants {}".format(" ".join(ctx.attr.initial_constants))
     ctx.actions.write(
         output = ctx.outputs.executable,
         content = script_content,
@@ -310,6 +312,7 @@
         "deps": attr.label_list(providers = [OutputGroupInfo]),
         "subroutine": attr.string(),
         "secrets": attr.string_list(),
+        "initial_constants": attr.string_list(),
         "_checker": attr.label(
             default = "//hw/ip/otbn/util:check_const_time",
             executable = True,
diff --git a/sw/otbn/crypto/BUILD b/sw/otbn/crypto/BUILD
index 88eb8f1..a848469 100644
--- a/sw/otbn/crypto/BUILD
+++ b/sw/otbn/crypto/BUILD
@@ -344,19 +344,19 @@
 #   subroutine = "p384_sign",
 # )
 
-# TODO: Add an argument to the constant-time checker script that accepts
-# "required constant registers". This test fails because the subroutine
-# requires some registers (indirect references) to be constant at the start,
-# and without this information the constant-time checker cannot construct the
-# information-flow graph.
-#
-# otbn_consttime_test(
-#   name = "p384_proj_add_consttime",
-#   deps = [
-#       ":p384_ecdsa_sign_test"
-#   ],
-#   subroutine = "proj_add_p384",
-# )
+otbn_consttime_test(
+    name = "proj_add_p384_consttime",
+    initial_constants = [
+        "x22:10",
+        "x23:11",
+        "x24:16",
+        "x25:17",
+    ],
+    subroutine = "proj_add_p384",
+    deps = [
+        ":p384_ecdsa_sign_test",
+    ],
+)
 
 otbn_consttime_test(
     name = "scalar_mult_p384_consttime",