genNISTTestVector.py (16208B)
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 # This Source Code Form is subject to the terms of the Mozilla Public 5 # License, v. 2.0. If a copy of the MPL was not distributed with this file, 6 # You can obtain one at http://mozilla.org/MPL/2.0/. 7 8 import json 9 import os 10 import subprocess 11 import hashlib 12 13 from cryptography.hazmat.backends import default_backend 14 from cryptography.hazmat.primitives.asymmetric import ec 15 from cryptography.hazmat.primitives import serialization 16 import binascii 17 18 script_dir = os.path.dirname(os.path.abspath(__file__)) 19 20 # Imports a JSON testvector file. 21 def import_testvector(file): 22 """Import a JSON testvector file and return an array of the contained objects.""" 23 with open(file) as f: 24 vectors = json.loads(f.read()) 25 return vectors 26 27 # Convert a test data string to a hex array. 28 def string_to_hex(string): 29 """Convert a string of hex chars to a string representing a C-format array of hex bytes.""" 30 b = bytearray.fromhex(string) 31 result = ', '.join("{:#04x}".format(x) for x in b) 32 return result 33 34 def digest(string): 35 b = bytearray.fromhex(string) 36 h = hashlib.sha3_256() 37 h.update(b) 38 return h.hexdigest() 39 40 #now split them into readable lines 41 def string_line_split(string, group_len, count, spaces): 42 result='' 43 lstring = string; 44 while (len(lstring) > count*group_len): 45 result += lstring[:count*group_len].rjust(spaces); 46 result += '\n' 47 lstring=lstring[count*group_len:]; 48 result += lstring.rjust(spaces) 49 return result 50 51 # put it together 52 def string_to_hex_array(string): 53 result="{\n" 54 result += string_line_split(' '+string_to_hex(string), 6, 12, 4) 55 result += '}' 56 return result 57 58 59 mldsa_spki={ 60 "ML-DSA-44": "30820532300b06096086480165030403110382052100", 61 "ML-DSA-65": "308207b2300b0609608648016503040312038207a100", 62 "ML-DSA-87": "30820a32300b060960864801650304031303820a2100", 63 } 64 65 mldsa_p11param={ 66 "ML-DSA-44": "CKP_ML_DSA_44", 67 "ML-DSA-65": "CKP_ML_DSA_65", 68 "ML-DSA-87": "CKP_ML_DSA_87", 69 } 70 71 class MLDSA_VERIFY(): 72 key_name='' 73 has_name=False 74 def init_global(self, group, group_result, out_defs): 75 paramSet= group['parameterSet'] 76 self.paramSet=paramSet 77 if not paramSet in mldsa_spki: 78 return False 79 if 'pk' in group: 80 rawkey= group['pk'] 81 paramSet= group['parameterSet'] 82 key_name = "kPubKey"+str(group['tgId']) 83 key=mldsa_spki[paramSet]+rawkey 84 out_defs.append('// Key Type'+paramSet+'\n') 85 out_defs.append('static const std::vector<uint8_t> ' + key_name + string_to_hex_array(key) + ';\n\n') 86 self.key_name=key_name 87 self.has_name=True 88 return True 89 self.has_name=False 90 # only fetch the test types that we support: external 91 # pure, and externalMu == false. 92 # we can adjust these when we and externalMu support and 93 # MLDSA Hash support (if we ever do) 94 if 'signatureInterface' in group: 95 if group['signatureInterface'] != "external": 96 return False 97 if 'preHash' in group: 98 if group['preHash'] != "pure": 99 return False 100 if 'externalMu' in group: 101 if group['externalMu']: 102 return False 103 return True 104 def format_testcase(self, testcase, testcase_result, out_defs): 105 key=mldsa_spki[self.paramSet]+testcase['pk'] 106 result = '\n// {}\n'.format(self.paramSet) 107 result = '// tcID: {}\n'.format(testcase['tcId']) 108 result += '{{{},\n'.format(testcase['tcId']) 109 result += '// signature\n{},\n'.format(string_to_hex_array(testcase['signature'])) 110 if self.has_name: 111 result += '{},\n'.format(self.key_name) 112 else: 113 result += '// pubkey\n{},\n'.format(string_to_hex_array(key)) 114 if 'context' in testcase: 115 result += '// context\n{},\n'.format(string_to_hex_array(testcase['context'])) 116 else: 117 result += '// context\n{{}},\n' 118 result += '// message\n{},\n'.format(string_to_hex_array(testcase['message'])) 119 result += '{}}},\n'.format(str(testcase_result['testPassed']).lower()) 120 display = 'tcID {}: {} '.format(testcase['tcId'],self.paramSet) 121 display += 'key({}) message({}) '.format(len(key)/2,len(testcase['message'])/2) 122 display += 'signature({}) ctx({}) '.format(len(testcase['signature'])/2,len(testcase['context'])/2,) 123 display += '{}'.format(testcase_result['testPassed']) 124 print(display) 125 return result 126 127 class MLDSA_KEYGEN(): 128 def init_global(self, group, group_result, out_defs): 129 paramSet= group['parameterSet'] 130 self.paramSet = paramSet 131 return paramSet in mldsa_p11param 132 def format_testcase(self, testcase, testcase_result, out_defs): 133 seed=testcase['seed'] 134 pubk=testcase_result['pk'] 135 privk=testcase_result['sk'] 136 result = '\n// {}\n'.format(self.paramSet) 137 result = '// tcID: {}\n'.format(testcase['tcId']) 138 result += '{{{},\n'.format(testcase['tcId']) 139 result += '{},\n'.format(mldsa_p11param[self.paramSet]) 140 result += '//seed\n{},\n'.format(string_to_hex_array(seed)) 141 result += '//raw pubkey\n{},\n'.format(string_to_hex_array(pubk)) 142 result += '//raw privkey\n{}}},\n'.format(string_to_hex_array(privk)) 143 display = 'tcID {}: {} '.format(testcase['tcId'],self.paramSet) 144 display += 'seed({}) pubk({}) '.format(len(seed)/2,len(pubk)/2) 145 display += 'privk({})'.format(len(privk)/2) 146 print(display) 147 return result 148 149 mlkem_freebl_param={ 150 "ML-KEM-768": "params_ml_kem768", 151 "ML-KEM-1024": "params_ml_kem1024", 152 } 153 154 mlkem_freebl_test_param={ 155 "ML-KEM-768": "params_ml_kem768_test_mode", 156 "ML-KEM-1024": "params_ml_kem1024_test_mode", 157 } 158 159 mlkem_prefix={ 160 "ML-KEM-768": "MlKem768", 161 "ML-KEM-1024": "MlKem1024", 162 } 163 164 class MLKEM_KEYGEN(): 165 def init_global(self, group, group_result, out_defs): 166 paramSet= group['parameterSet'] 167 self.paramSet = paramSet 168 return paramSet in mlkem_freebl_param 169 def format_testcase(self, testcase, testcase_result, out_defs): 170 seed=testcase['d']+testcase['z'] 171 pubk=testcase_result['ek'] 172 privk=testcase_result['dk'] 173 pubkDigest=digest(pubk) 174 privkDigest=digest(privk) 175 result = '\n// {}\n'.format(self.paramSet) 176 result = '// tcID: {}\n'.format(testcase['tcId']) 177 result += '{{{},\n'.format(testcase['tcId']) 178 result += '{},\n'.format(mlkem_freebl_param[self.paramSet]) 179 result += '//seed\n{},\n'.format(string_to_hex_array(seed)) 180 result += '//publicKeyDigest\n{},\n'.format(string_to_hex_array(pubkDigest)) 181 result += '//privateKeyDigest\n{}}},\n'.format(string_to_hex_array(privkDigest)) 182 display = 'tcID {}: {} '.format(testcase['tcId'],self.paramSet) 183 display += 'seed({}) pubk({}) '.format(len(seed)/2,len(pubk)/2) 184 display += 'privk({})'.format(len(privk)/2) 185 print(display) 186 return result 187 188 class MLKEM_ENCAP(): 189 key_name='' 190 has_name=False 191 def init_global(self, group, group_result, out_defs): 192 paramSet= group['parameterSet'] 193 self.paramSet=paramSet 194 if not paramSet in mlkem_freebl_test_param: 195 return False 196 if group['function'] != 'encapsulation': 197 return False 198 return True 199 def format_testcase(self, testcase, testcase_result, out_defs): 200 key=testcase['ek'] 201 result = '\n// {}\n'.format(self.paramSet) 202 result = '// tcID: {}\n'.format(testcase['tcId']) 203 result += '{{{},\n'.format(testcase['tcId']) 204 result += '{},\n'.format(mlkem_freebl_test_param[self.paramSet]) 205 result += '// entropy\n{},\n'.format(string_to_hex_array(testcase['m'])) 206 result += '// publicKey\n{},\n'.format(string_to_hex_array(testcase['ek'])) 207 cipherTextDigest=digest(testcase_result['c']) 208 result += '// cipherTextDigest\n{},\n'.format(string_to_hex_array(cipherTextDigest)) 209 result += '// secret\n{},\ntrue}},\n'.format(string_to_hex_array(testcase_result['k'])) 210 display = 'tcID {}: {} '.format(testcase['tcId'],self.paramSet) 211 display += 'key({}) encapsulate'.format(len(key)/2) 212 print(display) 213 return result 214 215 class MLKEM_DECAP(): 216 key_name='' 217 has_name=False 218 def init_global(self, group, group_result, out_defs): 219 paramSet= group['parameterSet'] 220 self.paramSet=paramSet 221 if not paramSet in mlkem_freebl_test_param: 222 return False 223 if group['function'] != 'decapsulation': 224 return False 225 return True 226 def format_testcase(self, testcase, testcase_result, out_defs): 227 result = '\n// {}\n'.format(self.paramSet) 228 result = '// tcID: {}\n'.format(testcase['tcId']) 229 result += '{{{},\n'.format(testcase['tcId']) 230 result += '{},\n'.format(mlkem_freebl_test_param[self.paramSet]) 231 result += '// privateKey\n{},\n'.format(string_to_hex_array(testcase['dk'])) 232 result += '// ciphertext\n{},\n'.format(string_to_hex_array(testcase['c'])) 233 result += '// secret\n{},\ntrue}},\n'.format(string_to_hex_array(testcase_result['k'])) 234 display = 'tcID {}: {} decapsulate'.format(testcase['tcId'],self.paramSet) 235 print(display) 236 return result 237 238 def matchID(_id, source, target): 239 for i in target: 240 if i[_id] == source[_id]: 241 return i 242 return {} 243 244 def generate_vectors_file(params): 245 """ 246 Generate and store a .h-file with test vectors for one test. 247 248 params -- Dictionary with parameters for test vector generation for the desired test. 249 """ 250 251 cases = import_testvector(os.path.join(script_dir, params['source_dir'] + params['prompt_file'])) 252 result = import_testvector(os.path.join(script_dir, params['source_dir'] + params['result_file'])) 253 254 base_vectors = "" 255 if 'base' in params: 256 with open(os.path.join(script_dir, params['base'])) as base: 257 base_vectors = base.read() 258 base_vectors += "\n\n" 259 260 header = standard_params['license'] 261 header += "\n" 262 header += standard_params['top_comment'] 263 header += "\n" 264 header += "#ifndef " + params['section'] + "\n" 265 header += "#define " + params['section'] + "\n" 266 header += "\n" 267 268 for include in standard_params['includes']: 269 header += "#include " + include + "\n" 270 271 header += "\n" 272 273 if 'includes' in params: 274 for include in params['includes']: 275 header += "#include " + include + "\n" 276 header += "\n" 277 278 shared_defs = [] 279 vectors_file = base_vectors + params['array_init'] 280 281 for group in cases['testGroups']: 282 group_result = matchID('tgId', group, result['testGroups']); 283 if (not params['formatter'].init_global(group, group_result, shared_defs)): 284 continue; 285 for test in group['tests']: 286 test_result = matchID('tcId', test, group_result['tests']); 287 vectors_file += params['formatter'].format_testcase(test, test_result, shared_defs) 288 289 vectors_file = vectors_file[:params['crop_size_end']] + '\n};\n\n' 290 vectors_file += "#endif // " + params['section'] + '\n' 291 292 with open(os.path.join(script_dir, params['target']), 'w') as target: 293 target.write(header) 294 for definition in shared_defs: 295 target.write(definition) 296 target.write(vectors_file) 297 298 299 standard_params = { 300 'includes': ['"testvectors_base/test-structs.h"'], 301 'license': 302 """/* vim: set ts=2 et sw=2 tw=80: */ 303 /* This Source Code Form is subject to the terms of the Mozilla Public 304 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 305 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 306 """, 307 308 'top_comment': 309 """/* This file is generated from sources in nss/gtests/common/wycheproof 310 * automatically and should not be touched manually. 311 * Generation is trigged by calling python3 genTestVectors.py */ 312 """ 313 } 314 315 # Parameters that describe the generation of a testvector file for each supoorted test. 316 # source -- relative path to the wycheproof JSON source file with testvectors. 317 # base -- relative path to non-wycheproof vectors. 318 # target -- relative path to where the finished .h-file is written. 319 # array_init -- string to initialize the c-header style array of testvectors. 320 # formatter -- the test case formatter class to be used for this test. 321 # crop_size_end -- number of characters removed from the end of the last generated test vector to close the array definition. 322 # section -- name of the section 323 # comment -- additional comments to add to the file just before definition of the test vector array. 324 325 ml_dsa_verify_params = { 326 'source_dir': 'source_vectors/', 327 'test_name': 'ML-DSA-sigVer-FIPS204', 328 'tag': 'v1.1.0.40', 329 'prompt_file': 'ml_dsa_verify_prompt.json', 330 'result_file': 'ml_dsa_verify_result.json', 331 'target': '../testvectors/ml-dsa-verify-vectors.h', 332 'array_init': 'const MlDsaVerifyTestVector kMLDsaNISTVerifyVectors[] = {\n', 333 'formatter' : MLDSA_VERIFY(), 334 'crop_size_end': -2, 335 'section': 'mldsa_verify_vectors_h__', 336 'comment' : '' 337 } 338 339 ml_dsa_keygen_params = { 340 'source_dir': 'source_vectors/', 341 'test_name': 'ML-DSA-keyGen-FIPS204', 342 'tag': 'v1.1.0.40', 343 'prompt_file': 'ml_dsa_keygen_prompt.json', 344 'result_file': 'ml_dsa_keygen_result.json', 345 'target': '../testvectors/ml-dsa-keygen-vectors.h', 346 'array_init': 'const MlDsaKeyGenTestVector kMLDsaNISTKeyGenVectors[] = {\n', 347 'formatter' : MLDSA_KEYGEN(), 348 'crop_size_end': -2, 349 'section': 'mldsa_keygen_vectors_h__', 350 'comment' : '' 351 } 352 353 ml_kem_decap_params = { 354 'source_dir': 'source_vectors/', 355 'test_name': 'ML-KEM-encapDecap-FIPS203', 356 'tag': 'v1.1.0.40', 357 'prompt_file': 'ml_kem_encap_decap_prompt.json', 358 'result_file': 'ml_kem_encap_decap_result.json', 359 'target': '../testvectors/ml-kem-decap-vectors.h', 360 'array_init': 'const std::vector<MlKemDecapTestVector> MlKemDecapTests = {\n', 361 'formatter' : MLKEM_DECAP(), 362 'crop_size_end': -2, 363 'section': 'mlkem_decap_vectors_h__', 364 'comment' : '' 365 } 366 367 ml_kem_encap_params = { 368 'source_dir': 'source_vectors/', 369 'test_name': 'ML-KEM-encapDecap-FIPS203', 370 'tag': 'v1.1.0.40', 371 'prompt_file': 'ml_kem_encap_decap_prompt.json', 372 'result_file': 'ml_kem_encap_decap_result.json', 373 'target': '../testvectors/ml-kem-encap-vectors.h', 374 'array_init': 'const std::vector<MlKemEncapTestVector> MlKemEncapTests = {\n', 375 'formatter' : MLKEM_ENCAP(), 376 'crop_size_end': -2, 377 'section': 'mlkem_encap_vectors_h__', 378 'comment' : '' 379 } 380 381 ml_kem_keygen_params = { 382 'source_dir': 'source_vectors/', 383 'test_name': 'ML-KEM-keyGen-FIPS203', 384 'tag': 'v1.1.0.40', 385 'prompt_file': 'ml_kem_keygen_prompt.json', 386 'result_file': 'ml_kem_keygen_result.json', 387 'target': '../testvectors/ml-kem-keygen-vectors.h', 388 'array_init': 'const std::vector<MlKemKeyGenTestVector> MlKemKeyGenTests = {\n', 389 'formatter' : MLKEM_KEYGEN(), 390 'crop_size_end': -2, 391 'section': 'mlkem_keygen_vectors_h__', 392 'comment' : '' 393 } 394 395 396 def update_tests(tests): 397 398 remote_base = "https://raw.githubusercontent.com/usnistgov/ACVP-Server/refs/tags/" 399 for test in tests: 400 remote = remote_base+test['tag']+"/gen-val/json-files/"+test['test_name']+"/" 401 subprocess.check_call(['wget', remote+"/prompt.json", '-O', 402 script_dir+'/'+test['source_dir']+test['prompt_file']]) 403 subprocess.check_call(['wget', remote+"/expectedResults.json", '-O', 404 script_dir+'/'+test['source_dir']+test['result_file']]) 405 406 def generate_test_vectors(): 407 """Generate C-header files for all supported tests.""" 408 all_tests = [ ml_kem_keygen_params, ml_kem_encap_params, ml_kem_decap_params ] 409 #all_tests = [ml_kem_keygen_params, ml_kem_encap_params, ml_kem_decap_params ] 410 update_tests(all_tests) 411 for test in all_tests: 412 generate_vectors_file(test) 413 414 def main(): 415 generate_test_vectors() 416 417 if __name__ == '__main__': 418 main()