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(self): # Check that all the winning path's files are downloaded. setup_keyrings() state = State() touch_build(100) # Run the state machine until we download the files. 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_download_winners_overwrite(self): # Check that all the winning path's files are downloaded, even if # those files already exist in their destination paths. setup_keyrings() state = State() touch_build(100) # Run the state machine until we download the files. for basename in '56789abcd': base = os.path.join(config.updater.cache_partition, basename) path = base + '.txt' with open(path, 'w', encoding='utf-8') as fp: print('stale', file=fp) 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_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_bad_signature(self): # Both files are downloaded, but the signature does not match the # image-master key. setup_keyrings() # Use the spare key as the blacklist, signed by itself. Since this # won't match the image-signing key, the check will fail. server_path = os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz') setup_keyring_txz('spare.gpg', 'spare.gpg', dict(type='blacklist'), server_path) with self.assertRaises(SignatureError) as cm: get_keyring('blacklist', 'gpg/blacklist.tar.xz', 'image-master') error = cm.exception # The local file name will be keyring.tar.xz in the cache directory. basename = os.path.basename self.assertEqual(basename(error.data_path), 'keyring.tar.xz') self.assertEqual(basename(error.signature_path), 'keyring.tar.xz.asc') # The crafted blacklist.tar.xz file will have an unpredictable # checksum due to tarfile variablility. with open(server_path, 'rb') as fp: checksum = hashlib.md5(fp.read()).hexdigest() self.assertEqual(error.data_checksum, checksum) # The signature file's checksum is also unpredictable. with open(server_path + '.asc', 'rb') as fp: checksum = hashlib.md5(fp.read()).hexdigest() self.assertEqual(error.signature_checksum, checksum)
def test_archive_and_image_masters(self): # There is also a system image master key which is also persistent, # mandatory, shipped, and non-expiring. It should never need # changing, but it is possible to do so if it gets compromised. setup_keyrings() keyrings = [ config.gpg.archive_master, config.gpg.image_master, ] with Context(*keyrings) as ctx: # The context now knows about two keys. self.assertEqual( ctx.fingerprints, set(['289518ED3A0C4CFE975A0B32E0979A7EADE8E880', '47691DEF271FB2B1FD3364513BC6AF1818E7F5FB'])) self.assertEqual( ctx.key_ids, set(['E0979A7EADE8E880', '3BC6AF1818E7F5FB'])) # Here are all the available uids. uids = [] for key in ctx.keys: uids.extend(key['uids']) self.assertEqual(uids, [ 'Ubuntu Archive Master Signing Key (TEST) ' '<*****@*****.**>', 'Ubuntu System Image Master Signing Key (TEST) ' '<*****@*****.**>' ])
def test_archive_image_masters_image_signing(self): # In addition to the above, there is also a image signing key which is # generally what downloaded files are signed with. This key is also # persistent, mandatory, and shipped. It is updated regularly and # expires every two years. setup_keyrings() keyrings = [ config.gpg.archive_master, config.gpg.image_master, config.gpg.image_signing, ] with Context(*keyrings) as ctx: # The context now knows about two keys. self.assertEqual( ctx.fingerprints, set(['289518ED3A0C4CFE975A0B32E0979A7EADE8E880', '47691DEF271FB2B1FD3364513BC6AF1818E7F5FB', 'C5E39F07D159687BA3E82BD15A0DE8A4F1F1846F'])) self.assertEqual( ctx.key_ids, set(['E0979A7EADE8E880', '3BC6AF1818E7F5FB', '5A0DE8A4F1F1846F'])) # Here are all the available uids. uids = [] for key in ctx.keys: uids.extend(key['uids']) self.assertEqual(uids, [ 'Ubuntu Archive Master Signing Key (TEST) ' '<*****@*****.**>', 'Ubuntu System Image Master Signing Key (TEST) ' '<*****@*****.**>', 'Ubuntu System Image Signing Key (TEST) ' '<*****@*****.**>', ])
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 test_asc_file_missing(self): # If the tar.xz.asc file cannot be downloaded, an error is raised. tarxz_path = os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz') setup_keyrings() setup_keyring_txz('spare.gpg', 'archive-master.gpg', dict(type='blacklist'), tarxz_path) os.remove(tarxz_path + '.asc') self.assertRaises(FileNotFoundError, get_keyring, 'blacklist', 'gpg/blacklist.tar.xz', 'image-master')
def test_load_channel_over_https_port_with_http_fails(self): # We maliciously put an HTTP server on the HTTPS port. setup_keyrings() state = State() # Try to get the blacklist. This will fail silently since it's okay # not to find a blacklist. state.run_thru('get_blacklist_1') # This will fail to get the channels.json file. with make_http_server(self._serverdir, 8943): self.assertRaises(FileNotFoundError, next, state)
def test_good_path(self): # Everything checks out, with the simplest possible keyring.json. setup_keyrings('archive-master') setup_keyring_txz( 'spare.gpg', 'archive-master.gpg', dict(type='image-master'), os.path.join(self._serverdir, 'gpg', 'image-master.tar.xz')) get_keyring('image-master', 'gpg/image-master.tar.xz', 'archive-master') with Context(config.gpg.archive_master) as ctx: self.assertEqual(ctx.fingerprints, set(['289518ED3A0C4CFE975A0B32E0979A7EADE8E880']))
def test_path_blacklist(self): # Get the blacklist keyring. setup_keyrings('archive-master', 'image-master') setup_keyring_txz( 'spare.gpg', 'image-master.gpg', dict(type='blacklist'), os.path.join(self._serverdir, 'gpg/blacklist.tar.xz')) url = 'gpg/blacklist.tar.xz'.format(config.channel, config.device) get_keyring('blacklist', url, 'image-master') blacklist_path = os.path.join(config.tempdir, 'blacklist.tar.xz') with Context(blacklist_path) as ctx: self.assertEqual(ctx.fingerprints, set(['94BE2CECF8A5AF9F3A10E2A6526B7016C3D2FB44']))
def test_bad_json_type(self): # This type, while the signatures match, the keyring type in the # keyring.json file does not match. setup_keyrings() setup_keyring_txz( 'device-signing.gpg', 'image-master.gpg', dict(type='master'), os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')) with self.assertRaises(KeyringError) as cm: get_keyring('blacklist', 'gpg/blacklist.tar.xz', 'image-master') self.assertEqual( cm.exception.message, 'keyring type mismatch; wanted: blacklist, got: master')
def test_bad_json_model(self): # Similar to above, but with a non-matching model name. setup_keyrings() setup_keyring_txz( 'device-signing.gpg', 'image-master.gpg', dict(type='blacklist', model='nexus0'), os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')) with self.assertRaises(KeyringError) as cm: get_keyring('blacklist', 'gpg/blacklist.tar.xz', 'image-master') self.assertEqual( cm.exception.message, 'keyring model mismatch; wanted: nexus7, got: nexus0')
def test_good_path_model(self): # Everything checks out with the model specified. setup_keyrings() setup_keyring_txz( 'spare.gpg', 'archive-master.gpg', dict(type='image-master', model='nexus7'), os.path.join(self._serverdir, 'gpg', 'image-master.tar.xz')) get_keyring('image-master', 'gpg/image-master.tar.xz', 'archive-master') with Context(config.gpg.archive_master) as ctx: self.assertEqual(ctx.fingerprints, set(['289518ED3A0C4CFE975A0B32E0979A7EADE8E880']))
def test_calculate_winner(self): # Calculate the winning upgrade path. setup_keyrings() state = State() touch_build(100) # Run the state machine long enough to get the candidates and winner. state.run_thru('calculate_winner') # There are three candidate upgrade paths. descriptions = [] for image in state.winner: # There's only one description per image so order doesn't matter. descriptions.extend(image.descriptions.values()) self.assertEqual(descriptions, ['Full B', 'Delta B.1', 'Delta B.2'])
def test_path_device_signing_keyring(self): # Get the device signing keyring. setup_keyrings('archive-master', 'image-master', 'image-signing') setup_keyring_txz( 'spare.gpg', 'image-signing.gpg', dict(type='device-signing'), os.path.join(self._serverdir, 'gpg', 'stable', 'nexus7', 'device-signing.tar.xz')) url = 'gpg/{}/{}/device-signing.tar.xz'.format(config.channel, config.device) get_keyring('device-signing', url, 'image-signing') with Context(config.gpg.device_signing) as ctx: self.assertEqual(ctx.fingerprints, set(['94BE2CECF8A5AF9F3A10E2A6526B7016C3D2FB44']))
def test_good_path_expiry(self): # Everything checks out, with the expiration date specified. next_year = datetime.now(tz=timezone.utc) + timedelta(days=365) setup_keyrings('archive-master') setup_keyring_txz( 'spare.gpg', 'archive-master.gpg', dict(type='image-master', expiry=next_year.timestamp()), os.path.join(self._serverdir, 'gpg', 'image-master.tar.xz')) get_keyring('image-master', 'gpg/image-master.tar.xz', 'archive-master') with Context(config.gpg.archive_master) as ctx: self.assertEqual(ctx.fingerprints, set(['289518ED3A0C4CFE975A0B32E0979A7EADE8E880']))
def test_expired(self): # Similar to above, but the expiry key in the json names a utc # timestamp that has already elapsed. last_year = datetime.now(tz=timezone.utc) + timedelta(days=-365) setup_keyrings() setup_keyring_txz( 'device-signing.gpg', 'image-master.gpg', dict(type='blacklist', model='nexus7', expiry=last_year.timestamp()), os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')) with self.assertRaises(KeyringError) as cm: get_keyring('blacklist', 'gpg/blacklist.tar.xz', 'image-master') self.assertEqual(cm.exception.message, 'expired keyring timestamp')
def test_load_index_good_path(self): # Load the index.json pointed to by the channels.json. All signatures # validate correctly and there is no device keyring or blacklist. self._copysign('index.channels_05.json', 'channels.json', 'image-signing.gpg') # index.index_04.json path B will win, with no bootme flags. self._copysign('index.index_04.json', 'stable/nexus7/index.json', 'image-signing.gpg') setup_keyrings() state = State() state.run_thru('get_index') self.assertEqual( state.index.global_.generated_at, datetime(2013, 4, 29, 18, 45, 27, tzinfo=timezone.utc)) self.assertEqual(state.index.images[0].files[1].checksum, 'bcd')
def test_load_index_with_bad_keyring(self): # Here, the index.json file is signed with a defective device keyring. self._copysign('index.channels_02.json', 'channels.json', 'image-signing.gpg') # This will be signed by a keyring that is not the device keyring. self._copysign('index.index_04.json', 'stable/nexus7/index.json', 'spare.gpg') setup_keyrings() setup_keyring_txz( 'device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz')) state = State() state.run_until('get_index') self.assertRaises(SignatureError, next, state)
def test_missing_device(self): # The system's device does not exist. self._copysign('index.channels_04.json', 'channels.json', 'image-signing.gpg') # index.index_04.json path B will win, with no bootme flags. self._copysign('index.index_04.json', 'stable/nexus7/index.json', 'image-signing.gpg') setup_keyrings() # Our device (nexus7) isn't in the channels.json file, so there's # nothing to do. Running the state machine to its conclusion leaves # us with no index file. state = State() list(state) # There really is nothing left to do. self.assertIsNone(state.index)
def test_no_download_winners_with_missing_signature(self): # If one of the download files is missing a signature, none of the # files get downloaded and get_files() fails. setup_keyrings() state = State() touch_build(100) # Remove a signature. os.remove(os.path.join(self._serverdir, '6/7/8.txt.asc')) # 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(FileNotFoundError, 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, txtfiles)
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_destination_blacklist(self): # Like above, but the blacklist files end up in the temporary # directory, since it's never persistent. setup_keyrings('archive-master', 'image-master') setup_keyring_txz( 'spare.gpg', 'image-master.gpg', dict(type='blacklist'), os.path.join(self._serverdir, 'gpg', 'blacklist.tar.xz')) txz_path = os.path.join(config.updater.data_partition, 'blacklist.tar.xz') asc_path = txz_path + '.asc' self.assertFalse(os.path.exists(txz_path)) self.assertFalse(os.path.exists(asc_path)) get_keyring('blacklist', 'gpg/blacklist.tar.xz', 'image-master') self.assertTrue(os.path.exists(txz_path)) self.assertTrue(os.path.exists(asc_path)) with Context(config.gpg.image_master) as ctx: self.assertTrue(ctx.verify(asc_path, txz_path))
def test_destination_image_signing(self): # When a keyring is downloaded, we preserve its .tar.xz and # .tar.xz.asc files. setup_keyrings('archive-master', 'image-master') setup_keyring_txz( 'image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz')) asc_path = config.gpg.image_signing + '.asc' self.assertFalse(os.path.exists(config.gpg.image_signing)) self.assertFalse(os.path.exists(asc_path)) get_keyring('image-signing', 'gpg/image-signing.tar.xz', 'image-master') self.assertTrue(os.path.exists(config.gpg.image_signing)) self.assertTrue(os.path.exists(asc_path)) with Context(config.gpg.image_master) as ctx: self.assertTrue(ctx.verify(asc_path, config.gpg.image_signing))
def test_blacklisted_signature(self): # Normally, the signature would be good, except that the fingerprint # of the device signing key is blacklisted. setup_keyrings('archive-master', 'image-master') blacklist = os.path.join(config.tempdir, 'gpg', 'blacklist.tar.xz') # Blacklist the image-master keyring. setup_keyring_txz('image-master.gpg', 'image-master.gpg', dict(type='blacklist'), blacklist) setup_keyring_txz( 'image-signing.gpg', 'image-master.gpg', dict(type='image-signing'), os.path.join(self._serverdir, 'gpg', 'image-signing.tar.xz')) # Now put an image-signing key on the server and attempt to download # it. Because the image-master is blacklisted, this will fail. self.assertRaises(SignatureError, get_keyring, 'image-signing', 'gpg/image-signing.tar.xz', 'image-master', blacklist)
def test_load_index_with_device_keyring(self): # Here, the index.json file is signed with a device keyring. self._copysign('index.channels_02.json', 'channels.json', 'image-signing.gpg') # index.index_04.json.json path B will win, with no bootme flags. self._copysign('index.index_04.json', 'stable/nexus7/index.json', 'device-signing.gpg') setup_keyrings() setup_keyring_txz( 'device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), os.path.join(self._serverdir, 'stable', 'nexus7', 'device.tar.xz')) state = State() state.run_thru('get_index') self.assertEqual( state.index.global_.generated_at, datetime(2013, 4, 29, 18, 45, 27, tzinfo=timezone.utc)) self.assertEqual(state.index.images[0].files[1].checksum, 'bcd')
def test_archive_master(self): # The archive master keyring contains the master key. This a # persistent, mandatory, shipped, non-expiring key. setup_keyrings() with Context(config.gpg.archive_master) as ctx: # There is only one key in the master keyring. self.assertEqual( ctx.fingerprints, set(['289518ED3A0C4CFE975A0B32E0979A7EADE8E880'])) self.assertEqual( ctx.key_ids, set(['E0979A7EADE8E880'])) # Here is some useful information about the master key. self.assertEqual(len(ctx.keys), 1) master = ctx.keys[0] self.assertEqual( master['uids'], ['Ubuntu Archive Master Signing Key (TEST) ' '<*****@*****.**>'])