def test_uncompressed(self): cabarchive = CabArchive() cabarchive['README.txt'] = CabFile(b'foofoofoofoofoofoofoofoo') cabarchive['firmware.bin'] = CabFile(b'barbarbarbarbarbarbarbar') buf = cabarchive.save() self.assertEqual(len(buf), 156) self.assertEqual(hashlib.sha1(buf).hexdigest(), '676654685d6b5918d68081a786ae1d4dbfeb5e01')
def test_compressed(self): cabarchive = CabArchive() cabarchive['README.txt'] = CabFile(b'foofoofoofoofoofoofoofoo') cabarchive['firmware.bin'] = CabFile(b'barbarbarbarbarbarbarbar') buf = cabarchive.save(compress=True) self.assertEqual(len(buf), 122) self.assertEqual(hashlib.sha1(buf).hexdigest(), '74e94703c403aa93b16d01b088eb52e3a9c73288')
def test_inf_invalid(self): cabarchive = CabArchive() cabarchive['firmware.bin'] = _get_valid_firmware() cabarchive['firmware.metainfo.xml'] = CabFile(b'<component/>') cabarchive['firmware.inf'] = CabFile(b'fubar') with self.assertRaises(MetadataInvalid): ufile = UploadedFile() ufile.parse('foo.cab', cabarchive.save())
def test_create_compressed(self): cabarchive = CabArchive() # make predictable dt_epoch = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) cabarchive["README.txt"] = CabFile(b"foofoofoofoofoofoofoofoo", mtime=dt_epoch) cabarchive["firmware.bin"] = CabFile(b"barbarbarbarbarbarbarbar", mtime=dt_epoch) buf = cabarchive.save(compress=True) self.assertEqual(len(buf), 122) self.assertEqual( hashlib.sha1(buf).hexdigest(), "74e94703c403aa93b16d01b088eb52e3a9c73288")
def _get_valid_metainfo(release_description='This stable release fixes bugs', version_format='quad'): txt = """<?xml version="1.0" encoding="UTF-8"?> <!-- Copyright 2015 Richard Hughes <*****@*****.**> --> <component type="firmware"> <id>com.hughski.ColorHug.firmware</id> <name>ColorHug Firmware</name> <summary>Firmware for the ColorHug</summary> <description><p>Updating the firmware improves performance.</p></description> <provides> <firmware type="flashed">84f40464-9272-4ef7-9399-cd95f12da696</firmware> </provides> <url type="homepage">http://www.hughski.com/</url> <metadata_license>CC0-1.0</metadata_license> <project_license>GPL-2.0+</project_license> <developer_name>Hughski Limited</developer_name> <releases> <release version="0x30002" timestamp="1424116753"> <description><p>%s</p></description> </release> </releases> <custom> <value key="foo">bar</value> <value key="LVFS::InhibitDownload"/> <value key="LVFS::VersionFormat">%s</value> </custom> </component> """ % (release_description, version_format) return CabFile(txt.encode('utf-8'))
def test_metainfo_invalid(self): cabarchive = CabArchive() cabarchive['firmware.bin'] = _get_valid_firmware() cabarchive['firmware.metainfo.xml'] = CabFile(b'<compoXXXXnent/>') with self.assertRaises(MetadataInvalid): ufile = UploadedFile() _add_version_formats(ufile) ufile.parse('foo.cab', cabarchive.save())
def test_invalid_bom(self): cabarchive = CabArchive() cabarchive['firmware.bin'] = _get_valid_firmware() cabarchive['firmware.metainfo.xml'] = CabFile(b'\xEF\xBB\xBF<?xml version="1.0" encoding="UTF-8"?>\n' b'<component type="firmware"/>\n') with self.assertRaises(MetadataInvalid): ufile = UploadedFile() _add_version_formats(ufile) ufile.parse('foo.cab', cabarchive.save())
def _get_generated_metainfo(): txt = """<?xml version="1.0" encoding="utf-8"?> <component type="firmware"> <id> com.dell.tbt7d538854.firmware </id> <provides> <firmware type="flashed"> 7d538854-204d-51b2-8f9d-1fe881c70200 </firmware> </provides> <name> XPS 7390 Thunderbolt </name> <summary> Update for the Thunderbolt host controller in a XPS 7390 </summary> <description> <p> Updating the thunderbolt NVM improves performance and stability. </p> </description> <url type="homepage"> http://support.dell.com/ </url> <metadata_license> CC0-1.0 </metadata_license> <project_license> proprietary </project_license> <developer_name> Dell Inc. </developer_name> <requires> <id compare="ge" version="1.2.3">org.freedesktop.fwupd</id> <hardware> foo|bar|baz </hardware> <firmware compare="ge" version="0.2.3"/> <firmware compare="eq" version="0.0.1"> bootloader </firmware> </requires> <keywords> <keyword> thunderbolt </keyword> </keywords> <releases> <release timestamp="1561009099" version="41.01"> <checksum filename="0x0962_nonsecure.bin" target="content"/> </release> </releases> </component> """ return CabFile(txt.encode('utf-8'))
def test_invalid_xml_header(self): cabarchive = CabArchive() cabarchive['firmware.bin'] = _get_valid_firmware() cabarchive['firmware.metainfo.xml'] = CabFile(b'<!-- Copyright 2015 Richard Hughes <*****@*****.**> -->\n' b'<?xml version="1.0" encoding="UTF-8"?>\n' b'<component type="firmware"/>\n') with self.assertRaises(MetadataInvalid): ufile = UploadedFile() _add_version_formats(ufile) ufile.parse('foo.cab', cabarchive.save())
def test_extra_files(self): cabarchive = CabArchive() cabarchive['firmware.bin'] = _get_valid_firmware() cabarchive['firmware.metainfo.xml'] = _get_valid_metainfo() cabarchive['README.txt'] = CabFile(b'fubar') ufile = UploadedFile() ufile.parse('foo.cab', cabarchive.save()) cabarchive2 = ufile.cabarchive_repacked self.assertIsNotNone(cabarchive2['firmware.bin']) self.assertIsNotNone(cabarchive2['firmware.metainfo.xml']) with self.assertRaises(KeyError): self.assertIsNotNone(cabarchive2['README.txt'])
def test_valid_with_ignored_inf(self): cabarchive = CabArchive() cabarchive['firmware.bin'] = _get_valid_firmware() cabarchive['firmware.metainfo.xml'] = _get_valid_metainfo(enable_inf_parsing=False) cabarchive['firmware.inf'] = CabFile(b'fubar') ufile = UploadedFile() _add_version_formats(ufile) ufile.parse('foo.cab', cabarchive.save()) cabarchive2 = ufile.cabarchive_repacked self.assertIsNotNone(cabarchive2['firmware.bin']) self.assertIsNotNone(cabarchive2['firmware.metainfo.xml']) self.assertIsNotNone(cabarchive2['firmware.inf'])
def repack(arc: CabArchive, arg: str) -> None: with tempfile.TemporaryDirectory("cabarchive") as tmpdir: print("Extracting to {}".format(tmpdir)) subprocess.call( ["cabextract", "--fix", "--quiet", "--directory", tmpdir, arg]) for fn in glob.iglob(os.path.join(tmpdir, "**"), recursive=True): try: with open(fn, "rb") as f: fn_noprefix = fn[len(tmpdir) + 1:] print("Adding: {}".format(fn_noprefix)) arc[fn_noprefix] = CabFile(f.read()) except IsADirectoryError as _: pass
def archive_sign(self, cabarchive, cabfile): # already signed detached_fn = cabfile.filename + '.p7b' if detached_fn in cabarchive: return # create the detached signature blob_p7b = self._sign_blob(cabfile.buf) if not blob_p7b: return # add it to the archive cabarchive[detached_fn] = CabFile(blob_p7b.encode('utf-8'))
def archive_sign(self, cabarchive, cabfile): # already signed detached_fn = cabfile.filename + '.asc' if detached_fn in cabarchive: return # create the detached signature blob_asc = _sigul_detached_sign_data(cabfile.buf, self.get_setting('sign_sigul_config_file', required=True), self.get_setting('sign_sigul_firmware_key', required=True)) # add it to the archive cabarchive[detached_fn] = CabFile(blob_asc.encode('utf-8'))
def archive_sign(self, cabarchive, cabfile): # already signed detached_fn = cabfile.filename + '.asc' if detached_fn in cabarchive: return # create the detached signature affidavit = Affidavit( self.get_setting('sign_gpg_firmware_uid', required=True), self.get_setting('sign_gpg_keyring_dir', required=True)) contents_asc = str(affidavit.create(cabfile.buf)) # add it to the archive cabarchive[detached_fn] = CabFile(contents_asc.encode('utf-8'))
def archive_finalize(self, cabarchive, metadata): # does the readme file already exist? filename = self.get_setting('info_readme_filename', required=True) if filename in cabarchive: return # read in the file and do substititons try: with open(self.get_setting('info_readme_template', required=True), 'rb') as f: template = f.read().decode('utf-8') except IOError as e: raise PluginError(e) for key in metadata: template = template.replace(key, metadata[key]) # add it to the archive cabarchive[filename] = CabFile(template.encode('utf-8'))
def _repackage_archive(filename, buf, tmpdir=None, flattern=True): """ Unpacks an archive (typically a .zip) into a CabArchive object """ # write to temp file src = tempfile.NamedTemporaryFile(mode='wb', prefix='foreignarchive_', suffix=".zip", dir=tmpdir, delete=True) src.write(buf) src.flush() # decompress to a temp directory dest = tempfile.TemporaryDirectory(prefix='foreignarchive_') # work out what binary to use split = filename.rsplit('.', 1) if len(split) < 2: raise NotImplementedError('Filename not valid') if split[1] == 'zip': argv = ['/usr/bin/bsdtar', '--directory', dest.name, '-xvf', src.name] else: raise NotImplementedError('Filename had no supported extension') # bail out early if not os.path.exists(argv[0]): raise IOError('command %s not found' % argv[0]) # extract ps = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if ps.wait() != 0: raise IOError('Failed to extract: %s' % ps.stderr.read()) # add all the fake CFFILE objects cabarchive = CabArchive() for fn in glob.glob(dest.name + '/**/*.*', recursive=True): with open(fn, 'rb') as f: fn = fn.replace('\\', '/') if flattern: fn = os.path.basename(fn) cabarchive[fn] = CabFile(f.read()) return cabarchive
def _get_valid_inf(): txt = """[Version] Class=Firmware ClassGuid={f2e7dd72-6468-4e36-b6f1-6488f42c1b52} DriverVer=04/18/2015,2.0.3 [Firmware_CopyFiles] firmware.bin [Firmware_AddReg] HKR,,FirmwareId,,{2082b5e0-7a64-478a-b1b2-e3404fab6dad} HKR,,FirmwareVersion,%REG_DWORD%,0x0000000 HKR,,FirmwareFilename,,firmware.bin [Strings] Provider = "Hughski" MfgName = "Hughski Limited" FirmwareDesc = "ColorHug2 Firmware" DiskName = "Firmware for the ColorHug2 Colorimeter" """ return CabFile(txt.encode('utf-8'))
def _get_alternate_metainfo(): txt = """<?xml version="1.0" encoding="UTF-8"?> <!-- Copyright 2019 Richard Hughes <*****@*****.**> --> <component type="firmware"> <id>com.hughski.ColorHug.firmware</id> <name>ColorHug</name> <summary>Firmware for the ColorHug</summary> <description><p>Updating the firmware improves performance.</p></description> <provides> <firmware type="flashed">84f40464-9272-4ef7-9399-cd95f12da696</firmware> </provides> <url type="homepage">http://www.hughski.com/</url> <metadata_license>CC0-1.0</metadata_license> <project_license>proprietary</project_license> <developer_name>Hughski Limited</developer_name> <releases> <release version="1.2.3" date="2019-07-02"> <description><p>This stable release fixes bugs</p></description> </release> </releases> </component> """ return CabFile(txt.encode('utf-8'))
def _sign_fw(fw): # load the .cab file download_dir = app.config['DOWNLOAD_DIR'] fn = os.path.join(download_dir, fw.filename) try: with open(fn, 'rb') as f: cabarchive = CabArchive(f.read()) except IOError as e: raise NotImplementedError('cannot read %s: %s' % (fn, str(e))) # create Jcat file jcatfile = JcatFile() # sign each component in the archive print('Signing: %s' % fn) for md in fw.mds: try: # create Jcat item with SHA1 and SHA256 checksum blob cabfile = cabarchive[md.filename_contents] jcatitem = jcatfile.get_item(md.filename_contents) jcatitem.add_blob(JcatBlobSha1(cabfile.buf)) jcatitem.add_blob(JcatBlobSha256(cabfile.buf)) # sign using plugins for blob in ploader.archive_sign(cabfile.buf): # add GPG only to archive for backwards compat with older fwupd if blob.kind == JcatBlobKind.GPG: fn_blob = md.filename_contents + '.' + blob.filename_ext cabarchive[fn_blob] = CabFile(blob.data) # add to Jcat file too jcatitem.add_blob(blob) except KeyError as _: raise NotImplementedError('no {} firmware found'.format( md.filename_contents)) # rewrite the metainfo.xml file to reflect latest changes and sign it for md in fw.mds: # write new metainfo.xml file component = _generate_metadata_mds([md], metainfo=True) blob_xml = b'<?xml version="1.0" encoding="UTF-8"?>\n' + \ ET.tostring(component, encoding='UTF-8', xml_declaration=False, pretty_print=True) _show_diff(cabarchive[md.filename_xml].buf, blob_xml) cabarchive[md.filename_xml].buf = blob_xml # sign it jcatitem = jcatfile.get_item(md.filename_xml) jcatitem.add_blob(JcatBlobSha1(blob_xml)) jcatitem.add_blob(JcatBlobSha256(blob_xml)) for blob in ploader.archive_sign(blob_xml): jcatitem.add_blob(blob) # write jcat file if jcatfile.items: cabarchive['firmware.jcat'] = CabFile(jcatfile.save()) # overwrite old file cab_data = cabarchive.save() with open(fn, 'wb') as f: f.write(cab_data) # inform the plugin loader ploader.file_modified(fn) # update the download size for md in fw.mds: md.release_download_size = len(cab_data) # update the database fw.checksum_signed_sha1 = hashlib.sha1(cab_data).hexdigest() fw.checksum_signed_sha256 = hashlib.sha256(cab_data).hexdigest() fw.signed_timestamp = datetime.datetime.utcnow() db.session.commit()
def _get_valid_firmware(): return CabFile('fubar'.ljust(1024).encode('utf-8'))
def test_create(self): # create new archive arc = CabArchive() arc.set_id = 0x0622 # first example cff = CabFile() cff.buf = (b"#include <stdio.h>\r\n\r\nvoid main(void)\r\n" b'{\r\n printf("Hello, world!\\n");\r\n}\r\n') cff.date = datetime.date(1997, 3, 12) cff.time = datetime.time(11, 13, 52) cff.is_arch = True arc["hello.c"] = cff # second example cff = CabFile() cff.buf = (b"#include <stdio.h>\r\n\r\nvoid main(void)\r\n" b'{\r\n printf("Welcome!\\n");\r\n}\r\n\r\n') cff.date = datetime.date(1997, 3, 12) cff.time = datetime.time(11, 15, 14) cff.is_arch = True arc["welcome.c"] = cff # verify data = arc.save(False) with open("/tmp/test.cab", "wb") as f: f.write(data) expected = ( b"\x4D\x53\x43\x46\x00\x00\x00\x00\xFD\x00\x00\x00\x00\x00\x00\x00" b"\x2C\x00\x00\x00\x00\x00\x00\x00\x03\x01\x01\x00\x02\x00\x00\x00" b"\x22\x06\x00\x00\x5E\x00\x00\x00\x01\x00\x00\x00\x4D\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x6C\x22\xBA\x59\x20\x00\x68\x65\x6C\x6C" b"\x6F\x2E\x63\x00\x4A\x00\x00\x00\x4D\x00\x00\x00\x00\x00\x6C\x22" b"\xE7\x59\x20\x00\x77\x65\x6C\x63\x6F\x6D\x65\x2E\x63\x00\xBD\x5A" b"\xA6\x30\x97\x00\x97\x00\x23\x69\x6E\x63\x6C\x75\x64\x65\x20\x3C" b"\x73\x74\x64\x69\x6F\x2E\x68\x3E\x0D\x0A\x0D\x0A\x76\x6F\x69\x64" b"\x20\x6D\x61\x69\x6E\x28\x76\x6F\x69\x64\x29\x0D\x0A\x7B\x0D\x0A" b"\x20\x20\x20\x20\x70\x72\x69\x6E\x74\x66\x28\x22\x48\x65\x6C\x6C" b"\x6F\x2C\x20\x77\x6F\x72\x6C\x64\x21\x5C\x6E\x22\x29\x3B\x0D\x0A" b"\x7D\x0D\x0A\x23\x69\x6E\x63\x6C\x75\x64\x65\x20\x3C\x73\x74\x64" b"\x69\x6F\x2E\x68\x3E\x0D\x0A\x0D\x0A\x76\x6F\x69\x64\x20\x6D\x61" b"\x69\x6E\x28\x76\x6F\x69\x64\x29\x0D\x0A\x7B\x0D\x0A\x20\x20\x20" b"\x20\x70\x72\x69\x6E\x74\x66\x28\x22\x57\x65\x6C\x63\x6F\x6D\x65" b"\x21\x5C\x6E\x22\x29\x3B\x0D\x0A\x7D\x0D\x0A\x0D\x0A") _check_range(bytearray(data), bytearray(expected)) # use cabextract to test validity try: self.assertEqual( subprocess.call(["cabextract", "--test", "/tmp/test.cab"]), 0) except FileNotFoundError as _: pass # check we can parse what we just created arc = CabArchive() with open("/tmp/test.cab", "rb") as f: arc.parse(f.read()) # add an extra file arc["test.inf"] = CabFile(b"$CHICAGO$") # save with compression with open("/tmp/test.cab", "wb") as f: f.write(arc.save(True)) # use cabextract to test validity try: self.assertEqual( subprocess.call(["cabextract", "--test", "/tmp/test.cab"]), 0) except FileNotFoundError as _: pass
def test_cabfile_length(self): self.assertEqual(len(CabFile(b'foofoofoofoofoofoofoofoo')), 24)