tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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()