gen_custom_ota.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #!/usr/bin/env python
  2. #
  3. # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
  4. #
  5. # SPDX-License-Identifier: Apache-2.0
  6. import sys
  7. import struct
  8. import argparse
  9. import binascii
  10. import hashlib
  11. import os
  12. import subprocess
  13. import shutil
  14. import json
  15. import lzma
  16. # src_file = 'hello-world.bin'
  17. # compressed_file = 'hello-world.bin.xz'
  18. # packed_compressed_file = 'hello-world.bin.xz.packed'
  19. # siged_packed_compressed_file = 'hello-world.bin.xz.packed.signed'
  20. binary_compress_type = {'none': 0, 'xz':1}
  21. header_version = {'v1': 1, 'v2': 2, 'v3': 3}
  22. SCRIPT_VERSION = '1.0.0'
  23. ORIGIN_APP_IMAGE_HEADER_LEN = 288 # sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t). See esp_app_format.h
  24. # At present, we calculate the checksum of the first 4KB data of the old app.
  25. OLD_APP_CHECK_DATA_SIZE = 4096
  26. # v1 compressed data header:
  27. # Note: Encryption_type field is deprecated, the field is reserved for compatibility.
  28. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
  29. # | | Magic | header | Compress | delta | Encryption | Reserved | Firmware | The length of | The MD5 of | The CRC32 for| compressed |
  30. # | | number | version | type | type | type | | version | compressed data| compressed data| the header | data |
  31. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
  32. # | Size | 4 bytes | 1 byte | 4 bits | 4 bits | 1 bytes | 1 bytes | 32 bytes | 4 bytes | 32 bytes | 4 bytes | |
  33. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
  34. # | Data | String | | | | | | String | little-endian | byte string | little-endian| |
  35. # | type | ended | | | | | | ended | integer | | integer | |
  36. # | |with ‘\0’| | | | | | with ‘\0’| | | | Binary data|
  37. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
  38. # | Data | “ESP” | 1 | 0/1 | 0/1 | | | | | | | |
  39. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|--------------|------------|
  40. # v2 compressed data header
  41. # Note: Encryption_type field is deprecated, the field is reserved for compatibility.
  42. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
  43. # | | Magic | header | Compress | delta | Encryption | Reserved | Firmware | The length of | The MD5 of | base app check| The CRC32 for | The CRC32 for| compressed |
  44. # | | number | version | type | type | type | | version | compressed data| compressed data| data len | base app data | the header | data |
  45. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
  46. # | Size | 4 bytes | 1 byte | 4 bits | 4 bits | 1 bytes | 1 bytes | 32 bytes | 4 bytes | 32 bytes | 4 bytes | 4 bytes | 4 bytes | |
  47. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
  48. # | Data | String | | | | | | String | little-endian | byte string | little-endian | little-endian | little-endian| |
  49. # | type | ended | | | | | | ended | integer | | integer | integer | integer | |
  50. # | |with ‘\0’| | | | | | with ‘\0’| | | | | | Binary data|
  51. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
  52. # | Data | “ESP” | 1 | 0/1 | 0/1 | | | | | | | | | |
  53. # |------|---------|---------|----------|------------|------------|-----------|----------|----------------|----------------|---------------|---------------|--------------|------------|
  54. # v3 compressed data header:
  55. # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
  56. # | | Magic | header | Compress | Reserved | Reserved | The length of | The MD5 of | The CRC32 for| compressed |
  57. # | | number | version | type | | | compressed data| compressed data| the header | data |
  58. # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
  59. # | Size | 4 bytes | 1 byte | 4 bits | 4 bits | 8 bytes | 4 bytes | 16 bytes | 4 bytes | |
  60. # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
  61. # | Data | String | integer | | | | little-endian | byte string | little-endian| |
  62. # | type | ended | | | | | integer | | integer | |
  63. # | |with ‘\0’| | | | | | | | Binary data|
  64. # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
  65. # | Data | “ESP” | 3 | 0/1 | | | | | | |
  66. # |------|---------|---------|----------|------------|-----------|----------------|----------------|--------------|------------|
  67. def xz_compress(store_directory, in_file):
  68. compressed_file = ''.join([in_file,'.xz'])
  69. if(os.path.exists(compressed_file)):
  70. os.remove(compressed_file)
  71. xz_compressor_filter = [
  72. {"id": lzma.FILTER_LZMA2, "preset": 6, "dict_size": 64*1024},
  73. ]
  74. with open(in_file, 'rb') as src_f:
  75. data = src_f.read()
  76. with lzma.open(compressed_file, "wb", format=lzma.FORMAT_XZ, check=lzma.CHECK_CRC32, filters=xz_compressor_filter) as f:
  77. f.write(data)
  78. f.close()
  79. if not os.path.exists(os.path.join(store_directory, os.path.split(compressed_file)[1])):
  80. shutil.copy(compressed_file, store_directory)
  81. print('copy xz file done')
  82. def secure_boot_sign(sign_key, in_file, out_file):
  83. ret = subprocess.call('espsecure.py sign_data --version 2 --keyfile {} --output {} {}'.format(sign_key, out_file, in_file), shell = True)
  84. if ret:
  85. raise Exception('sign failed')
  86. def get_app_name():
  87. with open('flasher_args.json') as f:
  88. try:
  89. flasher_args = json.load(f)
  90. return flasher_args['app']['file']
  91. except Exception as e:
  92. print(e)
  93. return ''
  94. def get_script_version():
  95. return SCRIPT_VERSION
  96. def main():
  97. parser = argparse.ArgumentParser()
  98. parser.add_argument('-hv', '--header_ver', nargs='?', choices = ['v1', 'v2', 'v3'],
  99. default='v3', help='the version of the packed file header [default:v3]')
  100. parser.add_argument('-c', '--compress_type', nargs= '?', choices = ['none', 'xz'],
  101. default='xz', help='compressed type [default:xz]')
  102. parser.add_argument('-i', '--in_file', nargs = '?',
  103. default='', help='the new app firmware')
  104. parser.add_argument('--sign_key', nargs = '?',
  105. default='', help='the sign key used for secure boot')
  106. parser.add_argument('-fv', '--fw_ver', nargs='?',
  107. default='', help='the version of the compressed data(this field is deprecated in v3)')
  108. parser.add_argument('--add_app_header', action="store_true", help='add app header to use native esp_ota_* & esp_https_ota_* APIs')
  109. parser.add_argument('-v', '--version', action='version', version=get_script_version(), help='the version of the script')
  110. args = parser.parse_args()
  111. compress_type = args.compress_type
  112. firmware_ver = args.fw_ver
  113. src_file = args.in_file
  114. sign_key = args.sign_key
  115. header_ver = args.header_ver
  116. add_app_header = args.add_app_header
  117. if src_file == '':
  118. origin_app_name = get_app_name()
  119. if(origin_app_name == ''):
  120. print('get origin app name fail')
  121. return
  122. if os.path.exists(origin_app_name):
  123. src_file = origin_app_name
  124. else:
  125. print('origin app.bin not found')
  126. return
  127. print('src file is: {}'.format(src_file))
  128. # rebuild the cpmpressed_app directroy
  129. cpmoressed_app_directory = 'custom_ota_binaries'
  130. if os.path.exists(cpmoressed_app_directory):
  131. shutil.rmtree(cpmoressed_app_directory)
  132. os.mkdir(cpmoressed_app_directory)
  133. print('The compressed file will store in {}'.format(cpmoressed_app_directory))
  134. #step1: compress
  135. if compress_type == 'xz':
  136. xz_compress(cpmoressed_app_directory, os.path.abspath(src_file))
  137. origin_app_name = os.path.split(src_file)[1]
  138. compressed_file_name = ''.join([origin_app_name, '.xz'])
  139. compressed_file = os.path.join(cpmoressed_app_directory, compressed_file_name)
  140. else:
  141. compressed_file = ''.join(src_file)
  142. print('compressed file is: {}'.format(compressed_file))
  143. #step2: packet the compressed image header
  144. with open(src_file, 'rb') as s_f:
  145. src_image_header = bytearray(s_f.read(ORIGIN_APP_IMAGE_HEADER_LEN))
  146. src_image_header[1] = 0x00
  147. packed_file = ''.join([compressed_file,'.packed'])
  148. with open(compressed_file, 'rb') as src_f:
  149. data = src_f.read()
  150. f_len = src_f.tell()
  151. # magic number
  152. bin_data = struct.pack('4s', b'ESP')
  153. # header version
  154. bin_data += struct.pack('B', header_version[header_ver])
  155. # Compress type
  156. bin_data += struct.pack('B', binary_compress_type[compress_type])
  157. print('compressed type: {}'.format(binary_compress_type[compress_type]))
  158. # in header v1/v2, there is a field "Encryption type", this field has been deprecated in v3
  159. if (header_version[header_ver] < 3):
  160. bin_data += struct.pack('B', 0)
  161. # Reserved
  162. bin_data += struct.pack('?', 0)
  163. # Firmware version
  164. bin_data += struct.pack('32s', firmware_ver.encode())
  165. else:
  166. # Reserved
  167. bin_data += struct.pack('10s', b'0')
  168. # The length of the compressed data
  169. bin_data += struct.pack('<I', f_len)
  170. print('compressed data len: {}'.format(f_len))
  171. # The MD5 for the compressed data
  172. if (header_version[header_ver] < 3):
  173. bin_data += struct.pack('32s', hashlib.md5(data).digest())
  174. if (header_version[header_ver] == 2):
  175. # Todo, if it's diff OTA, write base app check data len
  176. bin_data += struct.pack('<I', 0)
  177. # Todo, if it's diff OTA, write base app crc32 checksum
  178. bin_data += struct.pack('<I', 0)
  179. else:
  180. bin_data += struct.pack('16s', hashlib.md5(data).digest())
  181. # The CRC32 for the header
  182. bin_data += struct.pack('<I', binascii.crc32(bin_data, 0x0))
  183. # write compressed data
  184. bin_data += data
  185. with open(packed_file, 'wb') as dst_f:
  186. # write compressed image header and compressed dada
  187. dst_f.write(bin_data)
  188. print('packed file is: {}'.format(packed_file))
  189. #step3: if need sign, then sign the packed image
  190. if sign_key != '':
  191. signed_file = ''.join([packed_file,'.signed'])
  192. secure_boot_sign(sign_key, packed_file, signed_file)
  193. print('signed_file is: {}'.format(signed_file))
  194. else:
  195. signed_file = ''.join(packed_file)
  196. if (header_version[header_ver] == 3) and add_app_header:
  197. with open(signed_file, 'rb+') as src_f:
  198. packed_data = src_f.read()
  199. src_f.seek(0)
  200. # write origin app image header
  201. src_f.write(src_image_header)
  202. # write compressed image header and compressed dada
  203. src_f.write(packed_data)
  204. print('app image header has been added')
  205. if __name__ == '__main__':
  206. try:
  207. main()
  208. except Exception as e:
  209. print(e)
  210. sys.exit(2)