1#!/usr/bin/env python
2# SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
3# SPDX-License-Identifier: Apache-2.0
4import argparse
5import hashlib
6import hmac
7import json
8import os
9import struct
10import subprocess
11import sys
12
13from cryptography.hazmat.backends import default_backend
14from cryptography.hazmat.primitives import serialization
15from cryptography.hazmat.primitives.asymmetric import rsa
16from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
17from cryptography.utils import int_to_bytes
18
19try:
20    import nvs_partition_gen as nvs_gen
21except ImportError:
22    idf_path = os.getenv('IDF_PATH')
23    if not idf_path or not os.path.exists(idf_path):
24        raise Exception('IDF_PATH not found')
25    sys.path.insert(0, os.path.join(idf_path, 'components', 'nvs_flash', 'nvs_partition_generator'))
26    import nvs_partition_gen as nvs_gen
27
28# Check python version is proper or not to avoid script failure
29assert sys.version_info >= (3, 6, 0), 'Python version too low.'
30
31esp_ds_data_dir = 'esp_ds_data'
32# hmac_key_file is generated when HMAC_KEY is calculated, it is used when burning HMAC_KEY to efuse
33hmac_key_file = esp_ds_data_dir + '/hmac_key.bin'
34# csv and bin filenames are default filenames for nvs partition files created with this script
35csv_filename = esp_ds_data_dir + '/pre_prov.csv'
36bin_filename = esp_ds_data_dir + '/pre_prov.bin'
37expected_json_path = os.path.join('build', 'config', 'sdkconfig.json')
38# Targets supported by the script
39supported_targets = {'esp32s2', 'esp32c3', 'esp32s3'}
40supported_key_size = {'esp32s2':[1024, 2048, 3072, 4096], 'esp32c3':[1024, 2048, 3072], 'esp32s3':[1024, 2048, 3072, 4096]}
41
42
43# @return
44#       on success  idf_target - value of the IDF_TARGET read from build/config/sdkconfig.json
45#       on failure  None
46def get_idf_target():
47    if os.path.exists(expected_json_path):
48        sdkconfig = json.load(open(expected_json_path))
49        idf_target_read = sdkconfig['IDF_TARGET']
50        return idf_target_read
51    else:
52        print('ERROR: IDF_TARGET has not been set for the supported targets,'
53              "\nplase execute command \"idf.py set-target {TARGET}\" in the example directory")
54        return None
55
56
57def load_privatekey(key_file_path, password=None):
58    key_file = open(key_file_path, 'rb')
59    key = key_file.read()
60    key_file.close()
61    return serialization.load_pem_private_key(key, password=password, backend=default_backend())
62
63
64def number_as_bytes(number, pad_bits=None):
65    """
66    Given a number, format as a little endian array of bytes
67    """
68    result = int_to_bytes(number)[::-1]
69    while pad_bits is not None and len(result) < (pad_bits // 8):
70        result += b'\x00'
71    return result
72
73
74# @return
75#       c               : ciphertext_c
76#       iv              : initialization vector
77#       key_size        : key size of the RSA private key in bytes.
78# @input
79#       privkey         : path to the RSA private key
80#       priv_key_pass   : path to the RSA privaete key password
81#       hmac_key        : HMAC key value ( to calculate DS params)
82#       idf_target      : The target chip for the script (e.g. esp32s2, esp32c3, esp32s3)
83# @info
84#       The function calculates the encrypted private key parameters.
85#       Consult the DS documentation (available for the ESP32-S2) in the esp-idf programming guide for more details about the variables and calculations.
86def calculate_ds_parameters(privkey, priv_key_pass, hmac_key, idf_target):
87    private_key = load_privatekey(privkey, priv_key_pass)
88    if not isinstance(private_key, rsa.RSAPrivateKey):
89        print('ERROR: Only RSA private keys are supported')
90        sys.exit(-1)
91    if hmac_key is None:
92        print('ERROR: hmac_key cannot be None')
93        sys.exit(-2)
94
95    priv_numbers = private_key.private_numbers()
96    pub_numbers = private_key.public_key().public_numbers()
97    Y = priv_numbers.d
98    M = pub_numbers.n
99    key_size = private_key.key_size
100    if key_size not in supported_key_size[idf_target]:
101        print('ERROR: Private key size {0} not supported for the target {1},\nthe supported key sizes are {2}'
102              .format(key_size, idf_target, str(supported_key_size[idf_target])))
103        sys.exit(-1)
104
105    iv = os.urandom(16)
106
107    rr = 1 << (key_size * 2)
108    rinv = rr % pub_numbers.n
109    mprime = - rsa._modinv(M, 1 << 32)
110    mprime &= 0xFFFFFFFF
111    length = key_size // 32 - 1
112
113    # get max supported key size for the respective target
114    max_len = max(supported_key_size[idf_target])
115    aes_key = hmac.HMAC(hmac_key, b'\xFF' * 32, hashlib.sha256).digest()
116
117    md_in = number_as_bytes(Y, max_len) + \
118        number_as_bytes(M, max_len) + \
119        number_as_bytes(rinv, max_len) + \
120        struct.pack('<II', mprime, length) + \
121        iv
122
123    # expected_len = max_len_Y + max_len_M + max_len_rinv + (mprime + length packed (8 bytes))+ iv (16 bytes)
124    expected_len = (max_len / 8) * 3 + 8 + 16
125    assert len(md_in) == expected_len
126    md = hashlib.sha256(md_in).digest()
127    # In case of ESP32-S2
128    # Y4096 || M4096 || Rb4096 || M_prime32 || LENGTH32 || MD256 || 0x08*8
129    # In case of ESP32-C3
130    # Y3072 || M3072 || Rb3072 || M_prime32 || LENGTH32 || MD256 || 0x08*8
131    p = number_as_bytes(Y, max_len) + \
132        number_as_bytes(M, max_len) + \
133        number_as_bytes(rinv, max_len) + \
134        md + \
135        struct.pack('<II', mprime, length) + \
136        b'\x08' * 8
137
138    # expected_len = max_len_Y + max_len_M + max_len_rinv + md (32 bytes) + (mprime + length packed (8bytes)) + padding (8 bytes)
139    expected_len = (max_len / 8) * 3 + 32 + 8 + 8
140    assert len(p) == expected_len
141
142    cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
143    encryptor = cipher.encryptor()
144    c = encryptor.update(p) + encryptor.finalize()
145    return c, iv, key_size
146
147
148# @info
149#       The function makes use of the "espefuse.py" script to read the efuse summary
150def efuse_summary(args, idf_target):
151    os.system('python $IDF_PATH/components/esptool_py/esptool/espefuse.py --chip {0} -p {1} summary'.format(idf_target, (args.port)))
152
153
154# @info
155#       The function makes use of the "espefuse.py" script to burn the HMAC key on the efuse.
156def efuse_burn_key(args, idf_target):
157    # In case of a development (default) usecase we disable the read protection.
158    key_block_status = '--no-read-protect'
159
160    if args.production is True:
161        # Whitespace character will have no additional effect on the command and
162        # read protection will be enabled as the default behaviour of the command
163        key_block_status = ' '
164
165    os.system('python $IDF_PATH/components/esptool_py/esptool/espefuse.py --chip {0} -p {1} burn_key '
166              '{2} {3} HMAC_DOWN_DIGITAL_SIGNATURE {4}'
167              .format((idf_target), (args.port), ('BLOCK_KEY' + str(args.efuse_key_id)), (hmac_key_file), (key_block_status)))
168
169
170# @info
171#       Generate a custom csv file of encrypted private key parameters.
172#       The csv file is required by the nvs_partition_generator utility to create the nvs partition.
173def generate_csv_file(c, iv, hmac_key_id, key_size, csv_file):
174
175    with open(csv_file, 'wt', encoding='utf8') as f:
176        f.write('# This is a generated csv file containing required parameters for the Digital Signature operation\n')
177        f.write('key,type,encoding,value\nesp_ds_ns,namespace,,\n')
178        f.write('esp_ds_c,data,hex2bin,%s\n' % (c.hex()))
179        f.write('esp_ds_iv,data,hex2bin,%s\n' % (iv.hex()))
180        f.write('esp_ds_key_id,data,u8,%d\n' % (hmac_key_id))
181        f.write('esp_ds_rsa_len,data,u16,%d\n' % (key_size))
182
183
184class DefineArgs(object):
185    def __init__(self, attributes):
186        for key, value in attributes.items():
187            self.__setattr__(key, value)
188
189
190# @info
191#       This function uses the nvs_partition_generater utility
192#       to generate the nvs partition of the encrypted private key parameters.
193def generate_nvs_partition(input_filename, output_filename):
194
195    nvs_args = DefineArgs({
196        'input': input_filename,
197        'outdir': os.getcwd(),
198        'output': output_filename,
199        'size': hex(0x3000),
200        'version': 2,
201        'keyfile':None,
202    })
203
204    nvs_gen.generate(nvs_args, is_encr_enabled=False, encr_key=None)
205
206
207# @return
208#         The json formatted summary of the efuse.
209def get_efuse_summary_json(args, idf_target):
210    _efuse_summary = None
211    try:
212        _efuse_summary = subprocess.check_output(('python $IDF_PATH/components/esptool_py/esptool/espefuse.py '
213                                                  '--chip {0} -p {1} summary --format json'.format(idf_target, (args.port))), shell=True)
214    except subprocess.CalledProcessError as e:
215        print((e.output).decode('UTF-8'))
216        sys.exit(-1)
217
218    _efuse_summary = _efuse_summary.decode('UTF-8')
219    # Remove everything before actual json data from efuse_summary command output.
220    _efuse_summary = _efuse_summary[_efuse_summary.find('{'):]
221    try:
222        _efuse_summary_json = json.loads(_efuse_summary)
223    except json.JSONDecodeError:
224        print('ERROR: failed to parse the json output')
225        sys.exit(-1)
226    return _efuse_summary_json
227
228
229# @return
230#       on success: 256 bit HMAC key present in the given key_block (args.efuse_key_id)
231#       on failure: None
232# @info
233#       This function configures the provided efuse key_block.
234#       If the provided efuse key_block is empty the function generates a new HMAC key and burns it in the efuse key_block.
235#       If the key_block already contains a key the function reads the key from the efuse key_block
236def configure_efuse_key_block(args, idf_target):
237    efuse_summary_json = get_efuse_summary_json(args, idf_target)
238    key_blk = 'BLOCK_KEY' + str(args.efuse_key_id)
239    key_purpose = 'KEY_PURPOSE_' + str(args.efuse_key_id)
240
241    kb_writeable = efuse_summary_json[key_blk]['writeable']
242    kb_readable = efuse_summary_json[key_blk]['readable']
243    hmac_key_read = None
244
245    # If the efuse key block is writable (empty) then generate and write
246    # the new hmac key and check again
247    # If the efuse key block is not writable (already contains a key) then check if it is redable
248    if kb_writeable is True:
249        print('Provided key block (KEY BLOCK %1d) is writable\n Generating a new key and burning it in the efuse..\n' % (args.efuse_key_id))
250
251        new_hmac_key = os.urandom(32)
252        with open(hmac_key_file, 'wb') as key_file:
253            key_file.write(new_hmac_key)
254        # Burn efuse key
255        efuse_burn_key(args, idf_target)
256        if args.production is False:
257            # Read fresh summary of the efuse to read the key value from efuse.
258            # If the key read from efuse matches with the key generated
259            # on host then burn_key operation was successfull
260            new_efuse_summary_json = get_efuse_summary_json(args, idf_target)
261            hmac_key_read = new_efuse_summary_json[key_blk]['value']
262            print(hmac_key_read)
263            hmac_key_read = bytes.fromhex(hmac_key_read)
264            if new_hmac_key == hmac_key_read:
265                print('Key was successfully written to the efuse (KEY BLOCK %1d)' % (args.efuse_key_id))
266            else:
267                print('ERROR: Failed to burn the hmac key to efuse (KEY BLOCK %1d),'
268                      '\nPlease execute the script again using a different key id' % (args.efuse_key_id))
269                return None
270        else:
271            new_efuse_summary_json = get_efuse_summary_json(args, idf_target)
272            if new_efuse_summary_json[key_purpose]['value'] != 'HMAC_DOWN_DIGITAL_SIGNATURE':
273                print('ERROR: Failed to verify the key purpose of the key block{})'.format(args.efuse_key_id))
274                return None
275            hmac_key_read = new_hmac_key
276    else:
277        # If the efuse key block is redable, then read the key from efuse block and use it for encrypting the RSA private key parameters.
278        # If the efuse key block is not redable or it has key purpose set to a different
279        # value than "HMAC_DOWN_DIGITAL_SIGNATURE" then we cannot use it for DS operation
280        if kb_readable is True:
281            if efuse_summary_json[key_purpose]['value'] == 'HMAC_DOWN_DIGITAL_SIGNATURE':
282                print('Provided efuse key block (KEY BLOCK %1d) already contains a key with key_purpose=HMAC_DOWN_DIGITAL_SIGNATURE,'
283                      '\nusing the same key for encrypting the private key data...\n' % (args.efuse_key_id))
284                hmac_key_read = efuse_summary_json[key_blk]['value']
285                hmac_key_read = bytes.fromhex(hmac_key_read)
286                if args.keep_ds_data is True:
287                    with open(hmac_key_file, 'wb') as key_file:
288                        key_file.write(hmac_key_read)
289            else:
290                print('ERROR: Provided efuse key block ((KEY BLOCK %1d)) contains a key with key purpose different'
291                      'than HMAC_DOWN_DIGITAL_SIGNATURE,\nplease execute the script again with a different value of the efuse key id.' % (args.efuse_key_id))
292                return None
293        else:
294            print('ERROR: Provided efuse key block (KEY BLOCK %1d) is not readable and writeable,'
295                  '\nplease execute the script again with a different value of the efuse key id.' % (args.efuse_key_id))
296            return None
297
298    # Return the hmac key burned into the efuse
299    return hmac_key_read
300
301
302def cleanup(args):
303    if args.keep_ds_data is False:
304        if os.path.exists(hmac_key_file):
305            os.remove(hmac_key_file)
306        if os.path.exists(csv_filename):
307            os.remove(csv_filename)
308
309
310def main():
311    parser = argparse.ArgumentParser(description='''Generate an HMAC key and burn it in the desired efuse key block (required for Digital Signature),
312    Generates an NVS partition containing the encrypted private key parameters from the client private key.
313            ''')
314
315    parser.add_argument(
316        '--private-key',
317        dest='privkey',
318        default='client.key',
319        metavar='relative/path/to/client-priv-key',
320        help='relative path to client private key')
321
322    parser.add_argument(
323        '--pwd', '--password',
324        dest='priv_key_pass',
325        metavar='[password]',
326        help='the password associated with the private key')
327
328    parser.add_argument(
329        '--summary',
330        dest='summary',action='store_true',
331        help='Provide this option to print efuse summary of the chip')
332
333    parser.add_argument(
334        '--efuse_key_id',
335        dest='efuse_key_id', type=int, choices=range(1,6),
336        metavar='[key_id] ',
337        default=1,
338        help='Provide the efuse key_id which contains/will contain HMAC_KEY, default is 1')
339
340    parser.add_argument(
341        '--port', '-p',
342        dest='port',
343        metavar='[port]',
344        required=True,
345        help='UART com port to which the ESP device is connected')
346
347    parser.add_argument(
348        '--keep_ds_data_on_host','-keep_ds_data',
349        dest='keep_ds_data', action='store_true',
350        help='Keep encrypted private key data and key on host machine for testing purpose')
351
352    parser.add_argument(
353        '--production', '-prod',
354        dest='production', action='store_true',
355        help='Enable production configurations. e.g.keep efuse key block read protection enabled')
356
357    args = parser.parse_args()
358
359    idf_target = get_idf_target()
360    if idf_target not in supported_targets:
361        if idf_target is not None:
362            print('ERROR: The script does not support the target %s' % idf_target)
363        sys.exit(-1)
364    idf_target = str(idf_target)
365
366    if args.summary is not False:
367        efuse_summary(args, idf_target)
368        sys.exit(0)
369
370    if (os.path.exists(args.privkey) is False):
371        print('ERROR: The provided private key file does not exist')
372        sys.exit(-1)
373
374    if (os.path.exists(esp_ds_data_dir) is False):
375        os.makedirs(esp_ds_data_dir)
376
377    # Burn hmac_key on the efuse block (if it is empty) or read it
378    # from the efuse block (if the efuse block already contains a key).
379    hmac_key_read = configure_efuse_key_block(args, idf_target)
380    if hmac_key_read is None:
381        sys.exit(-1)
382
383    # Calculate the encrypted private key data along with all other parameters
384    c, iv, key_size = calculate_ds_parameters(args.privkey, args.priv_key_pass, hmac_key_read, idf_target)
385
386    # Generate csv file for the DS data and generate an NVS partition.
387    generate_csv_file(c, iv, args.efuse_key_id, key_size, csv_filename)
388    generate_nvs_partition(csv_filename, bin_filename)
389    cleanup(args)
390
391
392if __name__ == '__main__':
393    main()
394