Skip to content

Commit 18c8396

Browse files
willieyzmkannwischer
authored andcommitted
ACVP: Download testvectors transparently on-the-fly
- This commit is ported from mlkem-native PR#1119. - Due to differences between the ACVP test structures of mldsa-native and mlkem-native, several modifications have been made: - Replaced the `prompt` and `expectedResult` ACVP tests with a single `internalProjection` test. - Removed the logic related to ACVT checking, as mldsa-native does not currently use ACVT testing. - The following is the original commit message: - This commit changes the acvp_client.py to automatically download the required testvectors file for a given version of the ACVP Server. - The main benefit of this change is that changes to the testvectors (which have been happening much more frequent than I expected) do not pollute our git history to the same extend anymore. - The local copies of the ACVP files are removed.Note that before we were testing v1.0.0.40, v1.0.0.39, and v1.0.0.38. - This commit changes that to only use v1.0.0.40 by default. - Additional versions can be tested by calling the acvp_client.py directly with the --version flag. This will be added to CI in the next commit. Signed-off-by: willieyz <[email protected]>
1 parent cede4fa commit 18c8396

File tree

6 files changed

+120
-6680
lines changed

6 files changed

+120
-6680
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
test/build
44
.direnv
5+
# Downloaded ACVP test data
6+
test/.acvp-data/

test/acvp_client.py

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/usr/bin/env python3
12
# Copyright (c) The mldsa-native project authors
23
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
34

@@ -8,28 +9,78 @@
89
#
910
# Invokes `acvp_mldsa{lvl}` under the hood.
1011

12+
import argparse
1113
import os
1214
import json
1315
import sys
1416
import subprocess
17+
import urllib.request
18+
from pathlib import Path
1519

1620
# Check if we need to use a wrapper for execution (e.g. QEMU)
1721
exec_prefix = os.environ.get("EXEC_WRAPPER", "")
1822
exec_prefix = [exec_prefix] if exec_prefix != "" else []
1923

20-
acvp_dir = "test/acvp_data"
21-
acvp_keygen_json = f"{acvp_dir}/acvp_keygen_internalProjection.json"
22-
acvp_sigGen_json = f"{acvp_dir}/acvp_sigGen_internalProjection.json"
23-
acvp_sigVer_json = f"{acvp_dir}/acvp_sigVer_internalProjection.json"
2424

25-
with open(acvp_keygen_json, "r") as f:
26-
acvp_keygen_data = json.load(f)
25+
def download_acvp_files(version="v1.1.0.40"):
26+
"""Download ACVP test files for the specified version if not present."""
27+
base_url = f"https://raw.githubusercontent.com/usnistgov/ACVP-Server/{version}/gen-val/json-files"
2728

28-
with open(acvp_sigGen_json, "r") as f:
29-
acvp_sigGen_data = json.load(f)
29+
# Files we need to download for ML-KEM
30+
files_to_download = [
31+
"ML-DSA-keyGen-FIPS204/internalProjection.json",
32+
"ML-DSA-sigGen-FIPS204/internalProjection.json",
33+
"ML-DSA-sigGen-FIPS204/internalProjection.json",
34+
]
3035

31-
with open(acvp_sigVer_json, "r") as f:
32-
acvp_sigVer_data = json.load(f)
36+
# Create directory structure
37+
data_dir = Path(f"test/.acvp-data/{version}/files")
38+
data_dir.mkdir(parents=True, exist_ok=True)
39+
40+
for file_path in files_to_download:
41+
local_file = data_dir / file_path
42+
local_file.parent.mkdir(parents=True, exist_ok=True)
43+
44+
if not local_file.exists():
45+
url = f"{base_url}/{file_path}"
46+
print(f"Downloading {file_path}...", file=sys.stderr)
47+
try:
48+
urllib.request.urlretrieve(url, local_file)
49+
# Verify the file is valid JSON
50+
with open(local_file, "r") as f:
51+
json.load(f)
52+
except json.JSONDecodeError as e:
53+
print(
54+
f"Error: Downloaded file {file_path} is not valid JSON: {e}",
55+
file=sys.stderr,
56+
)
57+
local_file.unlink(missing_ok=True)
58+
return False
59+
except Exception as e:
60+
print(f"Error downloading {file_path}: {e}", file=sys.stderr)
61+
local_file.unlink(missing_ok=True)
62+
return False
63+
64+
return True
65+
66+
67+
def loadAcvpData(internalProjection):
68+
with open(internalProjection, "r") as f:
69+
internalProjectionData = json.load(f)
70+
return (internalProjection, internalProjectionData)
71+
72+
73+
def loadDefaultAcvpData(version="v1.1.0.40"):
74+
data_dir = f"test/.acvp-data/{version}/files"
75+
acvp_jsons_for_version = [
76+
f"{data_dir}/ML-DSA-keyGen-FIPS204/internalProjection.json",
77+
f"{data_dir}/ML-DSA-sigGen-FIPS204/internalProjection.json",
78+
f"{data_dir}/ML-DSA-sigGen-FIPS204/internalProjection.json",
79+
]
80+
acvp_data = []
81+
for internalProjection in acvp_jsons_for_version:
82+
acvp_data.append(loadAcvpData(internalProjection))
83+
return acvp_data
3384

3485

3586
def err(msg, **kwargs):
@@ -173,14 +224,62 @@ def run_sigVer_test(tg, tc):
173224
info("OK")
174225

175226

176-
for tg in acvp_keygen_data["testGroups"]:
177-
for tc in tg["tests"]:
178-
run_keyGen_test(tg, tc)
227+
def runTestSingle(internalProjectionName, internalProjection):
228+
info(f"Running ACVP tests for {internalProjectionName}")
229+
230+
assert internalProjection["algorithm"] == "ML-DSA"
231+
assert (
232+
internalProjection["mode"] == "keyGen"
233+
or internalProjection["mode"] == "sigGen"
234+
or internalProjection["mode"] == "sigVer"
235+
)
236+
237+
# copy top level fields into the results
238+
results = internalProjection.copy()
239+
240+
results["testGroups"] = []
241+
for tg in internalProjection["testGroups"]:
242+
tgResult = {
243+
"tgId": tg["tgId"],
244+
"tests": [],
245+
}
246+
results["testGroups"].append(tgResult)
247+
for tc in tg["tests"]:
248+
if internalProjection["mode"] == "keyGen":
249+
result = run_keyGen_test(tg, tc)
250+
elif internalProjection["mode"] == "sigGen":
251+
result = run_sigGen_test(tg, tc)
252+
elif internalProjection["mode"] == "sigVer":
253+
result = run_sigVer_test(tg, tc)
254+
tgResult["tests"].append(result)
255+
256+
257+
def runTest(data):
258+
for internalProjectionName, internalProjection in data:
259+
runTestSingle(internalProjectionName, internalProjection)
260+
info("ALL GOOD!")
261+
262+
263+
def test(version="v1.1.0.40"):
264+
# load data from downloaded files
265+
data = loadDefaultAcvpData(version)
266+
267+
runTest(data)
268+
269+
270+
parser = argparse.ArgumentParser()
271+
272+
parser.add_argument(
273+
"--version",
274+
"-v",
275+
default="v1.1.0.40",
276+
help="ACVP test vector version (default: v1.1.0.40)",
277+
)
278+
args = parser.parse_args()
179279

180-
for tg in acvp_sigGen_data["testGroups"]:
181-
for tc in tg["tests"]:
182-
run_sigGen_test(tg, tc)
280+
# Download files if needed
281+
if not download_acvp_files(args.version):
282+
print("Failed to download ACVP test files", file=sys.stderr)
283+
sys.exit(1)
183284

184-
for tg in acvp_sigVer_data["testGroups"]:
185-
for tc in tg["tests"]:
186-
run_sigVer_test(tg, tc)
285+
test(args.version)

test/acvp_data/README.md

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)