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