def setUp(self): # Start both an HTTP and an HTTPS server running. The former is for # the zip files and the latter is for everything else. Vend them out # of a temporary directory which we load up with the right files. self._stack = ExitStack() try: self._serverdir = self._stack.enter_context(temporary_directory()) copy('winner.channels_01.json', self._serverdir, 'channels.json') sign(os.path.join(self._serverdir, 'channels.json'), 'image-signing.gpg') # Path B will win, with no bootme flags. self._indexpath = os.path.join('stable', 'nexus7', 'index.json') copy('winner.index_02.json', self._serverdir, self._indexpath) sign(os.path.join(self._serverdir, self._indexpath), 'image-signing.gpg') # Create every file in path B. The file contents will be the # checksum value. We need to create the signatures on the fly. setup_index('winner.index_02.json', self._serverdir, 'image-signing.gpg') self._stack.push( make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem')) self._stack.push(make_http_server(self._serverdir, 8980)) except: self._stack.close() raise
def test_load_channel_bad_signature_gets_fixed(self, config_d): # Like above, but the second download of the image signing key results # in a properly signed channels.json file. sign(self._channels_path, 'spare.gpg') setup_keyrings() self._state.run_thru('get_channel') # At this point, the state machine has determined that the # channels.json file is not signed with the cached image signing key, # so it will try to download a new imaging signing key. Let's put one # on the server, but it will not match the key that channels.json is # signed with. self.assertIsNone(self._state.channels) setup_keyring_txz( 'spare.gpg', 'image-master.gpg', dict(type='image-signing'), os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz')) # This will succeed by grabbing a new image-signing key. config = Configuration(config_d) with open(config.gpg.image_signing, 'rb') as fp: checksum = hashlib.md5(fp.read()).digest() next(self._state) with open(config.gpg.image_signing, 'rb') as fp: self.assertNotEqual(checksum, hashlib.md5(fp.read()).digest()) # The next state transition will find that the channels.json file is # properly signed. next(self._state) self.assertIsNotNone(self._state.channels) self.assertEqual( self._state.channels.daily.devices.nexus7.keyring.signature, '/daily/nexus7/device-keyring.tar.xz.asc')
def test_download_winners_signed_by_wrong_key(self): # There is a device key, but the image files are signed by the image # signing key, which according to the spec means the files are not # signed correctly. setup_keyrings() # To set up the device signing key, we need to load this channels.json # file and copy the device keyring to the server. copy('winner.channels_02.json', self._serverdir, 'channels.json') sign(os.path.join(self._serverdir, 'channels.json'), 'image-signing.gpg') setup_keyring_txz( 'device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz')) sign(os.path.join(self._serverdir, self._indexpath), 'device-signing.gpg') # All the downloadable files are now signed with a bogus key. setup_index('winner.index_02.json', self._serverdir, 'spare.gpg') touch_build(100) # Run the state machine until just before we download the files. state = State() state.run_until('download_files') # The next state transition will fail because of the missing signature. self.assertRaises(SignatureError, next, state) # There are no downloaded files. txtfiles = set(filename for filename in os.listdir(config.tempdir) if os.path.splitext(filename)[1] == '.txt') self.assertEqual(len(txtfiles), 0)
def test_signature_invalid(self): # The .validate() method raises a SignatureError exception with extra # information when the signature is invalid. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: dst = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), dst) # Get the dst's checksum now, because the file will get deleted # when the tmpdir context manager exits. with open(dst, 'rb') as fp: dst_checksum = hashlib.md5(fp.read()).hexdigest() with Context(dst) as ctx: with self.assertRaises(SignatureError) as cm: ctx.validate(channels_json + '.asc', channels_json) error = cm.exception basename = os.path.basename self.assertEqual(basename(error.signature_path), 'channels.json.asc') self.assertEqual(basename(error.data_path), 'channels.json') # The contents of the signature file are not predictable. with open(channels_json + '.asc', 'rb') as fp: checksum = hashlib.md5(fp.read()).hexdigest() self.assertEqual(error.signature_checksum, checksum) self.assertEqual( error.data_checksum, '715c63fecbf44b62f9fa04a82dfa7d29') basenames = [basename(path) for path in error.keyrings] self.assertEqual(basenames, ['image-signing.tar.xz']) self.assertIsNone(error.blacklist) self.assertEqual(error.keyring_checksums, [dst_checksum]) self.assertIsNone(error.blacklist_checksum)
def test_load_channel_good_path(self): # A channels.json file signed by the image signing key, no blacklist. sign(self._channels_path, 'image-signing.gpg') setup_keyrings() self._state.run_thru('get_channel') channels = self._state.channels self.assertEqual(channels.daily.devices.nexus7.keyring.signature, '/daily/nexus7/device-keyring.tar.xz.asc')
def setUp(self): self._stack = ExitStack() try: self._serverdir = self._stack.enter_context(temporary_directory()) copy('channel.channels_01.json', self._serverdir, 'channels.json') sign(os.path.join(self._serverdir, 'channels.json'), 'image-signing.gpg') except: self._stack.close() raise
def test_good_validation(self): # The .validate() method does nothing if the signature is good. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'image-signing.gpg') with temporary_directory() as tmpdir: keyring = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring) with Context(keyring) as ctx: self.assertIsNone( ctx.validate(channels_json + '.asc', channels_json))
def test_bad_signature(self): # In this case, the file is signed with the device key, so it will not # verify against the image signing key. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: dst = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), dst) with Context(dst) as ctx: self.assertFalse( ctx.verify(channels_json + '.asc', channels_json))
def test_good_signature(self): # We have a channels.json file signed with the imaging signing key, as # would be the case in production. The signature will match a context # loaded with the public key. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'image-signing.gpg') with temporary_directory() as tmpdir: keyring = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring) with Context(keyring) as ctx: self.assertTrue( ctx.verify(channels_json + '.asc', channels_json))
def test_good_signature_with_multiple_keyrings(self): # Like above, the file is signed with the device key, but this time we # include both the image signing and device signing pubkeys. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') with temporary_directory() as tmpdir: keyring_1 = os.path.join(tmpdir, 'image-signing.tar.xz') keyring_2 = os.path.join(tmpdir, 'device-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring_1) setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), keyring_2) with Context(keyring_1, keyring_2) as ctx: self.assertTrue( ctx.verify(channels_json + '.asc', channels_json))
def test_no_download_winners_with_bad_signature(self): # If one of the download files has a bad a signature, none of the # downloaded files are available. setup_keyrings() state = State() touch_build(100) # Break a signature sign(os.path.join(self._serverdir, '6', '7', '8.txt'), 'spare.gpg') # Run the state machine to calculate the winning path. state.run_until('download_files') # The next state transition will fail because of the missing signature. self.assertRaises(SignatureError, next, state) # There are no downloaded files. txtfiles = set(filename for filename in os.listdir(config.tempdir) if os.path.splitext(filename)[1] == '.txt') self.assertEqual(len(txtfiles), 0)
def test_download_winners_bad_checksums(self): # Similar to the various good paths, except because the checksums are # wrong in this index.json file, we'll get a error when downloading. copy('winner.index_01.json', self._serverdir, self._indexpath) sign(os.path.join(self._serverdir, self._indexpath), 'image-signing.gpg') setup_index('winner.index_01.json', self._serverdir, 'image-signing.gpg') setup_keyrings() state = State() touch_build(100) # Run the state machine until we're prepped to download state.run_until('download_files') # Now try to download the files and get the error. with self.assertRaises(FileNotFoundError) as cm: next(state) self.assertIn('HASH ERROR', str(cm.exception))
def test_bad_signature_with_validate(self, config): # This is similar to the above, except that the .validate() API is # used instead. channels_json = os.path.join(self._tmpdir, 'channels.json') channels_asc = channels_json + '.asc' copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: dst = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), dst) with Context(dst) as ctx: self.assertRaises(SignatureError, ctx.validate, channels_asc, channels_json) config.skip_gpg_verification = True ctx.validate(channels_asc, channels_json)
def test_bad_signature_with_multiple_keyrings(self): # The file is signed with the image master key, but it won't verify # against the image signing and device signing pubkeys. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'image-master.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: keyring_1 = os.path.join(tmpdir, 'image-signing.tar.xz') keyring_2 = os.path.join(tmpdir, 'device-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring_1) setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), keyring_2) with Context(keyring_1, keyring_2) as ctx: self.assertFalse( ctx.verify(channels_json + '.asc', channels_json))
def test_signature_invalid_due_to_blacklist(self): # Like above, but we put the device signing key id in the blacklist. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: keyring_1 = os.path.join(tmpdir, 'image-signing.tar.xz') keyring_2 = os.path.join(tmpdir, 'device-signing.tar.xz') blacklist = os.path.join(tmpdir, 'blacklist.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring_1) setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), keyring_2) # We're letting the device signing pubkey stand in for a blacklist. setup_keyring_txz('device-signing.gpg', 'image-master.gpg', dict(type='blacklist'), blacklist) # Get the keyring checksums now, because the files will get # deleted when the tmpdir context manager exits. keyring_checksums = [] for path in (keyring_1, keyring_2): with open(path, 'rb') as fp: checksum = hashlib.md5(fp.read()).hexdigest() keyring_checksums.append(checksum) with open(blacklist, 'rb') as fp: blacklist_checksum = hashlib.md5(fp.read()).hexdigest() with Context(keyring_1, keyring_2, blacklist=blacklist) as ctx: with self.assertRaises(SignatureError) as cm: ctx.validate(channels_json + '.asc', channels_json) error = cm.exception basename = os.path.basename self.assertEqual(basename(error.signature_path), 'channels.json.asc') self.assertEqual(basename(error.data_path), 'channels.json') # The contents of the signature file are not predictable. with open(channels_json + '.asc', 'rb') as fp: checksum = hashlib.md5(fp.read()).hexdigest() self.assertEqual(error.signature_checksum, checksum) self.assertEqual( error.data_checksum, '715c63fecbf44b62f9fa04a82dfa7d29') basenames = [basename(path) for path in error.keyrings] self.assertEqual(basenames, ['image-signing.tar.xz', 'device-signing.tar.xz']) self.assertEqual(basename(error.blacklist), 'blacklist.tar.xz') self.assertEqual(error.keyring_checksums, keyring_checksums) self.assertEqual(error.blacklist_checksum, blacklist_checksum)
def test_bad_signature(self, config): # In this case, the file is signed with the device key, so it will not # verify against the image signing key, unless the # --skip-gpg-verification flag is set. channels_json = os.path.join(self._tmpdir, 'channels.json') channels_asc = channels_json + '.asc' copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: dst = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), dst) with Context(dst) as ctx: self.assertFalse(ctx.verify(channels_asc, channels_json)) # But with the --skip-gpg-verification flag set, the verify # call returns success. config.skip_gpg_verification = True self.assertTrue(ctx.verify(channels_asc, channels_json))
def test_signature_error_logging(self): # The repr/str of the SignatureError should contain lots of useful # information that will make debugging easier. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. tmpdir = self._stack.enter_context(temporary_directory()) dst = os.path.join(tmpdir, 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), dst) output = StringIO() with Context(dst) as ctx: try: ctx.validate(channels_json + '.asc', channels_json) except SignatureError: # For our purposes, log.exception() is essentially a wrapper # around this traceback call. We don't really care about the # full stack trace though. e = sys.exc_info() traceback.print_exception(e[0], e[1], e[2], limit=0, file=output) # 2014-02-12 BAW: Yuck, but I can't get assertRegex() to work properly. for i, line in enumerate(output.getvalue().splitlines()): if i == 0: self.assertEqual(line, 'Traceback (most recent call last):') elif i == 1: self.assertEqual(line, 'systemimage.gpg.SignatureError: ') elif i == 2: self.assertTrue(line.startswith(' sig path :')) elif i == 3: self.assertTrue(line.endswith('/channels.json.asc')) elif i == 4: self.assertEqual( line, ' data path: 715c63fecbf44b62f9fa04a82dfa7d29') elif i == 5: self.assertTrue(line.endswith('/channels.json')) elif i == 6: self.assertTrue(line.startswith(' keyrings :')) elif i == 7: self.assertTrue(line.endswith("/image-signing.tar.xz']")) elif i == 8: self.assertEqual(line, ' blacklist: no blacklist ')
def test_download_winners_signed_by_device_key(self): # Check that all the winning path's files are downloaded, even when # they are signed by the device key instead of the image signing # master. setup_keyrings() # To set up the device signing key, we need to load channels_03.json # and copy the device keyring to the server. copy('winner.channels_02.json', self._serverdir, 'channels.json') sign(os.path.join(self._serverdir, 'channels.json'), 'image-signing.gpg') setup_keyring_txz( 'device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz')) # The index.json file and all the downloadable files must now be # signed with the device key. sign(os.path.join(self._serverdir, self._indexpath), 'device-signing.gpg') setup_index('winner.index_02.json', self._serverdir, 'device-signing.gpg') touch_build(100) # Run the state machine until we download the files. state = State() state.run_thru('download_files') # The B path files contain their checksums. def assert_file_contains(filename, contents): path = os.path.join(config.updater.cache_partition, filename) with open(path, encoding='utf-8') as fp: self.assertEqual(fp.read(), contents) assert_file_contains('5.txt', '345') assert_file_contains('6.txt', '456') assert_file_contains('7.txt', '567') # Delta B.1 files. assert_file_contains('8.txt', '678') assert_file_contains('9.txt', '789') assert_file_contains('a.txt', '89a') # Delta B.2 files. assert_file_contains('b.txt', '9ab') assert_file_contains('d.txt', 'fed') assert_file_contains('c.txt', 'edc')
def test_load_channel_blacklisted_signature(self, config_d): # We get an error if the signature on the channels.json file is good # but the key is blacklisted. sign(self._channels_path, 'image-signing.gpg') setup_keyrings() setup_keyring_txz( 'image-signing.gpg', 'image-master.gpg', dict(type='blacklist'), os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')) self._state.run_thru('get_channel') # We now have an image-signing key which is blacklisted. This will # cause the state machine to try to download a new image signing key, # so let's put the cached one up on the server. This will still be # backlisted though. config = Configuration(config_d) key_path = os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz') shutil.copy(config.gpg.image_signing, key_path) shutil.copy(config.gpg.image_signing + '.asc', key_path + '.asc') # Run the state machine through _get_channel() again, only this time # because the key is still blacklisted, we'll get an exception. self.assertRaises(SignatureError, self._state.run_thru, 'get_channel')
def test_bad_signature_in_blacklist(self): # Like above, but we put the device signing key id in the blacklist. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: keyring_1 = os.path.join(tmpdir, 'image-signing.tar.xz') keyring_2 = os.path.join(tmpdir, 'device-signing.tar.xz') blacklist = os.path.join(tmpdir, 'blacklist.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring_1) setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), keyring_2) # We're letting the device signing pubkey stand in for a blacklist. setup_keyring_txz('device-signing.gpg', 'image-master.gpg', dict(type='blacklist'), blacklist) with Context(keyring_1, keyring_2, blacklist=blacklist) as ctx: self.assertFalse( ctx.verify(channels_json + '.asc', channels_json))
def test_load_channel_bad_signature(self): # We get an error if the signature on the channels.json file is bad. sign(self._channels_path, 'spare.gpg') setup_keyrings() self._state.run_thru('get_channel') # At this point, the state machine has determined that the # channels.json file is not signed with the cached image signing key, # so it will try to download a new imaging signing key. Let's put one # on the server, but it will not match the key that channels.json is # signed with. key_path = os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), key_path) # This will succeed by grabbing a new image-signing key. from systemimage.testing.controller import stop_downloader stop_downloader(SystemImagePlugin.controller) next(self._state) # With the next state transition, we'll go back to trying to get the # channel.json file. Since the signature will still be bad, we'll get # a SignatureError this time. self.assertRaises(SignatureError, next, self._state)
def test_good_signature_not_in_blacklist(self): # We sign the file with the device signing key, and verify it against # the imaging signing and device signing keyrings. In this case # though, we also have a blacklist keyring, but none of the keyids in # the blacklist match the keyid that the file was signed with. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst='channels.json') sign(channels_json, 'device-signing.gpg') # Verify the signature with the pubkey. with temporary_directory() as tmpdir: keyring_1 = os.path.join(tmpdir, 'image-signing.tar.xz') keyring_2 = os.path.join(tmpdir, 'device-signing.tar.xz') blacklist = os.path.join(tmpdir, 'blacklist.tar.xz') setup_keyring_txz('image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), keyring_1) setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), keyring_2) setup_keyring_txz('spare.gpg', 'image-master.gpg', dict(type='blacklist'), blacklist) with Context(keyring_1, keyring_2, blacklist=blacklist) as ctx: self.assertTrue( ctx.verify(channels_json + '.asc', channels_json))
def test_get_details(self): # Get the details of an available update. self._setup_server_keyrings() # Index 14 has a more interesting upgrade path, and will yield a # richer description set. index_dir = Path(self._serverdir) / self.CHANNEL / self.DEVICE index_path = index_dir / 'index.json' copy('api.index_02.json', index_dir, 'index.json') sign(index_path, 'device-signing.gpg') setup_index('api.index_02.json', self._serverdir, 'device-signing.gpg') # Get the descriptions. update = Mediator().check_for_update() self.assertTrue(update.is_available) self.assertEqual(update.size, 180009) self.assertEqual(len(update.descriptions), 3) # The first contains the descriptions for the full update. self.assertEqual(update.descriptions[0], { 'description': 'Full B', 'description-en': 'The full B', }) # The first delta. self.assertEqual( update.descriptions[1], { 'description': 'Delta B.1', 'description-en_US': 'This is the delta B.1', 'description-xx': 'XX This is the delta B.1', 'description-yy': 'YY This is the delta B.1', 'description-yy_ZZ': 'YY-ZZ This is the delta B.1', }) # The second delta. self.assertEqual( update.descriptions[2], { 'description': 'Delta B.2', 'description-xx': 'Oh delta, my delta', 'description-xx_CC': 'This hyar is the delta B.2', })
def _copysign(self, src, dst, keyring): server_dst = os.path.join(self._serverdir, dst) makedirs(os.path.dirname(server_dst)) copy(src, self._serverdir, dst) sign(server_dst, keyring)