espsecure.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. #!/usr/bin/env python
  2. # ESP32 secure boot utility
  3. # https://github.com/themadinventor/esptool
  4. #
  5. # Copyright (C) 2016 Espressif Systems (Shanghai) PTE LTD
  6. #
  7. # This program is free software; you can redistribute it and/or modify it under
  8. # the terms of the GNU General Public License as published by the Free Software
  9. # Foundation; either version 2 of the License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful, but WITHOUT
  12. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  13. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License along with
  16. # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
  17. # Street, Fifth Floor, Boston, MA 02110-1301 USA.
  18. from __future__ import division, print_function
  19. import argparse
  20. import hashlib
  21. import os
  22. import struct
  23. import sys
  24. import ecdsa
  25. import esptool
  26. import pyaes
  27. def get_chunks(source, chunk_len):
  28. """ Returns an iterator over 'chunk_len' chunks of 'source' """
  29. return (source[i: i + chunk_len] for i in range(0, len(source), chunk_len))
  30. def endian_swap_words(source):
  31. """ Endian-swap each word in 'source' bitstring """
  32. assert len(source) % 4 == 0
  33. words = "I" * (len(source) // 4)
  34. return struct.pack("<" + words, *struct.unpack(">" + words, source))
  35. def swap_word_order(source):
  36. """ Swap the order of the words in 'source' bitstring """
  37. assert len(source) % 4 == 0
  38. words = "I" * (len(source) // 4)
  39. return struct.pack(words, *reversed(struct.unpack(words, source)))
  40. def _load_hardware_key(keyfile):
  41. """ Load a 256-bit key, similar to stored in efuse, from a file
  42. 192-bit keys will be extended to 256-bit using the same algorithm used
  43. by hardware if 3/4 Coding Scheme is set.
  44. """
  45. key = keyfile.read()
  46. if len(key) not in [24, 32]:
  47. raise esptool.FatalError("Key file contains wrong length (%d bytes), 24 or 32 expected." % len(key))
  48. if len(key) == 24:
  49. key = key + key[8:16]
  50. print("Using 192-bit key (extended)")
  51. else:
  52. print("Using 256-bit key")
  53. assert len(key) == 32
  54. return key
  55. def digest_secure_bootloader(args):
  56. """ Calculate the digest of a bootloader image, in the same way the hardware
  57. secure boot engine would do so. Can be used with a pre-loaded key to update a
  58. secure bootloader. """
  59. if args.iv is not None:
  60. print("WARNING: --iv argument is for TESTING PURPOSES ONLY")
  61. iv = args.iv.read(128)
  62. else:
  63. iv = os.urandom(128)
  64. plaintext_image = args.image.read()
  65. args.image.seek(0)
  66. # secure boot engine reads in 128 byte blocks (ie SHA512 block
  67. # size), but also doesn't look for any appended SHA-256 digest
  68. fw_image = esptool.ESP32FirmwareImage(args.image)
  69. if fw_image.append_digest:
  70. if len(plaintext_image) % 128 <= 32:
  71. # ROM bootloader will read to the end of the 128 byte block, but not
  72. # to the end of the SHA-256 digest at the end
  73. new_len = len(plaintext_image) - (len(plaintext_image) % 128)
  74. plaintext_image = plaintext_image[:new_len]
  75. # if image isn't 128 byte multiple then pad with 0xFF (ie unwritten flash)
  76. # as this is what the secure boot engine will see
  77. if len(plaintext_image) % 128 != 0:
  78. plaintext_image += b"\xFF" * (128 - (len(plaintext_image) % 128))
  79. plaintext = iv + plaintext_image
  80. # Secure Boot digest algorithm in hardware uses AES256 ECB to
  81. # produce a ciphertext, then feeds output through SHA-512 to
  82. # produce the digest. Each block in/out of ECB is reordered
  83. # (due to hardware quirks not for security.)
  84. key = _load_hardware_key(args.keyfile)
  85. aes = pyaes.AESModeOfOperationECB(key)
  86. digest = hashlib.sha512()
  87. for block in get_chunks(plaintext, 16):
  88. block = block[::-1] # reverse each input block
  89. cipher_block = aes.encrypt(block)
  90. # reverse and then byte swap each word in the output block
  91. cipher_block = cipher_block[::-1]
  92. for block in get_chunks(cipher_block, 4):
  93. # Python hashlib can build each SHA block internally
  94. digest.update(block[::-1])
  95. if args.output is None:
  96. args.output = os.path.splitext(args.image.name)[0] + "-digest-0x0000.bin"
  97. with open(args.output, "wb") as f:
  98. f.write(iv)
  99. digest = digest.digest()
  100. for word in get_chunks(digest, 4):
  101. f.write(word[::-1]) # swap word order in the result
  102. f.write(b'\xFF' * (0x1000 - f.tell())) # pad to 0x1000
  103. f.write(plaintext_image)
  104. print("digest+image written to %s" % args.output)
  105. def generate_signing_key(args):
  106. """ Generate an ECDSA signing key for signing secure boot images (post-bootloader) """
  107. if os.path.exists(args.keyfile):
  108. raise esptool.FatalError("ERROR: Key file %s already exists" % args.keyfile)
  109. sk = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
  110. with open(args.keyfile, "wb") as f:
  111. f.write(sk.to_pem())
  112. print("ECDSA NIST256p private key in PEM format written to %s" % args.keyfile)
  113. def _load_ecdsa_signing_key(args):
  114. sk = ecdsa.SigningKey.from_pem(args.keyfile.read())
  115. if sk.curve != ecdsa.NIST256p:
  116. raise esptool.FatalError("Signing key uses incorrect curve. ESP32 Secure Boot only supports NIST256p (openssl calls this curve 'prime256v1")
  117. return sk
  118. def sign_data(args):
  119. """ Sign a data file with a ECDSA private key, append binary signature to file contents """
  120. sk = _load_ecdsa_signing_key(args)
  121. # calculate signature of binary data
  122. binary_content = args.datafile.read()
  123. signature = sk.sign_deterministic(binary_content, hashlib.sha256)
  124. # back-verify signature
  125. vk = sk.get_verifying_key()
  126. vk.verify(signature, binary_content, hashlib.sha256) # throws exception on failure
  127. if args.output is None or os.path.abspath(args.output) == os.path.abspath(args.datafile.name): # append signature to input file
  128. args.datafile.close()
  129. outfile = open(args.datafile.name, "ab")
  130. else: # write file & signature to new file
  131. outfile = open(args.output, "wb")
  132. outfile.write(binary_content)
  133. outfile.write(struct.pack("I", 0)) # Version indicator, allow for different curves/formats later
  134. outfile.write(signature)
  135. outfile.close()
  136. print("Signed %d bytes of data from %s with key %s" % (len(binary_content), args.datafile.name, args.keyfile.name))
  137. def verify_signature(args):
  138. """ Verify a previously signed binary image, using the ECDSA public key """
  139. key_data = args.keyfile.read()
  140. if b"-BEGIN EC PRIVATE KEY" in key_data:
  141. sk = ecdsa.SigningKey.from_pem(key_data)
  142. vk = sk.get_verifying_key()
  143. elif b"-BEGIN PUBLIC KEY" in key_data:
  144. vk = ecdsa.VerifyingKey.from_pem(key_data)
  145. elif len(key_data) == 64:
  146. vk = ecdsa.VerifyingKey.from_string(key_data,
  147. curve=ecdsa.NIST256p)
  148. else:
  149. raise esptool.FatalError("Verification key does not appear to be an EC key in PEM format or binary EC public key data. Unsupported")
  150. if vk.curve != ecdsa.NIST256p:
  151. raise esptool.FatalError("Public key uses incorrect curve. ESP32 Secure Boot only supports NIST256p (openssl calls this curve 'prime256v1")
  152. binary_content = args.datafile.read()
  153. data = binary_content[0:-68]
  154. sig_version, signature = struct.unpack("I64s", binary_content[-68:])
  155. if sig_version != 0:
  156. raise esptool.FatalError("Signature block has version %d. This version of espsecure only supports version 0." % sig_version)
  157. print("Verifying %d bytes of data" % len(data))
  158. try:
  159. if vk.verify(signature, data, hashlib.sha256):
  160. print("Signature is valid")
  161. else:
  162. raise esptool.FatalError("Signature is not valid")
  163. except ecdsa.keys.BadSignatureError:
  164. raise esptool.FatalError("Signature is not valid")
  165. def extract_public_key(args):
  166. """ Load an ECDSA private key and extract the embedded public key as raw binary data. """
  167. sk = _load_ecdsa_signing_key(args)
  168. vk = sk.get_verifying_key()
  169. args.public_keyfile.write(vk.to_string())
  170. print("%s public key extracted to %s" % (args.keyfile.name, args.public_keyfile.name))
  171. def digest_private_key(args):
  172. sk = _load_ecdsa_signing_key(args)
  173. repr(sk.to_string())
  174. digest = hashlib.sha256()
  175. digest.update(sk.to_string())
  176. result = digest.digest()
  177. if args.keylen == '192':
  178. result = result[0:24]
  179. args.digest_file.write(result)
  180. print("SHA-256 digest of private key %s%s written to %s" % (args.keyfile.name,
  181. "" if args.keylen == '256'
  182. else " (truncated to 192 bits)",
  183. args.digest_file.name))
  184. # flash encryption key tweaking pattern: the nth bit of the key is
  185. # flipped if the kth bit in the flash offset is set, where mapping
  186. # from n to k is provided by this list of 'n' bit offsets (range k)
  187. _FLASH_ENCRYPTION_TWEAK_PATTERN = [
  188. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  189. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  190. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  191. 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  192. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  193. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  194. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  195. 12, 11, 10, 9, 8, 7, 6, 5,
  196. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  197. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  198. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  199. 10, 9, 8, 7, 6, 5,
  200. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  201. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  202. 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5,
  203. 8, 7, 6, 5
  204. ]
  205. assert len(_FLASH_ENCRYPTION_TWEAK_PATTERN) == 256
  206. def _flash_encryption_tweak_range(flash_crypt_config=0xF):
  207. """ Return a list of the bit indexes that the "key tweak" applies to,
  208. as determined by the FLASH_CRYPT_CONFIG 4 bit efuse value.
  209. """
  210. tweak_range = []
  211. if (flash_crypt_config & 1) != 0:
  212. tweak_range += range(67)
  213. if (flash_crypt_config & 2) != 0:
  214. tweak_range += range(67, 132)
  215. if (flash_crypt_config & 4) != 0:
  216. tweak_range += range(132, 195)
  217. if (flash_crypt_config & 8) != 0:
  218. tweak_range += range(195, 256)
  219. return tweak_range
  220. def _flash_encryption_tweak_key(key, offset, tweak_range):
  221. """Apply XOR "tweak" values to the key, derived from flash offset
  222. 'offset'. This matches the ESP32 hardware flash encryption.
  223. tweak_range is a list of bit indexes to apply the tweak to, as
  224. generated by _flash_encryption_tweak_range() from the
  225. FLASH_CRYPT_CONFIG efuse value.
  226. Return tweaked key
  227. """
  228. if esptool.PYTHON2:
  229. key = [ord(k) for k in key]
  230. else:
  231. key = list(key)
  232. assert len(key) == 32
  233. offset_bits = [(offset & (1 << x)) != 0 for x in range(24)]
  234. for bit in tweak_range:
  235. if offset_bits[_FLASH_ENCRYPTION_TWEAK_PATTERN[bit]]:
  236. # note that each byte has a backwards bit order, compared
  237. # to how it is looked up in the tweak pattern table
  238. key[bit // 8] ^= 1 << (7 - (bit % 8))
  239. if esptool.PYTHON2:
  240. return b"".join(chr(k) for k in key)
  241. else:
  242. return bytes(key)
  243. def generate_flash_encryption_key(args):
  244. args.key_file.write(os.urandom(32))
  245. def _flash_encryption_operation(output_file, input_file, flash_address, keyfile, flash_crypt_conf, do_decrypt):
  246. key = _load_hardware_key(keyfile)
  247. if flash_address % 16 != 0:
  248. raise esptool.FatalError("Starting flash address 0x%x must be a multiple of 16" % flash_address)
  249. if flash_crypt_conf == 0:
  250. print("WARNING: Setting FLASH_CRYPT_CONF to zero is not recommended")
  251. tweak_range = _flash_encryption_tweak_range(flash_crypt_conf)
  252. aes = None
  253. while True:
  254. block_offs = flash_address + input_file.tell()
  255. block = input_file.read(16)
  256. if len(block) == 0:
  257. break
  258. elif len(block) < 16:
  259. if do_decrypt:
  260. raise esptool.FatalError("Data length is not a multiple of 16 bytes")
  261. pad = 16 - len(block)
  262. block = block + os.urandom(pad)
  263. print("Note: Padding with %d bytes of random data (encrypted data must be multiple of 16 bytes long)" % pad)
  264. if (block_offs % 32 == 0) or aes is None:
  265. # each bit of the flash encryption key is XORed with tweak bits derived from the offset of 32 byte block of flash
  266. block_key = _flash_encryption_tweak_key(key, block_offs, tweak_range)
  267. aes = pyaes.AESModeOfOperationECB(block_key)
  268. block = block[::-1] # reverse input block byte order
  269. # note AES is used inverted for flash encryption, so
  270. # "decrypting" flash uses AES encrypt algorithm and vice
  271. # versa. (This does not weaken AES.)
  272. if do_decrypt:
  273. block = aes.encrypt(block)
  274. else:
  275. block = aes.decrypt(block)
  276. block = block[::-1] # reverse output block byte order
  277. output_file.write(block)
  278. def decrypt_flash_data(args):
  279. return _flash_encryption_operation(args.output, args.encrypted_file, args.address, args.keyfile, args.flash_crypt_conf, True)
  280. def encrypt_flash_data(args):
  281. return _flash_encryption_operation(args.output, args.plaintext_file, args.address, args.keyfile, args.flash_crypt_conf, False)
  282. def main():
  283. parser = argparse.ArgumentParser(description='espsecure.py v%s - ESP32 Secure Boot & Flash Encryption tool' % esptool.__version__, prog='espsecure')
  284. subparsers = parser.add_subparsers(
  285. dest='operation',
  286. help='Run espsecure.py {command} -h for additional help')
  287. p = subparsers.add_parser('digest_secure_bootloader',
  288. help='Take a bootloader binary image and a secure boot key, and output a combined digest+binary ' +
  289. 'suitable for flashing along with the precalculated secure boot key.')
  290. p.add_argument('--keyfile', '-k', help="256 bit key for secure boot digest.", type=argparse.FileType('rb'), required=True)
  291. p.add_argument('--output', '-o', help="Output file for signed digest image.")
  292. p.add_argument('--iv', help="128 byte IV file. Supply a file for testing purposes only, if not supplied an IV will be randomly generated.",
  293. type=argparse.FileType('rb'))
  294. p.add_argument('image', help="Bootloader image file to calculate digest from", type=argparse.FileType('rb'))
  295. p = subparsers.add_parser('generate_signing_key',
  296. help='Generate a private key for signing secure boot images. Key file is generated in PEM format, ' +
  297. 'and contains a ECDSA NIST256p private key and matching public key.')
  298. p.add_argument('keyfile', help="Filename for private key file (embedded public key)")
  299. p = subparsers.add_parser('sign_data',
  300. help='Sign a data file for use with secure boot. Signing algorithm is determinsitic ECDSA w/ SHA-512.')
  301. p.add_argument('--keyfile', '-k', help="Private key file for signing. Key is in PEM format, ECDSA NIST256p curve. " +
  302. "generate_signing_key command can be used to generate a suitable signing key.", type=argparse.FileType('rb'), required=True)
  303. p.add_argument('--output', '-o', help="Output file for signed digest image. Default is to append signature to existing file.")
  304. p.add_argument('datafile', help="Data file to sign.", type=argparse.FileType('rb'))
  305. p = subparsers.add_parser('verify_signature',
  306. help='Verify a data file previously signed by "sign_data", using the public key.')
  307. p.add_argument('--keyfile', '-k', help="Public key file for verification. Can be private or public key in PEM format, " +
  308. "or a binary public key produced by extract_public_key command.",
  309. type=argparse.FileType('rb'), required=True)
  310. p.add_argument('datafile', help="Signed data file to verify signature.", type=argparse.FileType('rb'))
  311. p = subparsers.add_parser('extract_public_key',
  312. help='Extract the public verification key for signatures, save it as a raw binary file.')
  313. p.add_argument('--keyfile', '-k', help="Private key file (PEM format) to extract the public verification key from.", type=argparse.FileType('rb'),
  314. required=True)
  315. p.add_argument('public_keyfile', help="File to save new public key into", type=argparse.FileType('wb'))
  316. p = subparsers.add_parser('digest_private_key', help='Generate an SHA-256 digest of the private signing key. ' +
  317. 'This can be used as a reproducible secure bootloader or flash encryption key.')
  318. p.add_argument('--keyfile', '-k', help="Private key file (PEM format) to generate a digest from.", type=argparse.FileType('rb'),
  319. required=True)
  320. p.add_argument('--keylen', '-l', help="Length of private key digest file to generate (in bits).",
  321. choices=['192','256'], default='256')
  322. p.add_argument('digest_file', help="File to write 32 byte digest into", type=argparse.FileType('wb'))
  323. p = subparsers.add_parser('generate_flash_encryption_key', help='Generate a development-use 32 byte flash encryption key with random data.')
  324. p.add_argument('key_file', help="File to write 32 byte digest into", type=argparse.FileType('wb'))
  325. p = subparsers.add_parser('decrypt_flash_data', help='Decrypt some data read from encrypted flash (using known key)')
  326. p.add_argument('encrypted_file', help="File with encrypted flash contents", type=argparse.FileType('rb'))
  327. p.add_argument('--keyfile', '-k', help="File with flash encryption key", type=argparse.FileType('rb'),
  328. required=True)
  329. p.add_argument('--output', '-o', help="Output file for plaintext data.", type=argparse.FileType('wb'),
  330. required=True)
  331. p.add_argument('--address', '-a', help="Address offset in flash that file was read from.", required=True, type=esptool.arg_auto_int)
  332. p.add_argument('--flash_crypt_conf', help="Override FLASH_CRYPT_CONF efuse value (default is 0XF).", required=False, default=0xF, type=esptool.arg_auto_int)
  333. p = subparsers.add_parser('encrypt_flash_data', help='Encrypt some data suitable for encrypted flash (using known key)')
  334. p.add_argument('--keyfile', '-k', help="File with flash encryption key", type=argparse.FileType('rb'),
  335. required=True)
  336. p.add_argument('--output', '-o', help="Output file for encrypted data.", type=argparse.FileType('wb'),
  337. required=True)
  338. p.add_argument('--address', '-a', help="Address offset in flash where file will be flashed.", required=True, type=esptool.arg_auto_int)
  339. p.add_argument('--flash_crypt_conf', help="Override FLASH_CRYPT_CONF efuse value (default is 0XF).", required=False, default=0xF, type=esptool.arg_auto_int)
  340. p.add_argument('plaintext_file', help="File with plaintext content for encrypting", type=argparse.FileType('rb'))
  341. args = parser.parse_args()
  342. print('espsecure.py v%s' % esptool.__version__)
  343. if args.operation is None:
  344. parser.print_help()
  345. parser.exit(1)
  346. # each 'operation' is a module-level function of the same name
  347. operation_func = globals()[args.operation]
  348. operation_func(args)
  349. def _main():
  350. try:
  351. main()
  352. except esptool.FatalError as e:
  353. print('\nA fatal error occurred: %s' % e)
  354. sys.exit(2)
  355. if __name__ == '__main__':
  356. _main()