Skip to content

Commit

Permalink
Misc sslib.interface password/prompt enhancements (WIP)
Browse files Browse the repository at this point in the history
Fixes secure-systems-lab#122 together with secure-systems-lab#124 and secure-systems-lab#148
Prepares for fixing in-toto/in-toto#80

TODO (Maybe in separate PRs):

- support prompt arg for import_ecdsa_privatekey_from_file
  (see this commit)
- support prompt arg for interface.generate_and_write_*_keypair
- support password=None (no encryption) for
  generate_and_write_*_keypair (is there a way to check if
  encrypted?)

- add import_key_from_file(filepath, password=None, prompt=False, key_type=RSA), for pub/priv, rsa/ecdsa/ed25519
- add import_public_keys_from_file(filepaths, key_types=RSA), for pub/priv, rsa/ecdsa/ed25519
- add import_public_keys_from_gpg(keyids, gpg_home=None)

+ fix tests/docs/etc...
  • Loading branch information
lukpueh committed Mar 12, 2020
1 parent 19cab18 commit 73d1db1
Showing 1 changed file with 44 additions and 106 deletions.
150 changes: 44 additions & 106 deletions securesystemslib/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def get_password(prompt='Password: ', confirm=False):


def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS,
password=None):
password=None, prompt=True):
"""
<Purpose>
Generate an RSA key pair. The public portion of the generated RSA key is
Expand Down Expand Up @@ -287,51 +287,9 @@ def import_rsa_privatekey_from_file(filepath, password=None,
An RSA key object, conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'.
"""

# Does 'filepath' have the correct format?
# Ensure the arguments have the appropriate number of objects and object
# types, and that all dict keys are properly named.
# Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
securesystemslib.formats.PATH_SCHEMA.check_match(filepath)

# Is 'scheme' properly formatted?
securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme)

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 characters')

elif prompt:
# 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.
# 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 \'' + TERM_RED + filepath + TERM_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)

else:
logger.debug('No password was given. Attempting to import an'
' unencrypted file.')
password = _get_decryption_password(password, prompt, filepath)

# Read the contents of 'filepath' that should be a PEM formatted private key.
with open(filepath, 'rb') as file_object:
Expand Down Expand Up @@ -612,51 +570,17 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False):
<Returns>
An ed25519 key object of the form:
'securesystemslib.formats.ED25519KEY_SCHEMA'.
"""
# Does 'filepath' have the correct format?
# Ensure the arguments have the appropriate number of objects and object
# types, and that all dict keys are properly named.
# Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
"""
securesystemslib.formats.PATH_SCHEMA.check_match(filepath)

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 characters')

elif prompt:
# 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.
# 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 \'' + TERM_RED + filepath + TERM_RESET + '\': ',
confirm=False)

# If user sets an empty string for the password, explicitly set the
# password to None, because some functions may expect this later.
if len(password) == 0: # pragma: no cover
password = None
password = _get_decryption_password(password, prompt, filepath)

# Finally, regardless of password, try decrypting the key, if necessary.
# Otherwise, load it straight from the disk.
with open(filepath, 'rb') as file_object:
json_str = file_object.read()
return securesystemslib.keys.\
import_ed25519key_from_private_json(json_str, password=password)
return securesystemslib.keys.import_ed25519key_from_private_json(
json_str, password=password)



Expand Down Expand Up @@ -819,9 +743,7 @@ def import_ecdsa_publickey_from_file(filepath):





def import_ecdsa_privatekey_from_file(filepath, password=None):
def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False):
"""
<Purpose>
Import the encrypted ECDSA key file in 'filepath', decrypt it, and return
Expand All @@ -839,6 +761,10 @@ def import_ecdsa_privatekey_from_file(filepath, password=None):
encrypted key file 'filepath' must be decrypted before the ECDSA key
object can be returned.
prompt:
If True the user is prompted for a passphrase to decrypt 'filepath'.
Default is False.
<Exceptions>
securesystemslib.exceptions.FormatError, if the arguments are improperly
formatted or the imported key object contains an invalid key type (i.e.,
Expand All @@ -851,29 +777,10 @@ def import_ecdsa_privatekey_from_file(filepath, password=None):
<Returns>
An ECDSA key object of the form: 'securesystemslib.formats.ECDSAKEY_SCHEMA'.
"""
# Does 'filepath' have the correct format?
# Ensure the arguments have the appropriate number of objects and object
# types, and that all dict keys are properly named.
# Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
"""
securesystemslib.formats.PATH_SCHEMA.check_match(filepath)

# 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

# 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 ECDSA'
' key (' + TERM_RED + filepath + TERM_RESET + '): ',
confirm=False)

# Does 'password' have the correct format?
securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)
_get_decryption_password(password, prompt, filepath)

# Store the encrypted contents of 'filepath' prior to calling the decryption
# routine.
Expand Down Expand Up @@ -901,6 +808,37 @@ def import_ecdsa_privatekey_from_file(filepath, password=None):
return key_object


def _get_encryption_password(password, pompt, path):
pass


def _get_decryption_password(password, prompt, path):
"""Helper to prompt for and/or check a password. """

if password and prompt:
raise ValueError("passing 'password' and True for 'prompt' is not allowed")

if prompt:
password = get_password("enter password to decrypt private key file "
"'" + TERM_RED + path + TERM_RESET + "' "
"(leave empty if key not encrypted): '", confirm=False)

if len(password) == 0:
# A user who gets prompted for a password can only indicate that the key
# is not encrypted by entering no password in the prompt.
# To subsequently skip decryption we set the password to None.
password = None

# Do some vetting if a password was passed or entered by the user
if password is not None:
securesystemslib.formats.PASSWORD_SCHEMA.check_match(password)

if not len(password):
# TODO: It would be nice if PASSWORD_SCHEMA could do this
raise ValueError("'password' must be 1 or more characters long")

return password


if __name__ == '__main__':
# The interactive sessions of the documentation strings can
Expand Down

0 comments on commit 73d1db1

Please sign in to comment.