Skip to content

Commit 1736882

Browse files
authored
Merge pull request #415 from pq-code-package/acvp-on-the-fly
ACVP: Download testvectors transparently on the fly (Port mlkem-native PR#1119)
2 parents cede4fa + 34bc920 commit 1736882

File tree

9 files changed

+156
-6681
lines changed

9 files changed

+156
-6681
lines changed

.github/workflows/base.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ jobs:
6363
- name: tests func
6464
run: |
6565
./scripts/tests func
66+
quickcheck-acvp:
67+
strategy:
68+
fail-fast: false
69+
matrix:
70+
external:
71+
- ${{ github.repository_owner != 'pq-code-package' }}
72+
target:
73+
- runner: pqcp-arm64
74+
name: 'aarch64'
75+
- runner: ubuntu-latest
76+
name: 'x86_64'
77+
acvp-version: [v1.1.0.38, v1.1.0.39, v1.1.0.40]
78+
exclude:
79+
- {external: true,
80+
target: {
81+
runner: pqcp-arm64,
82+
name: 'aarch64'
83+
}}
84+
name: Quickcheck ACVP (${{ matrix.target.name }}, ${{ matrix.acvp-version }})
85+
runs-on: ${{ matrix.target.runner }}
86+
steps:
87+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
88+
- name: Run ACVP test
89+
run: |
90+
./scripts/tests acvp --version ${{ matrix.acvp-version }}
6691
quickcheck_bench:
6792
strategy:
6893
fail-fast: false

.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/

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ run_func_87: func_87
6161
run_func: run_func_44 run_func_65 run_func_87
6262

6363
run_acvp: acvp
64-
python3 ./test/acvp_client.py
64+
python3 ./test/acvp_client.py $(if $(ACVP_VERSION),--version $(ACVP_VERSION))
6565

6666
func_44: $(MLDSA44_DIR)/bin/test_mldsa44
6767
$(Q)echo " FUNC ML-DSA-44: $^"

scripts/tests

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,10 @@ class Tests:
426426
if stack_flags:
427427
env_update["STACK_ANALYSIS_FLAGS"] = " ".join(stack_flags)
428428

429+
# Add ACVP version for ACVP tests
430+
if test_type == TEST_TYPES.ACVP and hasattr(self.args, "version"):
431+
env_update["ACVP_VERSION"] = self.args.version
432+
429433
env = os.environ.copy()
430434
env.update(env_update)
431435

@@ -910,6 +914,12 @@ def cli():
910914
"acvp", help="Run ACVP client", parents=[common_parser]
911915
)
912916

917+
acvp_parser.add_argument(
918+
"--version",
919+
default="v1.1.0.40",
920+
help="ACVP test vector version (default: v1.1.0.40)",
921+
)
922+
913923
# examples arguments
914924
examples_parser = cmd_subparsers.add_parser(
915925
"examples", help="Run examples", parents=[common_parser]

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.

test/acvp_data/acvp_keygen_internalProjection.json

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

0 commit comments

Comments
 (0)