1# This file includes the operations with eFuses for ESP32-C61 chip
2#
3# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
4#
5# SPDX-License-Identifier: GPL-2.0-or-later
6
7import argparse
8import io
9import os  # noqa: F401. It is used in IDF scripts
10import traceback
11
12import espsecure
13
14import esptool
15
16from . import fields
17from .. import util
18from ..base_operations import (
19    add_common_commands,
20    add_force_write_always,
21    add_show_sensitive_info_option,
22    burn_bit,
23    burn_block_data,
24    burn_efuse,
25    check_error,
26    dump,
27    read_protect_efuse,
28    summary,
29    write_protect_efuse,
30)
31
32
33def protect_options(p):
34    p.add_argument(
35        "--no-write-protect",
36        help="Disable write-protecting of the key. The key remains writable. "
37        "(The keys use the RS coding scheme that does not support "
38        "post-write data changes. Forced write can damage RS encoding bits.) "
39        "The write-protecting of keypurposes does not depend on the option, "
40        "it will be set anyway.",
41        action="store_true",
42    )
43    p.add_argument(
44        "--no-read-protect",
45        help="Disable read-protecting of the key. The key remains readable software."
46        "The key with keypurpose[USER, RESERVED and *_DIGEST] "
47        "will remain readable anyway. For the rest keypurposes the read-protection "
48        "will be defined the option (Read-protect by default).",
49        action="store_true",
50    )
51
52
53def add_commands(subparsers, efuses):
54    add_common_commands(subparsers, efuses)
55    burn_key = subparsers.add_parser(
56        "burn_key", help="Burn the key block with the specified name"
57    )
58    protect_options(burn_key)
59    add_force_write_always(burn_key)
60    add_show_sensitive_info_option(burn_key)
61    burn_key.add_argument(
62        "block",
63        help="Key block to burn",
64        action="append",
65        choices=efuses.BLOCKS_FOR_KEYS,
66    )
67    burn_key.add_argument(
68        "keyfile",
69        help="File containing 256 bits of binary key data. For the ECDSA_KEY purpose use PEM file.",
70        action="append",
71        type=argparse.FileType("rb"),
72    )
73    burn_key.add_argument(
74        "keypurpose",
75        help="Purpose to set.",
76        action="append",
77        choices=fields.EfuseKeyPurposeField.KEY_PURPOSES_NAME,
78    )
79    for _ in efuses.BLOCKS_FOR_KEYS:
80        burn_key.add_argument(
81            "block",
82            help="Key block to burn",
83            nargs="?",
84            action="append",
85            metavar="BLOCK",
86            choices=efuses.BLOCKS_FOR_KEYS,
87        )
88        burn_key.add_argument(
89            "keyfile",
90            help="File containing 256 bits of binary key data. For the ECDSA_KEY purpose use PEM file.",
91            nargs="?",
92            action="append",
93            metavar="KEYFILE",
94            type=argparse.FileType("rb"),
95        )
96        burn_key.add_argument(
97            "keypurpose",
98            help="Purpose to set.",
99            nargs="?",
100            action="append",
101            metavar="KEYPURPOSE",
102            choices=fields.EfuseKeyPurposeField.KEY_PURPOSES_NAME,
103        )
104
105    burn_key_digest = subparsers.add_parser(
106        "burn_key_digest",
107        help="Parse a RSA public key and burn the digest to key efuse block",
108    )
109    protect_options(burn_key_digest)
110    add_force_write_always(burn_key_digest)
111    add_show_sensitive_info_option(burn_key_digest)
112    burn_key_digest.add_argument(
113        "block",
114        help="Key block to burn",
115        action="append",
116        choices=efuses.BLOCKS_FOR_KEYS,
117    )
118    burn_key_digest.add_argument(
119        "keyfile",
120        help="Key file to digest (PEM format)",
121        action="append",
122        type=argparse.FileType("rb"),
123    )
124    burn_key_digest.add_argument(
125        "keypurpose",
126        help="Purpose to set.",
127        action="append",
128        choices=fields.EfuseKeyPurposeField.DIGEST_KEY_PURPOSES,
129    )
130    for _ in efuses.BLOCKS_FOR_KEYS:
131        burn_key_digest.add_argument(
132            "block",
133            help="Key block to burn",
134            nargs="?",
135            action="append",
136            metavar="BLOCK",
137            choices=efuses.BLOCKS_FOR_KEYS,
138        )
139        burn_key_digest.add_argument(
140            "keyfile",
141            help="Key file to digest (PEM format)",
142            nargs="?",
143            action="append",
144            metavar="KEYFILE",
145            type=argparse.FileType("rb"),
146        )
147        burn_key_digest.add_argument(
148            "keypurpose",
149            help="Purpose to set.",
150            nargs="?",
151            action="append",
152            metavar="KEYPURPOSE",
153            choices=fields.EfuseKeyPurposeField.DIGEST_KEY_PURPOSES,
154        )
155
156    p = subparsers.add_parser(
157        "set_flash_voltage",
158        help="Permanently set the internal flash voltage regulator "
159        "to either 1.8V, 3.3V or OFF. "
160        "This means GPIO45 can be high or low at reset without "
161        "changing the flash voltage.",
162    )
163    p.add_argument("voltage", help="Voltage selection", choices=["1.8V", "3.3V", "OFF"])
164
165    p = subparsers.add_parser(
166        "burn_custom_mac", help="Burn a 48-bit Custom MAC Address to EFUSE BLOCK3."
167    )
168    p.add_argument(
169        "mac",
170        help="Custom MAC Address to burn given in hexadecimal format with bytes "
171        "separated by colons (e.g. AA:CD:EF:01:02:03).",
172        type=fields.base_fields.CheckArgValue(efuses, "CUSTOM_MAC"),
173    )
174    add_force_write_always(p)
175
176    p = subparsers.add_parser("get_custom_mac", help="Prints the Custom MAC Address.")
177
178
179def burn_custom_mac(esp, efuses, args):
180    efuses["CUSTOM_MAC"].save(args.mac)
181    if not efuses.burn_all(check_batch_mode=True):
182        return
183    get_custom_mac(esp, efuses, args)
184    print("Successful")
185
186
187def get_custom_mac(esp, efuses, args):
188    print("Custom MAC Address: {}".format(efuses["CUSTOM_MAC"].get()))
189
190
191def set_flash_voltage(esp, efuses, args):
192    raise esptool.FatalError("set_flash_voltage is not supported!")
193
194
195def adc_info(esp, efuses, args):
196    print("not supported yet")
197
198
199def key_block_is_unused(block, key_purpose_block):
200    if not block.is_readable() or not block.is_writeable():
201        return False
202
203    if key_purpose_block.get() != "USER" or not key_purpose_block.is_writeable():
204        return False
205
206    if not block.get_bitstring().all(False):
207        return False
208
209    return True
210
211
212def get_next_key_block(efuses, current_key_block, block_name_list):
213    key_blocks = [b for b in efuses.blocks if b.key_purpose_name]
214    start = key_blocks.index(current_key_block)
215
216    # Sort key blocks so that we pick the next free block (and loop around if necessary)
217    key_blocks = key_blocks[start:] + key_blocks[0:start]
218
219    # Exclude any other blocks that will be be burned
220    key_blocks = [b for b in key_blocks if b.name not in block_name_list]
221
222    for block in key_blocks:
223        key_purpose_block = efuses[block.key_purpose_name]
224        if key_block_is_unused(block, key_purpose_block):
225            return block
226
227    return None
228
229
230def split_512_bit_key(efuses, block_name_list, datafile_list, keypurpose_list):
231    i = keypurpose_list.index("XTS_AES_256_KEY")
232    block_name = block_name_list[i]
233
234    block_num = efuses.get_index_block_by_name(block_name)
235    block = efuses.blocks[block_num]
236
237    data = datafile_list[i].read()
238    if len(data) != 64:
239        raise esptool.FatalError(
240            "Incorrect key file size %d, XTS_AES_256_KEY should be 64 bytes" % len(data)
241        )
242
243    key_block_2 = get_next_key_block(efuses, block, block_name_list)
244    if not key_block_2:
245        raise esptool.FatalError("XTS_AES_256_KEY requires two free keyblocks")
246
247    keypurpose_list.append("XTS_AES_256_KEY_1")
248    datafile_list.append(io.BytesIO(data[:32]))
249    block_name_list.append(block_name)
250
251    keypurpose_list.append("XTS_AES_256_KEY_2")
252    datafile_list.append(io.BytesIO(data[32:]))
253    block_name_list.append(key_block_2.name)
254
255    keypurpose_list.pop(i)
256    datafile_list.pop(i)
257    block_name_list.pop(i)
258
259
260def burn_key(esp, efuses, args, digest=None):
261    if digest is None:
262        datafile_list = args.keyfile[
263            0 : len([name for name in args.keyfile if name is not None]) :
264        ]
265    else:
266        datafile_list = digest[0 : len([name for name in digest if name is not None]) :]
267    efuses.force_write_always = args.force_write_always
268    block_name_list = args.block[
269        0 : len([name for name in args.block if name is not None]) :
270    ]
271    keypurpose_list = args.keypurpose[
272        0 : len([name for name in args.keypurpose if name is not None]) :
273    ]
274
275    if "XTS_AES_256_KEY" in keypurpose_list:
276        # XTS_AES_256_KEY is not an actual HW key purpose, needs to be split into
277        # XTS_AES_256_KEY_1 and XTS_AES_256_KEY_2
278        split_512_bit_key(efuses, block_name_list, datafile_list, keypurpose_list)
279
280    util.check_duplicate_name_in_list(block_name_list)
281    if len(block_name_list) != len(datafile_list) or len(block_name_list) != len(
282        keypurpose_list
283    ):
284        raise esptool.FatalError(
285            "The number of blocks (%d), datafile (%d) and keypurpose (%d) "
286            "should be the same."
287            % (len(block_name_list), len(datafile_list), len(keypurpose_list))
288        )
289
290    print("Burn keys to blocks:")
291    for block_name, datafile, keypurpose in zip(
292        block_name_list, datafile_list, keypurpose_list
293    ):
294        efuse = None
295        for block in efuses.blocks:
296            if block_name == block.name or block_name in block.alias:
297                efuse = efuses[block.name]
298        if efuse is None:
299            raise esptool.FatalError("Unknown block name - %s" % (block_name))
300        num_bytes = efuse.bit_len // 8
301
302        block_num = efuses.get_index_block_by_name(block_name)
303        block = efuses.blocks[block_num]
304
305        if digest is None:
306            if keypurpose == "ECDSA_KEY":
307                sk = espsecure.load_ecdsa_signing_key(datafile)
308                data = sk.to_string()
309                if len(data) == 24:
310                    # the private key is 24 bytes long for NIST192p, and 8 bytes of padding
311                    data = b"\x00" * 8 + data
312            else:
313                data = datafile.read()
314        else:
315            data = datafile
316
317        print(" - %s" % (efuse.name), end=" ")
318        revers_msg = None
319        if efuses[block.key_purpose_name].need_reverse(keypurpose):
320            revers_msg = f"\tReversing byte order for {keypurpose} hardware peripheral"
321            data = data[::-1]
322        print(
323            "-> [{}]".format(
324                util.hexify(data, " ")
325                if args.show_sensitive_info
326                else " ".join(["??"] * len(data))
327            )
328        )
329        if revers_msg:
330            print(revers_msg)
331        if len(data) != num_bytes:
332            raise esptool.FatalError(
333                "Incorrect key file size %d. Key file must be %d bytes (%d bits) "
334                "of raw binary key data." % (len(data), num_bytes, num_bytes * 8)
335            )
336
337        if efuses[block.key_purpose_name].need_rd_protect(keypurpose):
338            read_protect = False if args.no_read_protect else True
339        else:
340            read_protect = False
341        write_protect = not args.no_write_protect
342
343        # using efuse instead of a block gives the advantage of checking it as the whole field.
344        efuse.save(data)
345
346        disable_wr_protect_key_purpose = False
347        if efuses[block.key_purpose_name].get() != keypurpose:
348            if efuses[block.key_purpose_name].is_writeable():
349                print(
350                    "\t'%s': '%s' -> '%s'."
351                    % (
352                        block.key_purpose_name,
353                        efuses[block.key_purpose_name].get(),
354                        keypurpose,
355                    )
356                )
357                efuses[block.key_purpose_name].save(keypurpose)
358                disable_wr_protect_key_purpose = True
359            else:
360                raise esptool.FatalError(
361                    "It is not possible to change '%s' to '%s' "
362                    "because write protection bit is set."
363                    % (block.key_purpose_name, keypurpose)
364                )
365        else:
366            print("\t'%s' is already '%s'." % (block.key_purpose_name, keypurpose))
367            if efuses[block.key_purpose_name].is_writeable():
368                disable_wr_protect_key_purpose = True
369
370        if disable_wr_protect_key_purpose:
371            print("\tDisabling write to '%s'." % block.key_purpose_name)
372            efuses[block.key_purpose_name].disable_write()
373
374        if read_protect:
375            print("\tDisabling read to key block")
376            efuse.disable_read()
377
378        if write_protect:
379            print("\tDisabling write to key block")
380            efuse.disable_write()
381        print("")
382
383    if not write_protect:
384        print("Keys will remain writeable (due to --no-write-protect)")
385    if args.no_read_protect:
386        print("Keys will remain readable (due to --no-read-protect)")
387
388    if not efuses.burn_all(check_batch_mode=True):
389        return
390    print("Successful")
391
392
393def burn_key_digest(esp, efuses, args):
394    digest_list = []
395    datafile_list = args.keyfile[
396        0 : len([name for name in args.keyfile if name is not None]) :
397    ]
398    block_list = args.block[
399        0 : len([block for block in args.block if block is not None]) :
400    ]
401    for block_name, datafile in zip(block_list, datafile_list):
402        efuse = None
403        for block in efuses.blocks:
404            if block_name == block.name or block_name in block.alias:
405                efuse = efuses[block.name]
406        if efuse is None:
407            raise esptool.FatalError("Unknown block name - %s" % (block_name))
408        num_bytes = efuse.bit_len // 8
409        digest = espsecure._digest_sbv2_public_key(datafile)
410        if len(digest) != num_bytes:
411            raise esptool.FatalError(
412                "Incorrect digest size %d. Digest must be %d bytes (%d bits) "
413                "of raw binary key data." % (len(digest), num_bytes, num_bytes * 8)
414            )
415        digest_list.append(digest)
416    burn_key(esp, efuses, args, digest=digest_list)
417
418
419def espefuse(esp, efuses, args, command):
420    parser = argparse.ArgumentParser()
421    subparsers = parser.add_subparsers(dest="operation")
422    add_commands(subparsers, efuses)
423    try:
424        cmd_line_args = parser.parse_args(command.split())
425    except SystemExit:
426        traceback.print_stack()
427        raise esptool.FatalError('"{}" - incorrect command'.format(command))
428    if cmd_line_args.operation == "execute_scripts":
429        configfiles = cmd_line_args.configfiles
430        index = cmd_line_args.index
431    # copy arguments from args to cmd_line_args
432    vars(cmd_line_args).update(vars(args))
433    if cmd_line_args.operation == "execute_scripts":
434        cmd_line_args.configfiles = configfiles
435        cmd_line_args.index = index
436    if cmd_line_args.operation is None:
437        parser.print_help()
438        parser.exit(1)
439    operation_func = globals()[cmd_line_args.operation]
440    # each 'operation' is a module-level function of the same name
441    operation_func(esp, efuses, cmd_line_args)
442
443
444def execute_scripts(esp, efuses, args):
445    efuses.batch_mode_cnt += 1
446    del args.operation
447    scripts = args.scripts
448    del args.scripts
449
450    for file in scripts:
451        with open(file.name, "r") as file:
452            exec(compile(file.read(), file.name, "exec"))
453
454    if args.debug:
455        for block in efuses.blocks:
456            data = block.get_bitstring(from_read=False)
457            block.print_block(data, "regs_for_burn", args.debug)
458
459    efuses.batch_mode_cnt -= 1
460    if not efuses.burn_all(check_batch_mode=True):
461        return
462    print("Successful")
463