Skip to content

Commit

Permalink
[cryptotest] refactor parse_rsp and clean up test vector build script
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Torok <[email protected]>
  • Loading branch information
RyanTorok authored and milesdai committed Feb 29, 2024
1 parent 053f56f commit 2a0f24e
Show file tree
Hide file tree
Showing 8 changed files with 414 additions and 147 deletions.
21 changes: 12 additions & 9 deletions sw/host/cryptotest/testvectors/data/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ load("@bazel_skylib//rules:run_binary.bzl", "run_binary")

package(default_visibility = ["//visibility:public"])

genrule(
run_binary(
name = "nist_cavp_ecdsa_fips_186_4_sig_ver_json",
srcs = [
"@nist_cavp_ecdsa_fips_186_4//:SigVer.rsp",
"//sw/host/cryptotest/testvectors/data/schemas:ecdsa_sig_ver_schema",
"@nist_cavp_ecdsa_fips_186_4//:SigVer.rsp",
],
outs = [":nist_cavp_ecdsa_fips_186_4_sig_ver.json"],
args = [
"--src",
"$(location @nist_cavp_ecdsa_fips_186_4//:SigVer.rsp)",
"--dst",
"$(location :nist_cavp_ecdsa_fips_186_4_sig_ver.json)",
"--schema",
"$(location //sw/host/cryptotest/testvectors/data/schemas:ecdsa_sig_ver_schema)",
],
outs = ["nist_cavp_ecdsa_fips_186_4_sig_ver.json"],
cmd = """$(location //sw/host/cryptotest/testvectors/parsers:nist_cavp_ecdsa_parser) \
--src $(location @nist_cavp_ecdsa_fips_186_4//:SigVer.rsp) \
--dst $(RULEDIR)/nist_cavp_ecdsa_fips_186_4_sig_ver \
--schema $(location //sw/host/cryptotest/testvectors/data/schemas:ecdsa_sig_ver_schema)""",
message = "Parsing testvector - NIST CAVP Digital Signatures FIPS 186-4 - ECDSA",
tools = ["//sw/host/cryptotest/testvectors/parsers:nist_cavp_ecdsa_parser"],
tool = "//sw/host/cryptotest/testvectors/parsers:nist_cavp_ecdsa_parser",
)

[
Expand Down
11 changes: 10 additions & 1 deletion sw/host/cryptotest/testvectors/parsers/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
load("@ot_python_deps//:requirements.bzl", "requirement")

package(default_visibility = ["//visibility:public"])
Expand All @@ -13,6 +13,15 @@ py_library(
visibility = ["//visibility:private"],
)

py_test(
name = "cryptotest_util_test",
srcs = [
"cryptotest_util.py",
"cryptotest_util_test.py",
],
visibility = ["//visibility:private"],
)

py_binary(
name = "nist_aes_parser",
srcs = ["nist_aes_parser.py"],
Expand Down
106 changes: 78 additions & 28 deletions sw/host/cryptotest/testvectors/parsers/cryptotest_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,103 @@
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

def parse_rsp(file_path: str) -> dict:
def parse_rsp(file_path: str, persists: list[str] = []) -> dict:
"""Parser for NIST `.rsp` files.
NIST response files (.rsp) provide the expected results of cryptographic
operations. No formal standard exists for these files, but they generally take
the following format:
- Each file contains zero or more sections. The start of each section is
denoted by a line beginning with "[" and ending with "]". The text between
the brackets is the title of the section.
- Each section contains at least one entry where an entry is a collection of
key-value pairs. Entries are separated from each other by an empty line.
- Lines beginning with a "[" are headers. Headers contain either a bare name,
like "Section Name", a single key-value pair, like "Hash = SHA-256", or a
list of strings, like "RSA-2048, SHA-256".
- A line that starts with a "#" is a comment and is ignored.
- Other non-blank lines contain a single key-value pair.
Due to the heterogeneity of these files, this function applies multiple complex
rules to parse the content into a set of test vectors:
- Some key-value pairs are applied to multiple test cases. These include the
key-value pairs in headers, as well as those with keys specified in the
argument array `persists`.
- A group of non-header key-value pair lines (delimited by newlines or a header
line) constitutes a test case if it does not contain any keys in `persists`.
- If a group of non-header key-value pair lines contains the same key multiple
times without a newline in between, all of their associated values will be
kept in a list.
"""
result = dict()
curr_section = list()
curr_section_name = ""
curr_entry = dict()
persistent_variables = {}
test_cases = []
test_case = None
exclude_group = False
group_variables = []
with open(file_path, "r") as file:
for line_num, line in enumerate(file):
line = line.strip()
# Ignore comments
if line.startswith("#"):
continue
elif line.startswith("["):
# Store the previous section and start a new section
if curr_section:
result[curr_section_name] = curr_section
curr_section = list()
curr_section_name = line.strip("[]")
# We have a header. If the header is a single title, store
# it as the section name. If it is a key-value pair, store
# the pair in our set of persistent variables.

# If we were working on a test case, add it.
if test_case is not None:
if not exclude_group:
test_cases.append(test_case)
test_case = None
group_variables = []
exclude_group = False
header_text = line.strip("[]")
kv = [s.strip() for s in header_text.split("=", maxsplit=1)]
if len(kv) == 1:
# We don't have a key-value pair. See if we have a CSV list.
csv = [s.strip() for s in header_text.split(",")]
if len(csv) == 1:
# Just a name. Store it as a string.
persistent_variables["section_name"] = header_text
else:
# We have a CSV list. Store it as an array.
persistent_variables["section_name"] = csv
else:
# We have a key-value pair to store.
persistent_variables[kv[0]] = kv[1]
elif line == "":
# Store the previous entry and start a new entry
if curr_entry:
curr_section.append(curr_entry)
curr_entry = dict()
# We are staring a new group. Add the test case if we have one,
# and clear our list of variables for the current group.
if test_case is not None and not exclude_group:
test_cases.append(test_case)
test_case = None
group_variables = []
exclude_group = False
elif line and "=" in line:
# Append the key-value pair to the current entry
if test_case is None:
test_case = persistent_variables.copy()
# Get the key and value
key, value = [s.strip() for s in line.split("=", maxsplit=1)]
if key in curr_entry.keys():
raise SyntaxError(f"Line {line_num}: Duplicate key ({key}) in entry")
curr_entry[key] = value
d = test_case
# If we have a variable intended to persist, store it properly and
# remember the current group does not constitute a test case.
if key in persists:
d = persistent_variables
exclude_group = True
if key in group_variables:
# We've already seen this variable in the current group. Store all
# the values in a list.
if type(d[key]) == str:
d[key] = [d[key], value]
else:
d[key].append(value)
else:
# We haven't seen this variable yet in the current
# group. Store its value and remember we've seen
# it.
d[key] = value
group_variables.append(key)

# If there is no newline at the end of the rsp file, then append the
# last entry to the section
if curr_entry:
curr_section.append(curr_entry)
# Append the last section to the result
result[curr_section_name] = curr_section
return result
if test_case is not None:
test_cases.append(test_case)
return test_cases


def str_to_byte_array(s: str) -> list:
Expand Down
210 changes: 210 additions & 0 deletions sw/host/cryptotest/testvectors/parsers/cryptotest_util_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

import tempfile
import os
import unittest

from cryptotest_util import parse_rsp


def createRsp(content: bytes) -> str:
with tempfile.NamedTemporaryFile(delete=False, dir=os.environ["TEST_TMPDIR"]) as temp:
temp.write(content)
return temp.name


class RspTests(unittest.TestCase):

"""Check that we can parse headers and variables."""
def test_simple(self):
rsp = createRsp(b"""
[section name]
x = 1234
y = 5678
""")
expected = [{
"section_name": "section name",
"x": "1234",
"y": "5678",
}]
self.assertEqual(expected, parse_rsp(rsp, []))

def test_no_trailing_newline(self):
"""Check that we can parse a test case at the end of the file with no trailing newline."""
rsp = createRsp(b"""
[section name]
x = 1234
y = 5678
""")
expected = [{
"section_name": "section name",
"x": "1234",
"y": "5678",
}]
self.assertEqual(expected, parse_rsp(rsp, []))

def test_csv_header(self):
"""Check that we can operate without headers and parse multiple test cases."""
rsp = createRsp(b"""
[a, b, c]
x = 1234
y = 5678
""")
expected = [
{
"section_name": ["a", "b", "c"],
"x": "1234",
"y": "5678",
},
]
self.assertEqual(expected, parse_rsp(rsp, ["m", "n"]))

def test_multiple_tests_no_headers(self):
"""Check that we can operate without headers and parse multiple test cases."""
rsp = createRsp(b"""
x = 1234
y = 5678
x = 1111
y = 2222
""")
expected = [
{
"x": "1234",
"y": "5678",
},
{
"x": "1111",
"y": "2222",
},
]
self.assertEqual(expected, parse_rsp(rsp, ["m", "n"]))

def test_header_variables(self):
"""Check that header variables are stored persistently."""
rsp = createRsp(b"""
[section name]
[a = hello]
[b = world]
x = 1234
y = 5678
x = 1111
y = 2222
""")
expected = [
{
"section_name": "section name",
"a": "hello",
"b": "world",
"x": "1234",
"y": "5678",
},
{
"section_name": "section name",
"a": "hello",
"b": "world",
"x": "1111",
"y": "2222",
},
]
self.assertEqual(expected, parse_rsp(rsp, []))

def test_header_delimiter(self):
"""Check that headers can delimit test cases."""
rsp = createRsp(b"""
[a = hello]
x = 1234
y = 5678
[a = world]
x = 1111
y = 2222
""")
expected = [
{
"a": "hello",
"x": "1234",
"y": "5678",
},
{
"a": "world",
"x": "1111",
"y": "2222",
},
]
self.assertEqual(expected, parse_rsp(rsp, []))

def test_persistent_variables(self):
"""Check that the user-specified persistent variables are respected."""
rsp = createRsp(b"""
m = 1
n = 2
o = 3
x = 1234
y = 5678
x = 1111
y = 2222
""")
expected = [
{
"m": "1",
"n": "2",
"x": "1234",
"y": "5678",
},
{
"m": "1",
"n": "2",
"x": "1111",
"y": "2222",
},
]
self.assertEqual(expected, parse_rsp(rsp, ["m", "n"]))

def test_duplicate_variables(self):
"""Check that duplicate values of variables in the same group are stored in lists."""
rsp = createRsp(b"""
m = abc
m = def
n = efg
n = hij
x = 1234
y = 5678
x = 1111
y = 2222
x = 3333
y = 4444
""")
expected = [
{
"m": ["abc", "def"],
# Should not duplicate, because the copies are in different groups
"n": "hij",
"x": ["1234", "1111"],
"y": ["5678", "2222"],
},
{
"m": ["abc", "def"],
"n": "hij",
"x": "3333",
"y": "4444",
},
]
self.assertEqual(expected, parse_rsp(rsp, ["m", "n"]))


if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit 2a0f24e

Please sign in to comment.