├── .gitignore ├── LICENSE ├── README.rst ├── gen_swupdate.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | GEN_SWUPDATE(1) 3 | =============== 4 | 5 | NAME 6 | ==== 7 | gen_swupdate - SWU file generator 8 | 9 | SYNOPSIS 10 | ======== 11 | gen_swupdate.py [options] [sw-description.in] 12 | 13 | OPTIONS 14 | ======= 15 | -h, --help 16 | show the help message and exit 17 | --debug 18 | Enable various features for debugging 19 | -k KEY, --key=KEY 20 | pkcs11 uri or file name of the key used for signing the update 21 | -o OUTPUT, --output=OUTPUT 22 | filename of the resulting update file 23 | -C CHDIR, --chdir=CHDIR 24 | directory where the sw-update cpio archive is built 25 | -L LIBDIRS, --libdir=LIBDIRS 26 | add path where files (e.g. images and scripts) are searched 27 | 28 | DESCRIPTION 29 | =========== 30 | gen_swupdate is a tool to manage the creation of SWU files, which is the 31 | file format of SWUpdate. SWU files are cpio archives with a meta file that 32 | is called sw-description by default, followed by any other files that, 33 | e.g., can be block or UBIFS images, scripts, bootloader environments, etc. 34 | An overview is available at 35 | https://sbabic.github.io/swupdate/swupdate.html#single-image-delivery 36 | and in-depth documentation on the meta file with many available attributes 37 | at https://sbabic.github.io/swupdate/sw-description.html 38 | 39 | Depending on the SWUpdate configuration, several tools are needed in 40 | addition to a cpio implementation to create an SWU file. Especially, 41 | enabling cryptographic hashes or signatures on the included files involves 42 | a non-trivial order of tools like sha256sum or openssl, and adding their 43 | output to the sw-description which is too error-prone to do manually. 44 | After all, sw-description has a tree structure (libconfig or JSON format) 45 | and can become quite complex, especially when a redundant update is to be 46 | implemented with SWUpdate. 47 | 48 | SWUpdate does not provide any tooling to create an SWU file, which is why 49 | gen_swupdate was created. Its basic idea is to have a sw-description.in 50 | template with the constant information (e.g., included filenames) of a 51 | sw-description that is processed and the non-constant information (e.g., 52 | cryptographic hashes, signatures) is filled in to complete the 53 | sw-description. From that and the file list, a complete SWU file is packed 54 | with cpio. 55 | 56 | Feature List 57 | ------------ 58 | The following features are included in gen_swupdate (only a tiny subset of 59 | the overall attribute set): 60 | 61 | * implemented with Python and calls to cpio and openssl 62 | * libconfig format 63 | * SHA256 hashes for all included files 64 | * extraction of the decompressed-size of zlib-compressed UBI volumes 65 | * optional: plain RSA signatures for all included files 66 | * using PKCS#11 dongles for signatures 67 | 68 | BUGS 69 | ==== 70 | GNU cpio has a bug (see Debian bug #962188) that makes it 71 | write the wrong CPIO checksum for files greater than 2 GiB. 72 | SWUpdate verifies the checksum and fails for such files. 73 | 74 | gen_swupdate uses the paxcpio tool to create the CPIO by default 75 | and falls back to GNU cpio. paxcpio seems to be the only other 76 | common implementation that supports creating the crc format. 77 | 78 | SEE ALSO 79 | ======== 80 | swupdate(1), 81 | openssl(1), 82 | paxcpio(1), 83 | cpio(1) 84 | -------------------------------------------------------------------------------- /gen_swupdate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2017-2020 Linutronix GmbH 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import libconf 8 | import codecs 9 | 10 | import logging 11 | import os 12 | import os.path 13 | import shutil 14 | import hashlib 15 | from subprocess import Popen, PIPE 16 | 17 | from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter 18 | from tempfile import TemporaryDirectory 19 | 20 | import struct 21 | 22 | 23 | def getuncompressedsize(filename): 24 | with open(filename, 'rb') as f: 25 | f.seek(-4, 2) 26 | return struct.unpack('I', f.read(4))[0] 27 | 28 | 29 | def getsha256(filename): 30 | 31 | m = hashlib.sha256() 32 | 33 | with open(filename, 'rb') as f: 34 | while True: 35 | data = f.read(1024) 36 | if not data: 37 | break 38 | m.update(data) 39 | return m.hexdigest() 40 | 41 | 42 | def find_and_link_file(entry, libdirs): 43 | 44 | fname = entry.filename 45 | for d in libdirs: 46 | dname = os.path.join(d, fname) 47 | if os.path.exists(dname): 48 | try: 49 | os.symlink(dname, fname) 50 | except FileExistsError: 51 | pass 52 | return dname 53 | 54 | 55 | def handle_image(i, opt): 56 | if 'filename' not in i: 57 | return 58 | 59 | file_iv = find_and_link_file(i, opt.libdirs) 60 | 61 | sha256 = getsha256(i.filename) 62 | i['sha256'] = sha256 63 | 64 | if 'volume' in i and 'compressed' in i and ( 65 | i.compressed is True or i.compressed == "zlib"): 66 | 67 | if 'encrypted' in i: 68 | logging.warning("""The decompressed-size cannot be calculated 69 | for preencrypted volumes.""") 70 | else: 71 | unc_size = getuncompressedsize(file_iv) 72 | if 'properties' not in i: 73 | i['properties'] = {} 74 | i['properties']['decompressed-size'] = str(unc_size) 75 | 76 | 77 | def handle_script(i, opt): 78 | if 'filename' not in i: 79 | return 80 | 81 | find_and_link_file(i, opt.libdirs) 82 | 83 | sha256 = getsha256(i.filename) 84 | i['sha256'] = sha256 85 | 86 | 87 | def find_key(key, d): 88 | if isinstance(d, dict): 89 | if key in d: 90 | for i in d[key]: 91 | yield i 92 | for k in d.values(): 93 | for x in find_key(key, k): 94 | yield x 95 | 96 | 97 | def main(): 98 | parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter, 99 | description='''Generate (signed) swu-update file, 100 | based on information from a 101 | template sw-description.''') 102 | parser.add_argument("template", metavar="TEMPLATE", 103 | help="sw-description template (sw-decription.in)") 104 | parser.add_argument("--debug", action="store_true", dest="debug", 105 | default=False, 106 | help="Enable various features for debugging") 107 | parser.add_argument("-k", "--key", dest="key", 108 | help="""pkcs11 uri or file name of the key used for 109 | signing the update""") 110 | parser.add_argument("-o", "--output", dest="output", 111 | default="firmware.swu", 112 | help="filename of the resulting update file") 113 | parser.add_argument("-C", "--chdir", dest="chdir", 114 | help="""directory where the sw-update cpio archive is 115 | built""") 116 | parser.add_argument("-L", "--libdir", dest="libdirs", action="append", 117 | default=['.'], 118 | help="""add path where files (e.g. images and scripts) 119 | are searched""") 120 | 121 | opt = parser.parse_args() 122 | 123 | # make all paths absolute 124 | swdescription_in = os.path.abspath(opt.template) 125 | if opt.key: 126 | keyfile = os.path.abspath(opt.key) 127 | opt.output = os.path.abspath(opt.output) 128 | opt.libdirs = [os.path.abspath(p) for p in opt.libdirs] 129 | 130 | fp = codecs.open(swdescription_in, 'r', 'utf-8') 131 | cc = libconf.load(fp, filename=swdescription_in) 132 | 133 | if not opt.chdir: 134 | temp = TemporaryDirectory() 135 | opt.chdir = temp.name 136 | 137 | os.chdir(opt.chdir) 138 | 139 | for i in find_key('images', cc.software): 140 | handle_image(i, opt) 141 | 142 | for i in find_key('scripts', cc.software): 143 | handle_script(i, opt) 144 | 145 | for i in find_key('files', cc.software): 146 | handle_script(i, opt) 147 | 148 | fp = codecs.open('sw-description', 'w', 'utf-8') 149 | libconf.dump(cc, fp) 150 | fp.close() 151 | 152 | files = ['sw-description'] 153 | if opt.key: 154 | if os.path.isfile(keyfile): 155 | logging.warning("""Please consider providing a pkcs11 uri instead 156 | of a key file.""") 157 | sign_cmd = 'openssl dgst -sha256 \ 158 | -sign "%s" sw-description \ 159 | > sw-description.sig' % keyfile 160 | else: 161 | sign_cmd = 'openssl dgst -sha256 \ 162 | -engine pkcs11 \ 163 | -keyform engine \ 164 | -sign "%s" sw-description \ 165 | > sw-description.sig' % opt.key 166 | 167 | # Preventing the malloc check works around Debian bug #923333 168 | if os.system('MALLOC_CHECK_=0 ' + sign_cmd) != 0: 169 | print('failed to sign sw-description') 170 | files.append('sw-description.sig') 171 | 172 | for i in find_key('images', cc.software): 173 | if 'filename' in i: 174 | files.append(i.filename) 175 | 176 | for i in find_key('scripts', cc.software): 177 | if 'filename' in i: 178 | files.append(i.filename) 179 | 180 | for i in find_key('files', cc.software): 181 | if 'filename' in i: 182 | files.append(i.filename) 183 | 184 | swfp = open(opt.output, 'wb') 185 | cpio_cmd = 'paxcpio' 186 | cpio_opt = '-L' 187 | if not shutil.which(cpio_cmd): 188 | cpio_cmd = 'cpio' 189 | cpio_opt = '--dereference' 190 | cpio = Popen([cpio_cmd, '-ov', '-H', 'crc', cpio_opt], 191 | stdin=PIPE, stdout=swfp) 192 | 193 | files_for_cpio = [] 194 | for i in files: 195 | if i not in files_for_cpio: 196 | files_for_cpio.append(i) 197 | 198 | for n in files_for_cpio: 199 | if cpio_cmd == 'cpio' and os.path.getsize(n) > (2 << 30): 200 | logging.warning('''%s is greater than 2GiB. %s will have a bad 201 | checksum with GNU cpio. Install paxcpio or 202 | configure SWUpdate with DISABLE_CPIO_CRC.''', 203 | n, opt.output) 204 | cpio.stdin.write(bytes(n+'\n', 'utf-8')) 205 | 206 | cpio.stdin.close() 207 | cpio.wait() 208 | 209 | swfp.close() 210 | 211 | print('finished') 212 | 213 | 214 | if __name__ == "__main__": 215 | main() 216 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2020 Linutronix GmbH 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | from setuptools import setup 6 | 7 | with open('README.rst') as f: 8 | readme = f.read() 9 | 10 | setup( 11 | name='gen_swupdate', 12 | version='0.4', 13 | description="SWUpdate SWU file generator", 14 | long_description=readme, 15 | author="Linutronix GmbH", 16 | author_email="info@linutronix.de", 17 | license="MIT", 18 | py_modules=['gen_swupdate'], 19 | install_requires=['libconf'], 20 | entry_points={'console_scripts': ['gen_swupdate = gen_swupdate:main']}, 21 | classifiers=[ 22 | 'License :: OSI Approved :: MIT License', 23 | 'Programming Language :: Python :: 3', 24 | ] 25 | ) 26 | --------------------------------------------------------------------------------