def test_rotation(self): # Initializing a key repository results in this many keys. We don't # support max_active_keys being set any lower. min_active_keys = 2 # Simulate every rotation strategy up to "rotating once a week while # maintaining a year's worth of keys." for max_active_keys in range(min_active_keys, 52 + 1): self.config_fixture.config(group='fernet_tokens', max_active_keys=max_active_keys) # Ensure that resetting the key repository always results in 2 # active keys. self.useFixture( ksfixtures.KeyRepository(self.config_fixture, 'fernet_tokens', CONF.fernet_tokens.max_active_keys)) # Validate the initial repository state. self.assertRepositoryState(expected_size=min_active_keys) # The repository should be initialized with a staged key (0) and a # primary key (1). The next key is just auto-incremented. exp_keys = [0, 1] next_key_number = exp_keys[-1] + 1 # keep track of next key self.assertEqual(exp_keys, self.keys) # Rotate the keys just enough times to fully populate the key # repository. key_utils = token_utils.TokenUtils( CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') for rotation in range(max_active_keys - min_active_keys): key_utils.rotate_keys() self.assertRepositoryState(expected_size=rotation + 3) exp_keys.append(next_key_number) next_key_number += 1 self.assertEqual(exp_keys, self.keys) # We should have a fully populated key repository now. self.assertEqual(max_active_keys, self.key_repository_size) # Rotate an additional number of times to ensure that we maintain # the desired number of active keys. key_utils = token_utils.TokenUtils( CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') for rotation in range(10): key_utils.rotate_keys() self.assertRepositoryState(expected_size=max_active_keys) exp_keys.pop(1) exp_keys.append(next_key_number) next_key_number += 1 self.assertEqual(exp_keys, self.keys)
def key_repository_signature(self): """Create a "thumbprint" of the current key repository. Because key files are renamed, this produces a hash of the contents of the key files, ignoring their filenames. The resulting signature can be used, for example, to ensure that you have a unique set of keys after you perform a key rotation (taking a static set of keys, and simply shuffling them, would fail such a test). """ # Load the keys into a list, keys is list of six.text_type. key_utils = token_utils.TokenUtils( CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens' ) keys = key_utils.load_keys() # Sort the list of keys by the keys themselves (they were previously # sorted by filename). keys.sort() # Create the thumbprint using all keys in the repository. signature = hashlib.sha1() for key in keys: # Need to convert key to six.binary_type for update. signature.update(key.encode('utf-8')) return signature.hexdigest()
def test_rotation_disk_write_fail(self): # Make sure that the init key repository contains 2 keys self.assertRepositoryState(expected_size=2) key_utils = token_utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') # Simulate the disk full situation mock_open = mock.mock_open() file_handle = mock_open() file_handle.flush.side_effect = IOError('disk full') with mock.patch('keystone.common.token_utils.open', mock_open): self.assertRaises(IOError, key_utils.rotate_keys) # Assert that the key repository is unchanged self.assertEqual(self.key_repository_size, 2) with mock.patch('keystone.common.token_utils.open', mock_open): self.assertRaises(IOError, key_utils.rotate_keys) # Assert that the key repository is still unchanged, even after # repeated rotation attempts. self.assertEqual(self.key_repository_size, 2) # Rotate the keys normally, without any mocking, to show that the # system can recover. key_utils.rotate_keys() # Assert that the key repository is now expanded. self.assertEqual(self.key_repository_size, 3)
def get_multi_fernet_keys(): key_utils = token_utils.TokenUtils(CONF.credential.key_repository, MAX_ACTIVE_KEYS, 'credential') keys = key_utils.load_keys(use_null_key=True) fernet_keys = [fernet.Fernet(key) for key in keys] crypto = fernet.MultiFernet(fernet_keys) return crypto, keys
def test_non_numeric_files(self): evil_file = os.path.join(CONF.fernet_tokens.key_repository, '~1') with open(evil_file, 'w'): pass key_utils = token_utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') keys = key_utils.load_keys() self.assertEqual(2, len(keys)) self.assertValidFernetKeys(keys)
def setUp(self): super(KeyRepository, self).setUp() directory = self.useFixture(fixtures.TempDir()).path self.config_fixture.config(group=self.key_group, key_repository=directory) token_utils = utils.TokenUtils(directory, self.max_active_keys, self.key_group) token_utils.create_key_directory() token_utils.initialize_key_repository()
def symptom_usability_of_credential_fernet_key_repository(): """Credential key repository is not setup correctly. The credential Fernet key repository is expected to be readable by the user running keystone, but not world-readable, because it contains security sensitive secrets. """ token_utils = utils.TokenUtils(CONF.credential.key_repository, credential_fernet.MAX_ACTIVE_KEYS, 'credential') return ('fernet' in CONF.credential.provider and not token_utils.validate_key_repository())
def symptom_keys_in_Fernet_key_repository(): """Fernet key repository is empty. After configuring keystone to use the Fernet token provider, you should use `keystone-manage fernet_setup` to initially populate your key repository with keys, and periodically rotate your keys with `keystone-manage fernet_rotate`. """ token_utils = utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') return ('fernet' in CONF.token.provider and not token_utils.load_keys())
def symptom_usability_of_Fernet_key_repository(): """Fernet key repository is not setup correctly. The Fernet key repository is expected to be readable by the user running keystone, but not world-readable, because it contains security-sensitive secrets. """ token_utils = utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') return ('fernet' in CONF.token.provider and not token_utils.validate_key_repository())
def symptom_keys_in_credential_fernet_key_repository(): """Credential key repository is empty. After configuring keystone to use the Fernet credential provider, you should use `keystone-manage credential_setup` to initially populate your key repository with keys, and periodically rotate your keys with `keystone-manage credential_rotate`. """ token_utils = utils.TokenUtils(CONF.credential.key_repository, credential_fernet.MAX_ACTIVE_KEYS, 'credential') return ('fernet' in CONF.credential.provider and not token_utils.load_keys())
def test_non_numeric_files(self): evil_file = os.path.join(CONF.fernet_tokens.key_repository, '99.bak') with open(evil_file, 'w'): pass key_utils = token_utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') key_utils.rotate_keys() self.assertTrue(os.path.isfile(evil_file)) keys = 0 for x in os.listdir(CONF.fernet_tokens.key_repository): if x == '99.bak': continue keys += 1 self.assertEqual(3, keys)
def test_rotation_empty_file(self): active_keys = 2 self.assertRepositoryState(expected_size=active_keys) empty_file = os.path.join(CONF.fernet_tokens.key_repository, '2') with open(empty_file, 'w'): pass key_utils = token_utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') # Rotate the keys to overwrite the empty file key_utils.rotate_keys() self.assertTrue(os.path.isfile(empty_file)) keys = key_utils.load_keys() self.assertEqual(3, len(keys)) self.assertTrue(os.path.getsize(empty_file) > 0)
def test_debug_message_not_logged_when_loading_fernet_credential_key(self): self.useFixture( ksfixtures.KeyRepository(self.config_fixture, 'credential', CONF.fernet_tokens.max_active_keys)) logging_fixture = self.useFixture(fixtures.FakeLogger(level=log.DEBUG)) fernet_utilities = token_utils.TokenUtils( CONF.credential.key_repository, credential_fernet.MAX_ACTIVE_KEYS, 'credential') fernet_utilities.load_keys() debug_message = ( 'Loaded 2 Fernet keys from %(dir)s, but `[credential] ' 'max_active_keys = %(max)d`; perhaps there have not been enough ' 'key rotations to reach `max_active_keys` yet?') % { 'dir': CONF.credential.key_repository, 'max': credential_fernet.MAX_ACTIVE_KEYS } self.assertNotIn(debug_message, logging_fixture.output)
def test_debug_message_logged_when_loading_fernet_token_keys(self): self.useFixture( ksfixtures.KeyRepository(self.config_fixture, 'fernet_tokens', CONF.fernet_tokens.max_active_keys)) logging_fixture = self.useFixture(fixtures.FakeLogger(level=log.DEBUG)) fernet_utilities = token_utils.TokenUtils( CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') fernet_utilities.load_keys() expected_debug_message = ( 'Loaded 2 Fernet keys from %(dir)s, but `[fernet_tokens] ' 'max_active_keys = %(max)d`; perhaps there have not been enough ' 'key rotations to reach `max_active_keys` yet?') % { 'dir': CONF.fernet_tokens.key_repository, 'max': CONF.fernet_tokens.max_active_keys } self.assertIn(expected_debug_message, logging_fixture.output)
def decrypt(self, credential): """Attempt to decrypt a credential. :param credential: an encrypted credential string :returns: a decrypted credential """ key_utils = token_utils.TokenUtils(CONF.credential.key_repository, MAX_ACTIVE_KEYS) keys = key_utils.load_keys(use_null_key=True) fernet_keys = [fernet.Fernet(key) for key in keys] crypto = fernet.MultiFernet(fernet_keys) try: if isinstance(credential, six.text_type): credential = credential.encode('utf-8') return crypto.decrypt(credential).decode('utf-8') except (fernet.InvalidToken, TypeError, ValueError): msg = _('Credential could not be decrypted. Please contact the' ' administrator') LOG.error(msg) raise exception.CredentialEncryptionError(msg)
def crypto(self): """Return a cryptography instance. You can extend this class with a custom crypto @property to provide your own token encoding / decoding. For example, using a different cryptography library (e.g. ``python-keyczar``) or to meet arbitrary security requirements. This @property just needs to return an object that implements ``encrypt(plaintext)`` and ``decrypt(ciphertext)``. """ token_utils = utils.TokenUtils(CONF.fernet_tokens.key_repository, CONF.fernet_tokens.max_active_keys, 'fernet_tokens') keys = token_utils.load_keys() if not keys: raise exception.KeysNotFound() fernet_instances = [fernet.Fernet(key) for key in keys] return fernet.MultiFernet(fernet_instances)