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