3
3
# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT
4
4
5
5
# ACVP client for ML-KEM
6
- #
7
- # Processes json files from
8
- # https://github.com/usnistgov/ACVP-Server/blob/master/gen-val/json-files
9
- #
6
+ # See https://pages.nist.gov/ACVP/draft-celi-acvp-ml-kem.html and
7
+ # https://github.com/usnistgov/ACVP-Server/tree/master/gen-val/json-files
10
8
# Invokes `acvp_mlkem{lvl}` under the hood.
11
9
12
10
import argparse
13
11
import os
14
12
import json
15
13
import sys
16
14
import subprocess
15
+ import urllib .request
16
+ from pathlib import Path
17
17
18
18
# Check if we need to use a wrapper for execution (e.g. QEMU)
19
19
exec_prefix = os .environ .get ("EXEC_WRAPPER" , "" )
20
20
exec_prefix = exec_prefix .split (" " ) if exec_prefix != "" else []
21
21
22
- acvp_dir = "test/acvp_data"
23
- acvp_jsons = [
24
- (
25
- f"{ acvp_dir } /acvp_v1.1.0.38_keyGen_prompt.json" ,
26
- f"{ acvp_dir } /acvp_v1.1.0.38_keyGen_expectedResults.json" ,
27
- ),
28
- (
29
- f"{ acvp_dir } /acvp_v1.1.0.39_keyGen_prompt.json" ,
30
- f"{ acvp_dir } /acvp_v1.1.0.39_keyGen_expectedResults.json" ,
31
- ),
32
- (
33
- f"{ acvp_dir } /acvp_v1.1.0.40_keyGen_prompt.json" ,
34
- f"{ acvp_dir } /acvp_v1.1.0.40_keyGen_expectedResults.json" ,
35
- ),
36
- (
37
- f"{ acvp_dir } /acvp_v1.1.0.38_encapDecap_prompt.json" ,
38
- f"{ acvp_dir } /acvp_v1.1.0.38_encapDecap_expectedResults.json" ,
39
- ),
40
- (
41
- f"{ acvp_dir } /acvp_v1.1.0.39_encapDecap_prompt.json" ,
42
- f"{ acvp_dir } /acvp_v1.1.0.39_encapDecap_expectedResults.json" ,
43
- ),
44
- (
45
- f"{ acvp_dir } /acvp_v1.1.0.40_encapDecap_prompt.json" ,
46
- f"{ acvp_dir } /acvp_v1.1.0.40_encapDecap_expectedResults.json" ,
47
- ),
48
- ]
22
+
23
+ def download_acvp_files (version = "v1.1.0.40" ):
24
+ """Download ACVP test files for the specified version if not present."""
25
+ base_url = f"https://raw.githubusercontent.com/usnistgov/ACVP-Server/{ version } /gen-val/json-files"
26
+
27
+ # Files we need to download for ML-KEM
28
+ files_to_download = [
29
+ "ML-KEM-keyGen-FIPS203/prompt.json" ,
30
+ "ML-KEM-keyGen-FIPS203/expectedResults.json" ,
31
+ "ML-KEM-encapDecap-FIPS203/prompt.json" ,
32
+ "ML-KEM-encapDecap-FIPS203/expectedResults.json" ,
33
+ ]
34
+
35
+ # Create directory structure
36
+ data_dir = Path (f"test/.acvp-data/{ version } /files" )
37
+ data_dir .mkdir (parents = True , exist_ok = True )
38
+
39
+ for file_path in files_to_download :
40
+ local_file = data_dir / file_path
41
+ local_file .parent .mkdir (parents = True , exist_ok = True )
42
+
43
+ if not local_file .exists ():
44
+ url = f"{ base_url } /{ file_path } "
45
+ print (f"Downloading { file_path } ..." , file = sys .stderr )
46
+ try :
47
+ urllib .request .urlretrieve (url , local_file )
48
+ # Verify the file is valid JSON
49
+ with open (local_file , "r" ) as f :
50
+ json .load (f )
51
+ except json .JSONDecodeError as e :
52
+ print (
53
+ f"Error: Downloaded file { file_path } is not valid JSON: { e } " ,
54
+ file = sys .stderr ,
55
+ )
56
+ local_file .unlink (missing_ok = True )
57
+ return False
58
+ except Exception as e :
59
+ print (f"Error downloading { file_path } : { e } " , file = sys .stderr )
60
+ local_file .unlink (missing_ok = True )
61
+ return False
62
+
63
+ return True
49
64
50
65
51
66
def loadAcvpData (prompt , expectedResults ):
@@ -59,9 +74,21 @@ def loadAcvpData(prompt, expectedResults):
59
74
return (prompt , promptData , expectedResults , expectedResultsData )
60
75
61
76
62
- def loadDefaultAcvpData ():
77
+ def loadDefaultAcvpData (version = "v1.1.0.40" ):
78
+
79
+ data_dir = f"test/.acvp-data/{ version } /files"
80
+ acvp_jsons_for_version = [
81
+ (
82
+ f"{ data_dir } /ML-KEM-keyGen-FIPS203/prompt.json" ,
83
+ f"{ data_dir } /ML-KEM-keyGen-FIPS203/expectedResults.json" ,
84
+ ),
85
+ (
86
+ f"{ data_dir } /ML-KEM-encapDecap-FIPS203/prompt.json" ,
87
+ f"{ data_dir } /ML-KEM-encapDecap-FIPS203/expectedResults.json" ,
88
+ ),
89
+ ]
63
90
acvp_data = []
64
- for prompt , expectedResults in acvp_jsons :
91
+ for prompt , expectedResults in acvp_jsons_for_version :
65
92
acvp_data .append (loadAcvpData (prompt , expectedResults ))
66
93
return acvp_data
67
94
@@ -280,7 +307,7 @@ def runTest(data, output):
280
307
info ("ALL GOOD!" )
281
308
282
309
283
- def test (prompt , expected , output ):
310
+ def test (prompt , expected , output , version = "v1.1.0.40" ):
284
311
assert (
285
312
prompt is not None or output is None
286
313
), "cannot produce output if there is no input"
@@ -293,8 +320,8 @@ def test(prompt, expected, output):
293
320
if prompt is not None :
294
321
data = [loadAcvpData (prompt , expected )]
295
322
else :
296
- # otherwise, load default data from acvp_data
297
- data = loadDefaultAcvpData ()
323
+ # load data from downloaded files
324
+ data = loadDefaultAcvpData (version )
298
325
299
326
runTest (data , output )
300
327
@@ -312,5 +339,20 @@ def test(prompt, expected, output):
312
339
parser .add_argument (
313
340
"-o" , "--output" , help = "Path to output file in json format" , required = False
314
341
)
342
+ parser .add_argument (
343
+ "--version" ,
344
+ "-v" ,
345
+ default = "v1.1.0.40" ,
346
+ help = "ACVP test vector version (default: v1.1.0.40)" ,
347
+ )
315
348
args = parser .parse_args ()
316
- test (args .prompt , args .expected , args .output )
349
+
350
+ if args .prompt is None :
351
+ print (f"Using ACVP test vectors version { args .version } " , file = sys .stderr )
352
+
353
+ # Download files if needed
354
+ if not download_acvp_files (args .version ):
355
+ print ("Failed to download ACVP test files" , file = sys .stderr )
356
+ sys .exit (1 )
357
+
358
+ test (args .prompt , args .expected , args .output , args .version )
0 commit comments