|
| 1 | +#!/usr/bin/env python3 |
1 | 2 | # Copyright (c) The mldsa-native project authors |
2 | 3 | # SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT |
3 | 4 |
|
|
8 | 9 | # |
9 | 10 | # Invokes `acvp_mldsa{lvl}` under the hood. |
10 | 11 |
|
| 12 | +import argparse |
11 | 13 | import os |
12 | 14 | import json |
13 | 15 | import sys |
14 | 16 | import subprocess |
| 17 | +import urllib.request |
| 18 | +from pathlib import Path |
15 | 19 |
|
16 | 20 | # Check if we need to use a wrapper for execution (e.g. QEMU) |
17 | 21 | exec_prefix = os.environ.get("EXEC_WRAPPER", "") |
18 | 22 | exec_prefix = [exec_prefix] if exec_prefix != "" else [] |
19 | 23 |
|
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" |
24 | 24 |
|
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" |
27 | 28 |
|
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 | + ] |
30 | 35 |
|
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 |
33 | 84 |
|
34 | 85 |
|
35 | 86 | def err(msg, **kwargs): |
@@ -173,14 +224,62 @@ def run_sigVer_test(tg, tc): |
173 | 224 | info("OK") |
174 | 225 |
|
175 | 226 |
|
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() |
179 | 279 |
|
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) |
183 | 284 |
|
184 | | -for tg in acvp_sigVer_data["testGroups"]: |
185 | | - for tc in tg["tests"]: |
186 | | - run_sigVer_test(tg, tc) |
| 285 | +test(args.version) |
0 commit comments