From c5c9d73faec89d6f380ad2bae191138656967e95 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 8 Mar 2018 11:08:47 -0500 Subject: [PATCH] Add 'prompt' arg to import rsa privatekey function Add an optional boolean 'prompt' argument to interface.import_rsa_privatekey_from_file and change the behavior of the function like so: If password is passed use passed password for decryption. If prompt is True use entered password for decryption. If no password is passed or entered, or if the entered password is an empty string, omit decryption. Passing and prompting for a passowrd is not possible. See code comments or secure-systems-lab/securesystemslib#122 for more details. This commit also adopts the unit tests accordingly. --- securesystemslib/interface.py | 92 +++++++++++++++++++++++++---------- tests/test_interface.py | 21 ++++++-- 2 files changed, 82 insertions(+), 31 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 9fddd60c..d0757cb6 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -237,11 +237,20 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, def import_rsa_privatekey_from_file(filepath, password=None, - scheme='rsassa-pss-sha256'): + scheme='rsassa-pss-sha256', prompt=False): """ - Import the encrypted PEM file in 'filepath', decrypt it, and return the key - object in 'securesystemslib.formats.RSAKEY_SCHEMA' format. + Import the PEM file in 'filepath' containing the private key. + + If password is passed use passed password for decryption. + If prompt is True use entered password for decryption. + If no password is passed or entered, or if the entered password is an empty + string, omit decryption. + + Passing and prompting for a passowrd is not possible. + + The returned key is an object in the + 'securesystemslib.formats.RSAKEY_SCHEMA' format. filepath: @@ -254,18 +263,35 @@ def import_rsa_privatekey_from_file(filepath, password=None, scheme: The signature scheme used by the imported key. + prompt: + If True the user is prompted for a passphrase to decrypt 'filepath'. + Default is False. + + ValueError, if 'password' is passed and 'prompt' is True. + + ValueError, if 'password' is passed and it is an empty string. + securesystemslib.exceptions.FormatError, if the arguments are improperly formatted. - securesystemslib.exceptions.CryptoError, if 'filepath' is not a valid - encrypted key file. + securesystemslib.exceptions.FormatError, if the entered password is + improperly formatted. + + IOError, if 'filepath' can't be loaded. + + securesystemslib.exceptions.CryptoError, if a password is available + and 'filepath' is not a valid key file encrypted using that password. + + securesystemslib.exceptions.CryptoError, if no password is available + and 'filepath' is not a valid non-encrypted key file. - The contents of 'filepath' is read, decrypted, and the key stored. + The contents of 'filepath' are read, optionally decrypted, and returned. An RSA key object, conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'. + """ # Does 'filepath' have the correct format? @@ -277,37 +303,51 @@ def import_rsa_privatekey_from_file(filepath, password=None, # Is 'scheme' properly formatted? securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme) - # If the caller does not provide a password argument, prompt for one. - # Password confirmation disabled here, which should ideally happen only - # when creating encrypted key files (i.e., improve usability). - if password is None: # pragma: no cover + if password and prompt: + raise ValueError("Passing 'password' and 'prompt' True is not allowed.") + + # If 'password' was passed check format and that it is not empty. + if password is not None: + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + # TODO: PASSWORD_SCHEMA should be securesystemslib.schema.AnyString(min=1) + if not len(password): + raise ValueError('Password must be 1 or more charcters') + + elif prompt: # pragma: no cover + # Password confirmation disabled here, which should ideally happen only + # when creating encrypted key files (i.e., improve usability). # It is safe to specify the full path of 'filepath' in the prompt and not # worry about leaking sensitive information about the key's location. # However, care should be taken when including the full path in exceptions # and log files. - password = get_password('Enter a password for the encrypted RSA' - ' file (' + Fore.RED + filepath + Fore.RESET + '): ', - confirm=False) + # NOTE: A user who gets prompted for a password, can only signal that the + # key is not encrypted by entering no password in the prompt, as opposed + # to a programmer who can call the function with or without a 'password'. + # Hence, we treat an empty password here, as if no 'password' was passed. + password = get_password('Enter a password for an encrypted RSA' + ' file \'' + Fore.RED + filepath + Fore.RESET + '\': ', + confirm=False) or None + + if password is not None: + # This check will not fail, because a mal-formatted passed password fails + # above and an entered password will always be a string (see get_password) + # However, we include it in case PASSWORD_SCHEMA or get_password changes. + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) - # Does 'password' have the correct format? - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + else: + logger.debug('No password was given. Attempting to import an' + ' unencrypted file.') - # Read the contents of 'filepath' that should be an encrypted PEM. + # Read the contents of 'filepath' that should be a PEM formatted private key. with open(filepath, 'rb') as file_object: - pem = file_object.read().decode('utf-8') + pem_key = file_object.read().decode('utf-8') # Convert 'pem_key' to 'securesystemslib.formats.RSAKEY_SCHEMA' format. # Raise 'securesystemslib.exceptions.CryptoError' if 'pem_key' is invalid. - if len(password): - rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem, - scheme, password) - - else: - logger.debug('An empty password was given. Attempting to import an' - ' unencrypted file.') - rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem, - scheme, password=None) + # If 'password' is None decryption will be omitted. + rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem_key, + scheme, password) return rsa_key diff --git a/tests/test_interface.py b/tests/test_interface.py index d317e093..97b4eb05 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -116,9 +116,8 @@ def test_generate_and_write_rsa_keypair(self): 'pw') self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(imported_privkey)) - # Try to import the unencrypted key file. - imported_privkey = interface.import_rsa_privatekey_from_file(test_keypath_unencrypted, - '') + # Try to import the unencrypted key file, by not passing a password + interface.import_rsa_privatekey_from_file(test_keypath_unencrypted) self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(imported_privkey)) # Custom 'bits' argument. @@ -167,7 +166,7 @@ def test_import_rsa_privatekey_from_file(self): # Load one of the pre-generated key files from # 'securesystemslib/tests/repository_data'. 'password' unlocks the # pre-generated key files. - key_filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), + key_filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', 'keystore', 'rsa_key') self.assertTrue(os.path.exists(key_filepath)) @@ -176,10 +175,22 @@ def test_import_rsa_privatekey_from_file(self): self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(imported_rsa_key)) - # Test improperly formatted argument. + # Test improperly formatted 'filepath' argument. self.assertRaises(securesystemslib.exceptions.FormatError, interface.import_rsa_privatekey_from_file, 3, 'pw') + # Test improperly formatted 'password' argument. + with self.assertRaises(securesystemslib.exceptions.FormatError): + interface.import_rsa_privatekey_from_file(key_filepath, 123) + + # Test unallowed empty 'password' + with self.assertRaises(ValueError): + interface.import_rsa_privatekey_from_file(key_filepath, '') + + # Test unallowed passing 'prompt' and 'password' + with self.assertRaises(ValueError): + interface.import_rsa_privatekey_from_file(key_filepath, + password='pw', prompt=True) # Test invalid argument. # Non-existent key file.