def __init__(self, logfile=None, loglevel='info'): self.loglevel = loglevel # Non-public. self._stack = ExitStack() self._stoppers = [] # Public. self.tmpdir = self._stack.enter_context(temporary_directory()) self.config_path = os.path.join(self.tmpdir, 'dbus-system.conf') self.serverdir = self._stack.enter_context(temporary_directory()) self.daemon_pid = None self.mode = 'live' self.udm_certs = '' self.curl_cert = '' self.patcher = None # Set up the dbus-daemon system configuration file. path = data_path('dbus-system.conf.in') with open(path, 'r', encoding='utf-8') as fp: template = fp.read() username = pwd.getpwuid(os.getuid()).pw_name config = template.format(tmpdir=self.tmpdir, user=username) with open(self.config_path, 'w', encoding='utf-8') as fp: fp.write(config) # We need a client.ini file for the subprocess. self.ini_tmpdir = self._stack.enter_context(temporary_directory()) self.ini_vardir = self._stack.enter_context(temporary_directory()) self.ini_logfile = (os.path.join(self.ini_tmpdir, 'client.log') if logfile is None else logfile) self.ini_path = os.path.join(self.tmpdir, 'config.d') makedirs(self.ini_path) self._reset_configs()
def test_date_unknown(self): # If there is no /userdata/.last_update file and no ini files, then # the last update date is unknown. with ExitStack() as stack: config_d = stack.enter_context(temporary_directory()) tempdir = stack.enter_context(temporary_directory()) userdata_path = os.path.join(tempdir, '.last_update') stack.enter_context( patch('systemimage.helpers.LAST_UPDATE_FILE', userdata_path)) config = Configuration(config_d) stack.enter_context(patch('systemimage.config._config', config)) self.assertEqual(last_update_date(), 'Unknown')
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_dangling_symlink(self, config): # LP: #1495688 reports a problem where /userdata/.last_update doesn't # exist, and the files in the config.d directory are dangling # symlinks. In this case, there's really little that can be done to # find a reliable last update date, but at least we don't crash. # # Start by deleting any existing .ini files in config.d. for path in Path(config.config_d).iterdir(): if path.suffix == '.ini': path.unlink() with ExitStack() as stack: tmpdir = stack.enter_context(temporary_directory()) userdata_path = Path(tmpdir) / '.last_update' stack.enter_context( patch('systemimage.helpers.LAST_UPDATE_FILE', str(userdata_path))) # Do not create the .last_update file. missing_ini = Path(tmpdir) / 'missing.ini' config.ini_files = [missing_ini] # Do not create the missing.ini file, but do create a symlink from # a config.d file to this missing file. default_ini = Path(config.config_d) / '00_default.ini' default_ini.symlink_to(missing_ini) last_update_date() self.assertEqual(last_update_date(), 'Unknown')
def sign(filename, pubkey_ring): """GPG sign the given file, producing an armored detached signature. :param filename: The path to the file to sign. :param pubkey_ring: The public keyring containing the key to sign the file with. This keyring must contain only one key, and its key id must exist in the master secret keyring. """ # filename could be a Path object. For now, just str-ify it. filename = str(filename) with ExitStack() as resources: home = resources.enter_context(temporary_directory()) secring = data_path('master-secring.gpg') pubring = data_path(pubkey_ring) ctx = gnupg.GPG( gnupghome=home, keyring=pubring, #verbose=True, secret_keyring=secring) public_keys = ctx.list_keys() assert len(public_keys) != 0, 'No keys found' assert len(public_keys) == 1, 'Too many keys' key_id = public_keys[0]['keyid'] dfp = resources.enter_context(open(filename, 'rb')) signed_data = ctx.sign_file(dfp, keyid=key_id, detach=True) sfp = resources.enter_context(open(filename + '.asc', 'wb')) sfp.write(signed_data.data)
def tempdir(self): if self._tempdir is None: makedirs(self.system.tempdir) self._tempdir = self._resources.enter_context( temporary_directory(prefix='system-image-', dir=self.system.tempdir)) return self._tempdir
def setUp(self): # Avoid circular imports. from systemimage.state import State self._resources = ExitStack() self._state = State() try: self._serverdir = self._resources.enter_context( temporary_directory()) # Start up both an HTTPS and HTTP server. The data files are # vended over the latter, everything else, over the former. self._resources.push( make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem')) self._resources.push(make_http_server(self._serverdir, 8980)) # Set up the server files. assert self.CHANNEL_FILE is not None, ( 'Subclasses must set CHANNEL_FILE') copy(self.CHANNEL_FILE, self._serverdir, 'channels.json') sign(os.path.join(self._serverdir, 'channels.json'), 'image-signing.gpg') assert self.CHANNEL is not None, 'Subclasses must set CHANNEL' assert self.DEVICE is not None, 'Subclasses must set DEVICE' index_path = os.path.join(self._serverdir, self.CHANNEL, self.DEVICE, 'index.json') head, tail = os.path.split(index_path) assert self.INDEX_FILE is not None, ( 'Subclasses must set INDEX_FILE') copy(self.INDEX_FILE, head, tail) sign(index_path, self.SIGNING_KEY) setup_index(self.INDEX_FILE, self._serverdir, self.SIGNING_KEY) except: self._resources.close() raise self.addCleanup(self._resources.close)
def setup_keyring_txz(keyring_src, signing_keyring, json_data, dst): """Set up the <keyring>.tar.xz and .asc files. The source keyring and json data is used to create a .tar.xz file and an associated .asc signature file. These are then copied to the given destination path name. :param keyring_src: The name of the source keyring (i.e. .gpg file), which should be relative to the test data directory. This will serve as the keyring.gpg file inside the tarball. :param signing_keyring: The name of the keyring to sign the resulting tarball with, again, relative to the test data directory. :param json_data: The JSON data dictionary, i.e. the contents of the keyring.json file inside the tarball. :param dst: The destination path of the .tar.xz file. For the resulting signature file, the .asc suffix will be automatically appended and copied next to the dst file. """ with temporary_directory() as tmpdir: copy(keyring_src, tmpdir, 'keyring.gpg') json_path = os.path.join(tmpdir, 'keyring.json') with open(json_path, 'w', encoding='utf-8') as fp: json.dump(json_data, fp) # Tar up the .gpg and .json files into a .tar.xz file. tarxz_path = os.path.join(tmpdir, 'keyring.tar.xz') with tarfile.open(tarxz_path, 'w:xz') as tf: tf.add(os.path.join(tmpdir, 'keyring.gpg'), 'keyring.gpg') tf.add(json_path, 'keyring.json') sign(tarxz_path, signing_keyring) # Copy the .tar.xz and .asc files to the proper directory under # the path the https server is vending them from. makedirs(os.path.dirname(dst)) shutil.copy(tarxz_path, dst) shutil.copy(tarxz_path + '.asc', dst + '.asc')
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 setUp(self): self._resources = ExitStack() tmpdir = self._resources.enter_context(temporary_directory()) self._mid_path = os.path.join(tmpdir, 'machine-id') self._resources.enter_context( patch('systemimage.helpers.UNIQUE_MACHINE_ID_FILES', [self._mid_path]))
def test_cancel(self): # Try to cancel the download of a big file. self.assertEqual(os.listdir(config.tempdir), []) with ExitStack() as stack: serverdir = stack.enter_context(temporary_directory()) stack.push(make_http_server(serverdir, 8980)) # Create a couple of big files to download. write_bytes(os.path.join(serverdir, 'bigfile_1.dat'), 10) write_bytes(os.path.join(serverdir, 'bigfile_2.dat'), 10) # The download service doesn't provide reliable cancel # granularity, so instead, we mock the 'started' signal to # immediately cancel the download. downloader = get_download_manager() def cancel_on_start(self, signal, path, started): if started: downloader.cancel() stack.enter_context(patch( 'systemimage.udm.DownloadReactor._do_started', cancel_on_start)) self.assertRaises( Canceled, downloader.get_files, _http_pathify([ ('bigfile_1.dat', 'bigfile_1.dat'), ('bigfile_2.dat', 'bigfile_2.dat'), ])) self.assertEqual(os.listdir(config.tempdir), [])
def setUp(self): self._stack = ExitStack() try: self._serverdir = self._stack.enter_context(temporary_directory()) self._stack.push( make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem')) except: self._stack.close() raise
def test_offset_no_file(self): with ExitStack() as stack: tmpdir = stack.enter_context(temporary_directory()) timekeep_file = Path(tmpdir) / 'timekeep' stack.enter_context( patch('systemimage.helpers.TIMEKEEPER_OFFSET_FILE', str(timekeep_file))) # Don't create the file self.assertEqual(get_android_offset(), 0)
def setUp(self): super().setUp() self._resources = ExitStack() try: self._serverdir = self._resources.enter_context( temporary_directory()) self._resources.push(make_http_server(self._serverdir, 8980)) except: self._resources.close() raise
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_offset_garbage_file(self): with ExitStack() as stack: tmpdir = stack.enter_context(temporary_directory()) timekeep_file = Path(tmpdir) / 'timekeep' stack.enter_context( patch('systemimage.helpers.TIMEKEEPER_OFFSET_FILE', str(timekeep_file))) timekeep_file.touch() with open(str(timekeep_file), 'w') as f: f.write("This is definitely not a timestamp") self.assertEqual(get_android_offset(), 0)
def setUp(self): # Start the HTTPS server running. Vend it 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()) self._stack.push( make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem')) 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_offset(self): with ExitStack() as stack: tmpdir = stack.enter_context(temporary_directory()) timekeep_file = Path(tmpdir) / 'timekeep' stack.enter_context( patch('systemimage.helpers.TIMEKEEPER_OFFSET_FILE', str(timekeep_file))) timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp()) timekeep_file.touch() with open(str(timekeep_file), 'w') as f: f.write(str(timestamp)) self.assertEqual(get_android_offset(), 1352560087)
def test_date_from_userdata(self): # The last upgrade data can come from /userdata/.last_update. with ExitStack() as stack: tmpdir = stack.enter_context(temporary_directory()) userdata_path = Path(tmpdir) / '.last_update' stack.enter_context( patch('systemimage.helpers.LAST_UPDATE_FILE', str(userdata_path))) timestamp = int(datetime(2012, 11, 10, 9, 8, 7).timestamp()) userdata_path.touch() os.utime(str(userdata_path), (timestamp, timestamp)) self.assertEqual(last_update_date(), '2012-11-10 09:08:07')
def _wrapper(self, function, ini_files, *args, **kws): start = 0 # It would be preferable to simply add a device='nexus7' argument, but that # causes 'decorator() takes 1 positional argument but 2 were given' device = kws.get('device', 'nexus7') with ExitStack() as resources: # Create the config.d directory and copy all the source ini files to # this directory in sequential order, interpolating in the temporary # tmp and var directories. config_d = resources.enter_context(temporary_directory()) temp_tmpdir = resources.enter_context(temporary_directory()) temp_vardir = resources.enter_context(temporary_directory()) for ini_file in ini_files: dst = os.path.join(config_d, '{:02d}_override.ini'.format(start)) start += 1 template = resource_bytes('systemimage.tests.data', ini_file).decode('utf-8') with atomic(dst) as fp: print(template.format(tmpdir=temp_tmpdir, vardir=temp_vardir), file=fp) # Patch the global configuration object so that it can be used # directly, which is good enough in most cases. Also patch the bit of # code that detects the device name. config = Configuration(config_d) resources.enter_context(patch('systemimage.config._config', config)) resources.enter_context( patch('systemimage.device.check_output', return_value=device)) # Make sure the cache_partition and data_partition exist. makedirs(config.updater.cache_partition) makedirs(config.updater.data_partition) # The method under test is allowed to specify some additional # keyword arguments, in order to pass some variables in from the # wrapper. signature = inspect.signature(function) if 'config_d' in signature.parameters: kws['config_d'] = config_d if 'config' in signature.parameters: kws['config'] = config # Call the function with the given arguments and return the result. return function(self, *args)
def setUp(self): self._stack = ExitStack() self._state = State() try: self._serverdir = self._stack.enter_context(temporary_directory()) self._stack.push( make_http_server(self._serverdir, 8943, 'cert.pem', 'key.pem')) copy('channel.channels_01.json', self._serverdir, 'channels.json') self._channels_path = os.path.join(self._serverdir, 'channels.json') except: self._stack.close() raise
def test_bad_not_even_a_signature(self): # The signature file isn't even a signature file. channels_json = os.path.join(self._tmpdir, 'channels.json') copy('gpg.channels_01.json', self._tmpdir, dst=channels_json) copy('gpg.channels_01.json', self._tmpdir, dst=channels_json + '.asc') with temporary_directory() as tmpdir: dst = os.path.join(tmpdir, 'device-signing.tar.xz') setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-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_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 __enter__(self): try: # Use a temporary directory for the $GNUPGHOME, but be sure to # arrange for the tempdir to be deleted no matter what. home = self._stack.enter_context( temporary_directory(prefix='si-gnupghome', dir=config.tempdir)) self._ctx = gnupg.GPG(gnupghome=home, keyring=self._keyrings) self._stack.callback(setattr, self, '_ctx', None) except: # pragma: no cover # Restore all context and re-raise the exception. self._stack.close() raise else: return self
def _use_cached_keyring(txz, asc, signing_key): if not _use_cached(txz, asc, (signing_key, )): return False # Do one additional check: unpack the .tar.xz file, grab the keyring.json # and if it has an expiry key, make sure that the keyring has not expired. with temporary_directory(dir=config.tempdir) as tmp: with tarfile.open(txz, 'r:xz') as tf: tf.extractall(tmp) json_path = os.path.join(tmp, 'keyring.json') with open(json_path, 'r', encoding='utf-8') as fp: data = json.load(fp) expiry = data.get('expiry') timestamp = datetime.now(tz=timezone.utc).timestamp() # We can use this keyring if it never expires, or if the expiration date # is some time in the future. return expiry is None or expiry > timestamp
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_bad_not_even_a_signature(self, config): # The signature file isn't even a signature file. Verification will # fail 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) copy('gpg.channels_01.json', self._tmpdir, dst=channels_json + '.asc') with temporary_directory() as tmpdir: dst = os.path.join(tmpdir, 'device-signing.tar.xz') setup_keyring_txz('device-signing.gpg', 'image-signing.gpg', dict(type='device-signing'), dst) with Context(dst) as ctx: self.assertFalse(ctx.verify(channels_asc, channels_json)) config.skip_gpg_verification = True self.assertTrue(ctx.verify(channels_asc, channels_json))
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)