diff --git a/README.rst b/README.rst index 5455f8a6..3d837679 100644 --- a/README.rst +++ b/README.rst @@ -83,9 +83,9 @@ text without prepended symbols is the output of a command. # If the key length is unspecified, it defaults to 3072 bits. A length of # less than 2048 bits raises an exception. A password may be supplied as an - # argument, otherwise a user prompt is presented. If the password is an - # empty string, the private key is saved unencrypted. - >>> generate_and_write_rsa_keypair("rsa_key2") + # argument like above, or on the prompt. If no password is passed or + # entered the private key is saved unencrypted. + >>> generate_and_write_rsa_keypair("rsa_key2", prompt=True) Enter a password for the RSA key: Confirm: @@ -134,10 +134,10 @@ Create and Import Ed25519 Keys # Continuing from the previous section . . . - # Generate and write an Ed25519 key pair. The private key is saved - # encrypted. A 'password' argument may be supplied, otherwise a prompt is - # presented. - >>> generate_and_write_ed25519_keypair('ed25519_key') + # Generate and write an Ed25519 key pair. A password may be supplied as an + # argument, or on the prompt. If no password is passed or entered the + # private key is saved unencrypted. + >>> generate_and_write_ed25519_keypair('ed25519_key', prompt=True) Enter a password for the Ed25519 key: Confirm: @@ -145,7 +145,7 @@ Create and Import Ed25519 Keys >>> public_ed25519_key = import_ed25519_publickey_from_file('ed25519_key.pub') # and its corresponding private key. - >>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key') + >>> private_ed25519_key = import_ed25519_privatekey_from_file('ed25519_key', prompt=True) Enter a password for the encrypted Ed25519 key: @@ -156,12 +156,12 @@ Create and Import ECDSA Keys # continuing from the previous sections . . . - >>> generate_and_write_ecdsa_keypair('ecdsa_key') + >>> generate_and_write_ecdsa_keypair('ecdsa_key', prompt=True) Enter a password for the ECDSA key: Confirm: >>> public_ecdsa_key = import_ecdsa_publickey_from_file('ecdsa_key.pub') - >>> private_ecdsa_key = import_ecdsa_privatekey_from_file('ecdsa_key') + >>> private_ecdsa_key = import_ecdsa_privatekey_from_file('ecdsa_key', prompt=True) Enter a password for the encrypted ECDSA key: diff --git a/securesystemslib/gpg/functions.py b/securesystemslib/gpg/functions.py index cbb27973..1133e8ac 100644 --- a/securesystemslib/gpg/functions.py +++ b/securesystemslib/gpg/functions.py @@ -232,43 +232,24 @@ def verify_signature(signature_object, pubkey_info, content): def export_pubkey(keyid, homedir=None): - """ - - Calls gpg command line utility to export the gpg public key bundle - identified by the passed keyid from the gpg keyring at the passed homedir - in a securesystemslib-style format. - - NOTE: The identified key is exported including the corresponding master - key and all subkeys. - - The executed base export command is defined in - securesystemslib.gpg.constants.GPG_EXPORT_PUBKEY_COMMAND. - - - keyid: - The GPG keyid in format: securesystemslib.formats.KEYID_SCHEMA - - homedir: (optional) - Path to the gpg keyring. If not passed the default keyring is used. - - - ValueError: - if the keyid does not match the required format. - - securesystemslib.exceptions.UnsupportedLibraryError: - If the gpg command is not available, or - the cryptography library is not installed. + """Exports a public key from a GnuPG keyring. - securesystemslib.gpg.execeptions.KeyNotFoundError: - if no key or subkey was found for that keyid. + Arguments: + keyid: An OpenPGP keyid in KEYID_SCHEMA format. + homedir (optional): A path to the GnuPG home directory. If not set the + default GnuPG home directory is used. + Raises: + ValueError: Keyid is not a string. + UnsupportedLibraryError: The gpg command or pyca/cryptography are not + available. + KeyNotFoundError: No key or subkey was found for that keyid. - - None. + Side Effects: + Calls system gpg command in a subprocess. - - The exported public key object in the format: - securesystemslib.formats.GPG_PUBKEY_SCHEMA. + Returns: + An OpenPGP public key object in GPG_PUBKEY_SCHEMA format. """ if not HAVE_GPG: # pragma: no cover @@ -302,7 +283,7 @@ def export_pubkey(keyid, homedir=None): def export_pubkeys(keyids, homedir=None): - """Export multiple public keys from a GnuPG keyring. + """Exports multiple public keys from a GnuPG keyring. Arguments: keyids: A list of OpenPGP keyids in KEYID_SCHEMA format. @@ -311,11 +292,18 @@ def export_pubkeys(keyids, homedir=None): Raises: TypeError: Keyids is not iterable. - See 'export_pubkey' for other exceptions. + ValueError: A Keyid is not a string. + UnsupportedLibraryError: The gpg command or pyca/cryptography are not + available. + KeyNotFoundError: No key or subkey was found for that keyid. + + Side Effects: + Calls system gpg command in a subprocess. Returns: - A dict with the OpenPGP keyids passed as the keyids argument for dict keys - and keys in GPG_PUBKEY_SCHEMA format for values. + A dict of OpenPGP public key objects in GPG_PUBKEY_SCHEMA format as values, + and their keyids as dict keys. + """ public_key_dict = {} diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 1fcd3813..f2ccac59 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -65,33 +65,17 @@ def get_password(prompt='Password: ', confirm=False): - """ - - Return the password entered by the user. If 'confirm' is True, the user is - asked to enter the previously entered password once again. If they match, - the password is returned to the caller. - - - prompt: - The text of the password prompt that is displayed to the user. - - confirm: - Boolean indicating whether the user should be prompted for the password - a second time. The two entered password must match, otherwise the - user is again prompted for a password. + """Prompts user to enter a password. - - None. + Arguments: + prompt (optional): A text displayed on the prompt (stderr). + confirm (optional): A boolean indicating if the user needs to enter the + same password twice. - - None. + Returns: + The password entered on the prompt. - - The password entered by the user. """ - - # Are the arguments the expected type? - # If not, raise 'securesystemslib.exceptions.FormatError'. securesystemslib.formats.TEXT_SCHEMA.check_match(prompt) securesystemslib.formats.BOOLEAN_SCHEMA.check_match(confirm) @@ -112,107 +96,154 @@ def get_password(prompt='Password: ', confirm=False): +def _get_key_file_encryption_password(password, prompt, path): + """Encryption password helper for `_generate_and_write_*_keypair` functions. - -def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, - password=None): + Combinations of 'password' and 'prompt' -> result (explanation) + ---------------------------------------------------------------- + None False -> return None (clear non-encryption desire) + "" False -> return password (clear encryption desire) + False -> raise (bad pw type, unclear encryption desire) + True -> raise (unclear password/prompt precedence) + None True -> prompt and return password if entered and None + otherwise (users on the prompt can only + indicate desire to not encrypt by entering no + password) """ - - Generate an RSA key pair. The public portion of the generated RSA key is - saved to <'filepath'>.pub, whereas the private key portion is saved to - <'filepath'>. If no password is given, the user is prompted for one. If - the 'password' is an empty string, the private key is saved unencrypted to - <'filepath'>. If the filepath is not given, the KEYID is used as the - filename and the keypair saved to the current working directory. - - The best available form of encryption, for a given key's backend, is used - with pyca/cryptography. According to their documentation, "it is a curated - encryption choice and the algorithm may change over time." - - - filepath: - The public and private key files are saved to .pub and - , respectively. If the filepath is not given, the public and - private keys are saved to the current working directory as .pub - and . KEYID is the generated key's KEYID. - - bits: - The number of bits of the generated RSA key. - - password: - The password to encrypt 'filepath'. If None, the user is prompted for a - password. If an empty string is given, the private key is written to - disk unencrypted. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - - Writes key files to '' and '.pub'. - - - The 'filepath' of the written key. + securesystemslib.formats.BOOLEAN_SCHEMA.check_match(prompt) + + # We don't want to decide which takes precedence so we fail + if password is not None and prompt: + raise ValueError("passing 'password' and 'prompt=True' is not allowed") + + # Prompt user for password and confirmation + if prompt: + password = get_password("enter password to encrypt private key file " + "'" + TERM_RED + str(path) + TERM_RESET + "' (leave empty if key " + "should not be encrypted): ", confirm=True) + + # Treat empty password as no password. A user on the prompt can only + # indicate the desire to not encrypt by entering no password. + if not len(password): + return None + + if password is not None: + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + + # Fail on empty passed password. A caller should pass None to indicate the + # desire to not encrypt. + if not len(password): + raise ValueError("encryption password must be 1 or more characters long") + + return password + + + +def _get_key_file_decryption_password(password, prompt, path): + """Decryption password helper for `import_*_privatekey_from_file` functions. + + Combinations of 'password' and 'prompt' -> result (explanation) + ---------------------------------------------------------------- + None False -> return None (clear non-decryption desire) + "" False -> return password (clear decryption desire) + False -> raise (bad pw type, unclear decryption desire) + True -> raise (unclear password/prompt precedence) + None True -> prompt and return password if entered and None + otherwise (users on the prompt can only indicate + desire to not decrypt by entering no password) + """ + securesystemslib.formats.BOOLEAN_SCHEMA.check_match(prompt) + + # We don't want to decide which takes precedence so we fail + if password is not None and prompt: + raise ValueError("passing 'password' and 'prompt=True' is not allowed") + + # Prompt user for password + if prompt: + password = get_password("enter password to decrypt private key file " + "'" + TERM_RED + str(path) + TERM_RESET + "' " + "(leave empty if key not encrypted): ", confirm=False) + + # Treat empty password as no password. A user on the prompt can only + # indicate the desire to not decrypt by entering no password. + if not len(password): + return None + + if password is not None: + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + # No additional vetting needed. Decryption will show if it was correct. + + return password + + - # Does 'bits' have the correct format? - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. +def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, + password=None, prompt=False): + """Generates RSA key pair and writes PEM-encoded keys to disk. + + If a password is passed or entered on the prompt, the private key is + encrypted. According to the documentation of the used pyca/cryptography + library, encryption is performed "using the best available encryption for a + given key's backend", which "is a curated encryption choice and the algorithm + may change over time." The private key is written in PKCS#1 and the public + key in X.509 SubjectPublicKeyInfo format. + + NOTE: A signing scheme can be assigned on key import (see import functions). + + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + bits (optional): The number of bits of the generated RSA key. + password (optional): An encryption password. + prompt (optional): A boolean indicating if the user should be prompted + for an encryption password. If the user enters an empty password, the + key is not encrypted. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: An empty string is passed as 'password', or both a 'password' + is passed and 'prompt' is true. + StorageError: Key files cannot be written. + + Side Effects: + Prompts user for a password if 'prompt' is True. + Writes key files to disk. + + Returns: + The private key filepath. + + """ securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(bits) - # Generate the public and private RSA keys. + # Generate private RSA key and extract public and private both in PEM rsa_key = securesystemslib.keys.generate_rsa_key(bits) public = rsa_key['keyval']['public'] private = rsa_key['keyval']['private'] + # Use passed 'filepath' or keyid as file name if not filepath: filepath = os.path.join(os.getcwd(), rsa_key['keyid']) - else: - logger.debug('The filepath has been specified. Not using the key\'s' - ' KEYID as the default filepath.') - - # Does 'filepath' have the correct format? securesystemslib.formats.PATH_SCHEMA.check_match(filepath) - # If the caller does not provide a password argument, prompt for one. - if password is None: + password = _get_key_file_encryption_password(password, prompt, filepath) - # 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' - ' key (' + TERM_RED + filepath + TERM_RESET + '): ', - confirm=True) - - else: - logger.debug('The password has been specified. Not prompting for one') - - # Does 'password' have the correct format? - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) - - # Encrypt the private key if 'password' is set. - if len(password): + # Encrypt the private key if a 'password' was passed or entered on the prompt + if password is not None: private = securesystemslib.keys.create_rsa_encrypted_pem(private, password) - else: - logger.debug('An empty password was given. Not encrypting the private key.') - - # If the parent directory of filepath does not exist, - # create it (and all its parent directories, if necessary). + # Create intermediate directories as required securesystemslib.util.ensure_parent_dir(filepath) - # Write the public key (i.e., 'public', which is in PEM format) to - # '.pub'. (1) Create a temporary file, (2) write the contents of - # the public key, and (3) move to final destination. + # Write PEM-encoded public key to .pub file_object = tempfile.TemporaryFile() file_object.write(public.encode('utf-8')) - # The temporary file is closed after the final move. securesystemslib.util.persist_temp_file(file_object, filepath + '.pub') - # Write the private key in encrypted PEM format to ''. - # Unlike the public key file, the private key does not have a file - # extension. + # Write PEM-encoded private key to file_object = tempfile.TemporaryFile() file_object.write(private.encode('utf-8')) securesystemslib.util.persist_temp_file(file_object, filepath) @@ -221,187 +252,191 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, +def generate_and_write_rsa_keypair(password, filepath=None, + bits=DEFAULT_RSA_KEY_BITS): + """Generates RSA key pair and writes PEM-encoded keys to disk. + + The private key is encrypted using the best available encryption algorithm + chosen by 'pyca/cryptography', which may change over time. The private key is + written in PKCS#1 and the public key in X.509 SubjectPublicKeyInfo format. + + NOTE: A signing scheme can be assigned on key import (see import functions). + + Arguments: + password: An encryption password. + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + bits (optional): The number of bits of the generated RSA key. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: An empty string is passed as 'password'. + StorageError: Key files cannot be written. + + Side Effects: + Writes key files to disk. + + Returns: + The private key filepath. -def import_rsa_privatekey_from_file(filepath, password=None, - scheme='rsassa-pss-sha256', prompt=False, - storage_backend=None): """ - - Import the PEM file in 'filepath' containing the private key. + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=password, prompt=False) + - If password is passed use passed password for decryption. - If prompt is True use entered password for decryption. - If no password is passed and either prompt is False or if the password - entered at the prompt is an empty string, omit decryption, treating the - key as if it is not encrypted. - If password is passed and prompt is True, an error is raised. (See below.) - The returned key is an object in the - 'securesystemslib.formats.RSAKEY_SCHEMA' format. +def generate_and_write_rsa_keypair_with_prompt(filepath=None, + bits=DEFAULT_RSA_KEY_BITS): + """Generates RSA key pair and writes PEM-encoded keys to disk. - - filepath: - file, an RSA encrypted PEM file. Unlike the public RSA PEM - key file, 'filepath' does not have an extension. + The private key is encrypted with a password entered on the prompt, using the + best available encryption algorithm chosen by 'pyca/cryptography', which may + change over time. The private key is written in PKCS#1 and the public key in + X.509 SubjectPublicKeyInfo format. - password: - The passphrase to decrypt 'filepath'. + NOTE: A signing scheme can be assigned on key import (see import functions). + + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + bits (optional): The number of bits of the generated RSA key. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - scheme: - The signature scheme used by the imported key. + Side Effects: + Prompts user for a password. + Writes key files to disk. - prompt: - If True the user is prompted for a passphrase to decrypt 'filepath'. - Default is False. + Returns: + The private key filepath. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + """ + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=None, prompt=True) - - 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. +def generate_and_write_unencrypted_rsa_keypair(filepath=None, + bits=DEFAULT_RSA_KEY_BITS): + """Generates RSA key pair and writes PEM-encoded keys to disk. - securesystemslib.exceptions.FormatError, if the entered password is - improperly formatted. + The private key is written in PKCS#1 and the public key in X.509 + SubjectPublicKeyInfo format. - IOError, if 'filepath' can't be loaded. + NOTE: A signing scheme can be assigned on key import (see import functions). - securesystemslib.exceptions.CryptoError, if a password is available - and 'filepath' is not a valid key file encrypted using that password. + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + bits (optional): The number of bits of the generated RSA key. - securesystemslib.exceptions.CryptoError, if no password is available - and 'filepath' is not a valid non-encrypted key file. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - - The contents of 'filepath' are read, optionally decrypted, and returned. + Side Effects: + Writes unencrypted key files to disk. - - An RSA key object, conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'. + Returns: + The private key filepath. """ + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=None, prompt=False) - # 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.") +def import_rsa_privatekey_from_file(filepath, password=None, + scheme='rsassa-pss-sha256', prompt=False, + storage_backend=None): + """Imports PEM-encoded RSA private key from file storage. - # If 'password' was passed check format and that it is not empty. - if password is not None: - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + The expected key format is PKCS#1. If a password is passed or entered on the + prompt, the private key is decrypted, otherwise it is treated as unencrypted. - # 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 + Arguments: + filepath: The path to read the file from. + password (optional): A password to decrypt the key. + scheme (optional): The signing scheme assigned to the returned key object. + See RSA_SCHEME_SCHEMA for available signing schemes. + prompt (optional): A boolean indicating if the user should be prompted + for a decryption password. If the user enters an empty password, the + key is not decrypted. + storage_backend (optional): An object implementing StorageBackendInterface. + If not passed a default FilesystemBackend will be used. - 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) + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: Both a 'password' is passed and 'prompt' is true. + StorageError: Key file cannot be read. + CryptoError: Key cannot be parsed. - else: - logger.debug('No password was given. Attempting to import an' - ' unencrypted file.') + Returns: + An RSA private key object conformant with 'RSAKEY_SCHEMA'. + + """ + securesystemslib.formats.PATH_SCHEMA.check_match(filepath) + securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme) + + password = _get_key_file_decryption_password(password, prompt, filepath) if storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() - # Read the contents of 'filepath' that should be a PEM formatted private key. with storage_backend.get(filepath) as file_object: 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 'password' is None decryption will be omitted. - rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem_key, - scheme, password) + # Optionally decrypt and convert PEM-encoded key to 'RSAKEY_SCHEMA' format + rsa_key = securesystemslib.keys.import_rsakey_from_private_pem( + pem_key, scheme, password) return rsa_key - - def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', storage_backend=None): - """ - - Import the RSA key stored in 'filepath'. The key object returned is in the - format 'securesystemslib.formats.RSAKEY_SCHEMA'. If the RSA PEM in - 'filepath' contains a private key, it is discarded. + """Imports PEM-encoded RSA public key from file storage. - - filepath: - .pub file, an RSA PEM file. + The expected key format is X.509 SubjectPublicKeyInfo. - scheme: - The signature scheme used by the imported key. - - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. - - - securesystemslib.exceptions.FormatError, if 'filepath' is improperly - formatted. + Arguments: + filepath: The path to read the file from. + scheme (optional): The signing scheme assigned to the returned key object. + See RSA_SCHEME_SCHEMA for available signing schemes. + storage_backend (optional): An object implementing StorageBackendInterface. + If not passed a default FilesystemBackend will be used. - securesystemslib.exceptions.Error, if a valid RSA key object cannot be - generated. This may be caused by an improperly formatted PEM file. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. - - 'filepath' is read and its contents extracted. + Returns: + An RSA public key object conformant with 'RSAKEY_SCHEMA'. - - 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 storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() - # Read the contents of the key file that should be in PEM format and contains - # the public portion of the RSA key. with storage_backend.get(filepath) as file_object: rsa_pubkey_pem = file_object.read().decode('utf-8') - # Convert 'rsa_pubkey_pem' to 'securesystemslib.formats.RSAKEY_SCHEMA' format. + # Convert PEM-encoded key to 'RSAKEY_SCHEMA' format try: rsakey_dict = securesystemslib.keys.import_rsakey_from_public_pem( rsa_pubkey_pem, scheme) @@ -414,119 +449,73 @@ def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', +def _generate_and_write_ed25519_keypair(filepath=None, password=None, + prompt=False): + """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. + If a password is passed or entered on the prompt, the private key is + encrypted using AES-256 in CTR mode, with the password strengthened in + PBKDF2-HMAC-SHA256. -def generate_and_write_ed25519_keypair(filepath=None, password=None): - """ - - Generate an Ed25519 keypair, where the encrypted key (using 'password' as - the passphrase) is saved to <'filepath'>. The public key portion of the - generated Ed25519 key is saved to <'filepath'>.pub. If the filepath is not - given, the KEYID is used as the filename and the keypair saved to the - current working directory. - - The private key is encrypted according to 'cryptography's approach: - "Encrypt using the best available encryption for a given key's backend. - This is a curated encryption choice and the algorithm may change over - time." - - - filepath: - The public and private key files are saved to .pub and - , respectively. If the filepath is not given, the public and - private keys are saved to the current working directory as .pub - and . KEYID is the generated key's KEYID. - - password: - The password, or passphrase, to encrypt the private portion of the - generated Ed25519 key. A symmetric encryption key is derived from - 'password', so it is not directly used. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if 'filepath' cannot be encrypted. - - - Writes key files to '' and '.pub'. - - - The 'filepath' of the written key. - """ + NOTE: The custom key format includes 'ed25519' as signing scheme. - # Generate a new Ed25519 key object. - ed25519_key = securesystemslib.keys.generate_ed25519_key() + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + password (optional): An encryption password. + prompt (optional): A boolean indicating if the user should be prompted + for an encryption password. If the user enters an empty password, the + key is not encrypted. - if not filepath: - filepath = os.path.join(os.getcwd(), ed25519_key['keyid']) + Raises: + UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: An empty string is passed as 'password', or both a 'password' + is passed and 'prompt' is true. + StorageError: Key files cannot be written. - else: - logger.debug('The filepath has been specified. Not using the key\'s' - ' KEYID as the default filepath.') + Side Effects: + Prompts user for a password if 'prompt' is True. + Writes key files to disk. - # 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) + Returns: + The private key filepath. - # If the caller does not provide a password argument, prompt for one. - if password is None: + """ + ed25519_key = securesystemslib.keys.generate_ed25519_key() - # 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 Ed25519' - ' key (' + TERM_RED + filepath + TERM_RESET + '): ', - confirm=True) + # Use passed 'filepath' or keyid as file name + if not filepath: + filepath = os.path.join(os.getcwd(), ed25519_key['keyid']) - else: - logger.debug('The password has been specified. Not prompting for one.') + securesystemslib.formats.PATH_SCHEMA.check_match(filepath) - # Does 'password' have the correct format? - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + password = _get_key_file_encryption_password(password, prompt, filepath) - # If the parent directory of filepath does not exist, - # create it (and all its parent directories, if necessary). + # Create intermediate directories as required securesystemslib.util.ensure_parent_dir(filepath) - # Create a temporary file, write the contents of the public key, and move - # to final destination. - file_object = tempfile.TemporaryFile() - - # Generate the ed25519 public key file contents in metadata format (i.e., - # does not include the keyid portion). + # Use custom JSON format for ed25519 keys on-disk keytype = ed25519_key['keytype'] keyval = ed25519_key['keyval'] scheme = ed25519_key['scheme'] ed25519key_metadata_format = securesystemslib.keys.format_keyval_to_metadata( keytype, scheme, keyval, private=False) + # Write public key to .pub + file_object = tempfile.TemporaryFile() file_object.write(json.dumps(ed25519key_metadata_format).encode('utf-8')) - - # Write the public key (i.e., 'public', which is in PEM format) to - # '.pub'. (1) Create a temporary file, (2) write the contents of - # the public key, and (3) move to final destination. - # The temporary file is closed after the final move. securesystemslib.util.persist_temp_file(file_object, filepath + '.pub') - # Write the encrypted key string, conformant to - # 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA', to ''. - file_object = tempfile.TemporaryFile() - - # Encrypt the private key if 'password' is set. - if len(password): + # Encrypt private key if we have a password, store as JSON string otherwise + if password is not None: ed25519_key = securesystemslib.keys.encrypt_key(ed25519_key, password) - else: - logger.debug('An empty password was given. ' - 'Not encrypting the private key.') ed25519_key = json.dumps(ed25519_key) - # Raise 'securesystemslib.exceptions.CryptoError' if 'ed25519_key' cannot be - # encrypted. + # Write private key to + file_object = tempfile.TemporaryFile() file_object.write(ed25519_key.encode('utf-8')) securesystemslib.util.persist_temp_file(file_object, filepath) @@ -534,48 +523,122 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None): +def generate_and_write_ed25519_keypair(password, filepath=None): + """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. + + The private key is encrypted using AES-256 in CTR mode, with the passed + password strengthened in PBKDF2-HMAC-SHA256. + + NOTE: The custom key format includes 'ed25519' as signing scheme. + + Arguments: + password: An encryption password. + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + + Raises: + UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: An empty string is passed as 'password'. + StorageError: Key files cannot be written. + + Side Effects: + Writes key files to disk. + + Returns: + The private key filepath. + + """ + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + return _generate_and_write_ed25519_keypair( + filepath=filepath, password=password, prompt=False) + + + +def generate_and_write_ed25519_keypair_with_prompt(filepath=None): + """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. + + The private key is encrypted using AES-256 in CTR mode, with the password + entered on the prompt strengthened in PBKDF2-HMAC-SHA256. + + NOTE: The custom key format includes 'ed25519' as signing scheme. + + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + + Raises: + UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. + + Side Effects: + Prompts user for a password. + Writes key files to disk. + + Returns: + The private key filepath. -def import_ed25519_publickey_from_file(filepath): """ - - Load the ED25519 public key object (conformant to - 'securesystemslib.formats.KEY_SCHEMA') stored in 'filepath'. Return - 'filepath' in securesystemslib.formats.ED25519KEY_SCHEMA format. + return _generate_and_write_ed25519_keypair( + filepath=filepath, password=None, prompt=True) + + + +def generate_and_write_unencrypted_ed25519_keypair(filepath=None): + """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. + + NOTE: The custom key format includes 'ed25519' as signing scheme. - If the key object in 'filepath' contains a private key, it is discarded. + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. - - filepath: - .pub file, a public key file. + Raises: + UnsupportedLibraryError: pyca/pynacl or pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - - securesystemslib.exceptions.FormatError, if 'filepath' is improperly - formatted or is an unexpected key type. + Side Effects: + Writes unencrypted key files to disk. - - The contents of 'filepath' is read and saved. + Returns: + The private key filepath. - - An ED25519 key object conformant to - 'securesystemslib.formats.ED25519KEY_SCHEMA'. """ + return _generate_and_write_ed25519_keypair( + filepath=filepath, password=None, prompt=False) + + +def import_ed25519_publickey_from_file(filepath): + """Imports custom JSON-formatted ed25519 public key from disk. + + NOTE: The signing scheme is set at key generation (see generate function). + + Arguments: + filepath: The path to read the file from. - # 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. + Raises: + FormatError: Argument is malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. + + Returns: + An ed25519 public key object conformant with 'ED25519KEY_SCHEMA'. + + """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) - # ED25519 key objects are saved in json and metadata format. Return the - # loaded key object in securesystemslib.formats.ED25519KEY_SCHEMA' format that - # also includes the keyid. + # Load custom on-disk JSON formatted key and convert to its custom in-memory + # dict key representation ed25519_key_metadata = securesystemslib.util.load_json_file(filepath) - ed25519_key, junk = \ - securesystemslib.keys.format_metadata_to_key(ed25519_key_metadata) + ed25519_key, _ = securesystemslib.keys.format_metadata_to_key( + ed25519_key_metadata) - # Raise an exception if an unexpected key type is imported. Redundant - # validation of 'keytype'. 'securesystemslib.keys.format_metadata_to_key()' - # should have fully validated 'ed25519_key_metadata'. + # Check that the generic loading functions indeed loaded an ed25519 key if ed25519_key['keytype'] != 'ed25519': message = 'Invalid key type loaded: ' + repr(ed25519_key['keytype']) raise securesystemslib.exceptions.FormatError(message) @@ -584,331 +647,291 @@ def import_ed25519_publickey_from_file(filepath): - - def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, storage_backend=None): - """ - - Import the encrypted ed25519 key file in 'filepath', decrypt it, and return - the key object in 'securesystemslib.formats.ED25519KEY_SCHEMA' format. - - The private key (may also contain the public part) is encrypted with AES - 256 and CTR the mode of operation. The password is strengthened with - PBKDF2-HMAC-SHA256. + """Imports custom JSON-formatted ed25519 private key from file storage. - - filepath: - file, an RSA encrypted key file. + If a password is passed or entered on the prompt, the private key is + decrypted, otherwise it is treated as unencrypted. - password: - The password, or passphrase, to import the private key (i.e., the - encrypted key file 'filepath' must be decrypted before the ed25519 key - object can be returned. + NOTE: The signing scheme is set at key generation (see generate function). - prompt: - If True the user is prompted for a passphrase to decrypt 'filepath'. - Default is False. + Arguments: + filepath: The path to read the file from. + password (optional): A password to decrypt the key. + prompt (optional): A boolean indicating if the user should be prompted + for a decryption password. If the user enters an empty password, the + key is not decrypted. + storage_backend (optional): An object implementing StorageBackendInterface. + If not passed a default FilesystemBackend will be used. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted or the imported key object contains an invalid key type (i.e., - not 'ed25519'). + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: Both a 'password' is passed and 'prompt' is true. + StorageError: Key file cannot be read. + Error, CryptoError: Key cannot be parsed. - securesystemslib.exceptions.CryptoError, if 'filepath' cannot be decrypted. - - 'password' is used to decrypt the 'filepath' key file. + Returns: + An ed25519 private key object conformant with 'ED25519KEY_SCHEMA'. - - 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.") + password = _get_key_file_decryption_password(password, prompt, filepath) if storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() - # 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: - password = None - - # Finally, regardless of password, try decrypting the key, if necessary. - # Otherwise, load it straight from storage. with storage_backend.get(filepath) as file_object: json_str = file_object.read() - return securesystemslib.keys.\ - import_ed25519key_from_private_json(json_str, password=password) + # Load custom on-disk JSON formatted key and convert to its custom + # in-memory dict key representation, decrypting it if password is not None + return securesystemslib.keys.import_ed25519key_from_private_json( + json_str, password=password) +def _generate_and_write_ecdsa_keypair(filepath=None, password=None, + prompt=False): + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. + If a password is passed or entered on the prompt, the private key is + encrypted using AES-256 in CTR mode, with the password strengthened in + PBKDF2-HMAC-SHA256. -def generate_and_write_ecdsa_keypair(filepath=None, password=None): - """ - - Generate an ECDSA keypair, where the encrypted key (using 'password' as the - passphrase) is saved to <'filepath'>. The public key portion of the - generated ECDSA key is saved to <'filepath'>.pub. If the filepath is not - given, the KEYID is used as the filename and the keypair saved to the - current working directory. - - The 'cryptography' library is currently supported. The private key is - encrypted according to 'cryptography's approach: "Encrypt using the best - available encryption for a given key's backend. This is a curated - encryption choice and the algorithm may change over time." - - - filepath: - The public and private key files are saved to .pub and - , respectively. If the filepath is not given, the public and - private keys are saved to the current working directory as .pub - and . KEYID is the generated key's KEYID. - - password: - The password, or passphrase, to encrypt the private portion of the - generated ECDSA key. A symmetric encryption key is derived from - 'password', so it is not directly used. - - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.CryptoError, if 'filepath' cannot be encrypted. - - - Writes key files to '' and '.pub'. - - - The 'filepath' of the written key. - """ + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - # Generate a new ECDSA key object. The 'cryptography' library is currently - # supported and performs the actual cryptographic operations. - ecdsa_key = securesystemslib.keys.generate_ecdsa_key() + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + password (optional): An encryption password. + prompt (optional): A boolean indicating if the user should be prompted + for an encryption password. If the user enters an empty password, the + key is not encrypted. - if not filepath: - filepath = os.path.join(os.getcwd(), ecdsa_key['keyid']) + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: An empty string is passed as 'password', or both a 'password' + is passed and 'prompt' is true. + StorageError: Key files cannot be written. - else: - logger.debug('The filepath has been specified. Not using the key\'s' - ' KEYID as the default filepath.') + Side Effects: + Prompts user for a password if 'prompt' is True. + Writes key files to disk. - # Does 'filepath' have the correct format? - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. - securesystemslib.formats.PATH_SCHEMA.check_match(filepath) + Returns: + The private key filepath. - # If the caller does not provide a password argument, prompt for one. - if password is None: + """ + ecdsa_key = securesystemslib.keys.generate_ecdsa_key() - # 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 ECDSA' - ' key (' + TERM_RED + filepath + TERM_RESET + '): ', - confirm=True) + # Use passed 'filepath' or keyid as file name + if not filepath: + filepath = os.path.join(os.getcwd(), ecdsa_key['keyid']) - else: - logger.debug('The password has been specified. Not prompting for one') + securesystemslib.formats.PATH_SCHEMA.check_match(filepath) - # Does 'password' have the correct format? - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + password = _get_key_file_encryption_password(password, prompt, filepath) - # If the parent directory of filepath does not exist, - # create it (and all its parent directories, if necessary). + # Create intermediate directories as required securesystemslib.util.ensure_parent_dir(filepath) - # Create a temporary file, write the contents of the public key, and move - # to final destination. - file_object = tempfile.TemporaryFile() - - # Generate the ECDSA public key file contents in metadata format (i.e., does - # not include the keyid portion). + # Use custom JSON format for ecdsa keys on-disk keytype = ecdsa_key['keytype'] keyval = ecdsa_key['keyval'] scheme = ecdsa_key['scheme'] ecdsakey_metadata_format = securesystemslib.keys.format_keyval_to_metadata( keytype, scheme, keyval, private=False) + # Write public key to .pub + file_object = tempfile.TemporaryFile() file_object.write(json.dumps(ecdsakey_metadata_format).encode('utf-8')) - - # Write the public key (i.e., 'public', which is in PEM format) to - # '.pub'. (1) Create a temporary file, (2) write the contents of - # the public key, and (3) move to final destination. securesystemslib.util.persist_temp_file(file_object, filepath + '.pub') - # Write the encrypted key string, conformant to - # 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA', to ''. + # Encrypt private key if we have a password, store as JSON string otherwise + if password is not None: + ecdsa_key = securesystemslib.keys.encrypt_key(ecdsa_key, password) + else: + ecdsa_key = json.dumps(ecdsa_key) + + # Write private key to file_object = tempfile.TemporaryFile() - # Raise 'securesystemslib.exceptions.CryptoError' if 'ecdsa_key' cannot be - # encrypted. - encrypted_key = securesystemslib.keys.encrypt_key(ecdsa_key, password) - file_object.write(encrypted_key.encode('utf-8')) + file_object.write(ecdsa_key.encode('utf-8')) securesystemslib.util.persist_temp_file(file_object, filepath) return filepath +def generate_and_write_ecdsa_keypair(password, filepath=None): + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. -def import_ecdsa_publickey_from_file(filepath): - """ - - Load the ECDSA public key object (conformant to - 'securesystemslib.formats.KEY_SCHEMA') stored in 'filepath'. Return - 'filepath' in securesystemslib.formats.ECDSAKEY_SCHEMA format. + The private key is encrypted using AES-256 in CTR mode, with the passed + password strengthened in PBKDF2-HMAC-SHA256. - If the key object in 'filepath' contains a private key, it is discarded. + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - - filepath: - .pub file, a public key file. + Arguments: + password: An encryption password. + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. - - securesystemslib.exceptions.FormatError, if 'filepath' is improperly - formatted or is an unexpected key type. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: An empty string is passed as 'password'. + StorageError: Key files cannot be written. - - The contents of 'filepath' is read and saved. + Side Effects: + Writes key files to disk. + + Returns: + The private key filepath. - - An ECDSA key object conformant to - 'securesystemslib.formats.ECDSAKEY_SCHEMA'. """ + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + return _generate_and_write_ecdsa_keypair( + filepath=filepath, password=password, prompt=False) - # 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) - # ECDSA key objects are saved in json and metadata format. Return the - # loaded key object in securesystemslib.formats.ECDSAKEY_SCHEMA' format that - # also includes the keyid. - ecdsa_key_metadata = securesystemslib.util.load_json_file(filepath) - ecdsa_key, junk = \ - securesystemslib.keys.format_metadata_to_key(ecdsa_key_metadata) - return ecdsa_key +def generate_and_write_ecdsa_keypair_with_prompt(filepath=None): + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. + + The private key is encrypted using AES-256 in CTR mode, with the password + entered on the prompt strengthened in PBKDF2-HMAC-SHA256. + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. + Side Effects: + Prompts user for a password. + Writes key files to disk. + + Returns: + The private key filepath. -def import_ecdsa_privatekey_from_file(filepath, password=None, - storage_backend=None): """ - - Import the encrypted ECDSA key file in 'filepath', decrypt it, and return - the key object in 'securesystemslib.formats.ECDSAKEY_SCHEMA' format. + return _generate_and_write_ecdsa_keypair( + filepath=filepath, password=None, prompt=True) - The 'cryptography' library is currently supported and performs the actual - cryptographic routine. - - filepath: - file, an ECDSA encrypted key file. - password: - The password, or passphrase, to import the private key (i.e., the - encrypted key file 'filepath' must be decrypted before the ECDSA key - object can be returned. +def generate_and_write_unencrypted_ecdsa_keypair(filepath=None): + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' as signing scheme. - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted or the imported key object contains an invalid key type (i.e., - not 'ecdsa'). + Arguments: + filepath (optional): The path to write the private key to. If not passed, + the key is written to CWD using the keyid as filename. The public key + is written to the same path as the private key using the suffix '.pub'. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. - securesystemslib.exceptions.CryptoError, if 'filepath' cannot be decrypted. + Side Effects: + Writes unencrypted key files to disk. - - 'password' is used to decrypt the 'filepath' key file. + Returns: + The private key filepath. - - An ECDSA key object of the form: 'securesystemslib.formats.ECDSAKEY_SCHEMA'. """ + return _generate_and_write_ecdsa_keypair( + filepath=filepath, password=None, prompt=False) - # 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. + + +def import_ecdsa_publickey_from_file(filepath): + """Imports custom JSON-formatted ecdsa public key from disk. + + NOTE: The signing scheme is set at key generation (see generate function). + + Arguments: + filepath: The path to read the file from. + + Raises: + FormatError: Argument is malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. + + Returns: + An ecdsa public key object conformant with 'ECDSAKEY_SCHEMA'. + + """ 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: + # Load custom on-disk JSON formatted key and convert to its custom in-memory + # dict key representation + ecdsa_key_metadata = securesystemslib.util.load_json_file(filepath) + ecdsa_key, _ = securesystemslib.keys.format_metadata_to_key( + ecdsa_key_metadata) - # 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) + return ecdsa_key - # Does 'password' have the correct format? - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + + +def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False, + storage_backend=None): + """Imports custom JSON-formatted ecdsa private key from file storage. + + If a password is passed or entered on the prompt, the private key is + decrypted, otherwise it is treated as unencrypted. + + NOTE: The signing scheme is set at key generation (see generate function). + + Arguments: + filepath: The path to read the file from. + password (optional): A password to decrypt the key. + prompt (optional): A boolean indicating if the user should be prompted + for a decryption password. If the user enters an empty password, the + key is not decrypted. + storage_backend (optional): An object implementing StorageBackendInterface. + If not passed a default FilesystemBackend will be used. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + ValueError: Both a 'password' is passed and 'prompt' is true. + StorageError: Key file cannot be read. + Error, CryptoError: Key cannot be parsed. + + Returns: + An ecdsa private key object conformant with 'ED25519KEY_SCHEMA'. + + """ + securesystemslib.formats.PATH_SCHEMA.check_match(filepath) + + password = _get_key_file_decryption_password(password, prompt, filepath) if storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() - # Store the encrypted contents of 'filepath' prior to calling the decryption - # routine. - encrypted_key = None - with storage_backend.get(filepath) as file_object: - encrypted_key = file_object.read() + key_data = file_object.read().decode('utf-8') - # Decrypt the loaded key file, calling the 'cryptography' library to generate - # the derived encryption key from 'password'. Raise - # 'securesystemslib.exceptions.CryptoError' if the decryption fails. - key_object = securesystemslib.keys.decrypt_key(encrypted_key.decode('utf-8'), - password) + # Decrypt private key if we have a password, directly load JSON otherwise + if password is not None: + key_object = securesystemslib.keys.decrypt_key(key_data, password) + else: + key_object = securesystemslib.util.load_json_string(key_data) # Raise an exception if an unexpected key type is imported. # NOTE: we support keytype's of ecdsa-sha2-nistp256 and ecdsa-sha2-nistp384 @@ -932,22 +955,25 @@ def import_ecdsa_privatekey_from_file(filepath, password=None, def import_publickeys_from_file(filepaths, key_types=None): """Imports multiple public keys from files. - NOTE: Use 'import_rsa_publickey_from_file' to specify any other than the - default signing schemes for an RSA key. + NOTE: The default signing scheme 'rsassa-pss-sha256' is assigned to RSA keys. + Use 'import_rsa_publickey_from_file' to specify any other than the default + signing scheme for an RSA key. ed25519 and ecdsa keys have the signing scheme + included in the custom key format (see generate functions). Arguments: filepaths: A list of paths to public key files. key_types (optional): A list of types of keys to be imported associated - with filepaths by index. Must be one of KEY_TYPE_RSA, KEY_TYPE_ED25519 or - KEY_TYPE_ECDSA. If not specified, all keys are assumed to be - KEY_TYPE_RSA. + with filepaths by index. Must be one of KEY_TYPE_RSA, KEY_TYPE_ED25519 + or KEY_TYPE_ECDSA. If not specified, all keys are assumed to be + KEY_TYPE_RSA. Raises: - TypeError: filepaths or key_types (if passed) is not iterable. - FormatError: key_types is passed and does not have the same length as - filepaths or contains an unsupported key type. - See import_ed25519_publickey_from_file, import_rsa_publickey_from_file and - import_ecdsa_publickey_from_file for other exceptions. + TypeError: filepaths or 'key_types' (if passed) is not iterable. + FormatError: Argument are malformed, or 'key_types' is passed and does not + have the same length as 'filepaths' or contains an unsupported type. + UnsupportedLibraryError: pyca/cryptography is not available. + StorageError: Key file cannot be read. + Error: Public key is malformed. Returns: A dict of public keys in KEYDICT_SCHEMA format. @@ -983,6 +1009,62 @@ def import_publickeys_from_file(filepaths, key_types=None): return key_dict + +def import_privatekey_from_file(filepath, key_type=None, password=None, + prompt=False): + """Imports private key from file. + + If a password is passed or entered on the prompt, the private key is + decrypted, otherwise it is treated as unencrypted. + + NOTE: The default signing scheme 'rsassa-pss-sha256' is assigned to RSA keys. + Use 'import_rsa_privatekey_from_file' to specify any other than the default + signing scheme for an RSA key. ed25519 and ecdsa keys have the signing scheme + included in the custom key format (see generate functions). + + Arguments: + filepath: The path to read the file from. + key_type (optional): One of KEY_TYPE_RSA, KEY_TYPE_ED25519 or + KEY_TYPE_ECDSA. Default is KEY_TYPE_RSA. + password (optional): A password to decrypt the key. + prompt (optional): A boolean indicating if the user should be prompted + for a decryption password. If the user enters an empty password, the + key is not decrypted. + + Raises: + FormatError: Arguments are malformed or 'key_type' is not supported. + ValueError: Both a 'password' is passed and 'prompt' is true. + UnsupportedLibraryError: pyca/cryptography is not available. + StorageError: Key file cannot be read. + Error, CryptoError: Key cannot be parsed. + + Returns: + A private key object conformant with one of 'ED25519KEY_SCHEMA', + 'RSAKEY_SCHEMA' or 'ECDSAKEY_SCHEMA'. + + """ + if key_type is None: + key_type = KEY_TYPE_RSA + + if key_type == KEY_TYPE_ED25519: + return import_ed25519_privatekey_from_file( + filepath, password=password, prompt=prompt) + + elif key_type == KEY_TYPE_RSA: + return import_rsa_privatekey_from_file( + filepath, password=password, prompt=prompt) + + elif key_type == KEY_TYPE_ECDSA: + return import_ecdsa_privatekey_from_file( + filepath, password=password, prompt=prompt) + + else: + raise securesystemslib.exceptions.FormatError( + "Unsupported key type '{}'. Must be '{}', '{}' or '{}'.".format( + key_type, KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA)) + + + if __name__ == '__main__': # The interactive sessions of the documentation strings can # be tested by running interface.py as a standalone module: diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index dcec9c00..7aa90531 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -40,6 +40,10 @@ import tempfile import unittest +if sys.version_info >= (3, 3): + import unittest.mock as mock +else: + import mock import securesystemslib.exceptions import securesystemslib.gpg.constants @@ -64,7 +68,26 @@ def test_interface(self): with self.assertRaises( securesystemslib.exceptions.UnsupportedLibraryError): - securesystemslib.interface.generate_and_write_rsa_keypair(password='pw') + securesystemslib.interface._generate_and_write_rsa_keypair(password='pw') + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_rsa_keypair('pw') + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_rsa_keypair('pw') + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + # Mock entry on prompt which is presented before lower-level functions + # raise UnsupportedLibraryError + with mock.patch("securesystemslib.interface.get_password", return_value=""): + securesystemslib.interface.generate_and_write_rsa_keypair_with_prompt() + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_unencrypted_rsa_keypair() with self.assertRaises( securesystemslib.exceptions.UnsupportedLibraryError): @@ -75,9 +98,24 @@ def test_interface(self): with self.assertRaises( securesystemslib.exceptions.UnsupportedLibraryError): - securesystemslib.interface.generate_and_write_ed25519_keypair( + securesystemslib.interface._generate_and_write_ed25519_keypair( password='pw') + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_ed25519_keypair('pw') + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + # Mock entry on prompt which is presented before lower-level functions + # raise UnsupportedLibraryError + with mock.patch("securesystemslib.interface.get_password", return_value=""): + securesystemslib.interface.generate_and_write_ed25519_keypair_with_prompt() + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_unencrypted_ed25519_keypair() + with self.assertRaises( securesystemslib.exceptions.UnsupportedLibraryError): path = os.path.join(self.temp_dir, 'ed25519_priv.json') @@ -88,16 +126,29 @@ def test_interface(self): with self.assertRaises( securesystemslib.exceptions.UnsupportedLibraryError): - securesystemslib.interface.generate_and_write_ecdsa_keypair( + securesystemslib.interface._generate_and_write_ecdsa_keypair( password='pw') + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_ecdsa_keypair('pw') + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + # Mock entry on prompt which is presented before lower-level functions + # raise UnsupportedLibraryError + with mock.patch("securesystemslib.interface.get_password", return_value=""): + securesystemslib.interface.generate_and_write_ecdsa_keypair_with_prompt() + + with self.assertRaises( + securesystemslib.exceptions.UnsupportedLibraryError): + securesystemslib.interface.generate_and_write_unencrypted_ecdsa_keypair() + with self.assertRaises( securesystemslib.exceptions.UnsupportedLibraryError): path = os.path.join(self.temp_dir, 'ecddsa.priv') with open(path, 'a') as f: f.write('{}') - # TODO: this is the only none generate_and_write_ function - # that prompts for a password when password == None securesystemslib.interface.import_ecdsa_privatekey_from_file( path, password='pw') diff --git a/tests/test_interface.py b/tests/test_interface.py index 48aa19a1..6c9c4f6e 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -60,16 +60,26 @@ from securesystemslib.exceptions import Error, FormatError, CryptoError from securesystemslib.interface import ( + _generate_and_write_rsa_keypair, generate_and_write_rsa_keypair, + generate_and_write_rsa_keypair_with_prompt, + generate_and_write_unencrypted_rsa_keypair, import_rsa_privatekey_from_file, import_rsa_publickey_from_file, + _generate_and_write_ed25519_keypair, generate_and_write_ed25519_keypair, + generate_and_write_ed25519_keypair_with_prompt, + generate_and_write_unencrypted_ed25519_keypair, import_ed25519_publickey_from_file, import_ed25519_privatekey_from_file, + _generate_and_write_ecdsa_keypair, generate_and_write_ecdsa_keypair, + generate_and_write_ecdsa_keypair_with_prompt, + generate_and_write_unencrypted_ecdsa_keypair, import_ecdsa_publickey_from_file, import_ecdsa_privatekey_from_file, - import_publickeys_from_file) + import_publickeys_from_file, + import_privatekey_from_file) @@ -100,13 +110,12 @@ def tearDown(self): def test_rsa(self): - """Test RSA key generation and import interface functions. """ + """Test RSA key _generation and import interface functions. """ # TEST: Generate default keys and import # Assert location and format fn_default = "default" - fn_default_ret = generate_and_write_rsa_keypair( - filepath=fn_default, password="") + fn_default_ret = _generate_and_write_rsa_keypair(filepath=fn_default) pub = import_rsa_publickey_from_file(fn_default + ".pub") priv = import_rsa_privatekey_from_file(fn_default) @@ -123,13 +132,13 @@ def test_rsa(self): # Assert importable without password fn_empty_prompt = "empty_prompt" with mock.patch("securesystemslib.interface.get_password", return_value=""): - generate_and_write_rsa_keypair(filepath=fn_empty_prompt) + _generate_and_write_rsa_keypair(filepath=fn_empty_prompt, prompt=True) import_rsa_privatekey_from_file(fn_empty_prompt) # TEST: Generate keys with auto-filename, i.e. keyid # Assert filename is keyid - fn_keyid = generate_and_write_rsa_keypair(password="") + fn_keyid = _generate_and_write_rsa_keypair() pub = import_rsa_publickey_from_file(fn_keyid + ".pub") priv = import_rsa_privatekey_from_file(fn_keyid) self.assertTrue( @@ -140,7 +149,7 @@ def test_rsa(self): # Assert length bits = 4096 fn_bits = "bits" - generate_and_write_rsa_keypair(filepath=fn_bits, password="", bits=bits) + _generate_and_write_rsa_keypair(filepath=fn_bits, bits=bits) priv = import_rsa_privatekey_from_file(fn_bits) # NOTE: Parse PEM with pyca/cryptography to get the key size property @@ -158,10 +167,10 @@ def test_rsa(self): fn_prompt = "prompt" # ... a passed pw ... - generate_and_write_rsa_keypair(filepath=fn_encrypted, password=pw) + _generate_and_write_rsa_keypair(filepath=fn_encrypted, password=pw) with mock.patch("securesystemslib.interface.get_password", return_value=pw): # ... and a prompted pw. - generate_and_write_rsa_keypair(filepath=fn_prompt) + _generate_and_write_rsa_keypair(filepath=fn_prompt, prompt=True) # Assert that both private keys are importable using the prompted pw ... import_rsa_privatekey_from_file(fn_prompt, prompt=True) @@ -185,15 +194,30 @@ def test_rsa(self): # TEST: Generation errors + for idx, (kwargs, err_msg) in enumerate([ + # Error on empty password + ({"password": ""}, + "encryption password must be 1 or more characters long"), + # Error on 'password' and 'prompt=True' + ({"password": pw, "prompt": True}, + "passing 'password' and 'prompt=True' is not allowed")]): + + with self.assertRaises(ValueError, msg="(row {})".format(idx)) as ctx: + _generate_and_write_rsa_keypair(**kwargs) + + self.assertEqual(err_msg, str(ctx.exception), + "expected: '{}' got: '{}' (row {})".format( + err_msg, ctx.exception, idx)) # Error on bad argument format for idx, kwargs in enumerate([ {"bits": 1024}, # Too low {"bits": "not-an-int"}, {"filepath": 123456}, # Not a string - {"password": 123456}]): # Not a string + {"password": 123456}, # Not a string + {"prompt": "not-a-bool"}]): with self.assertRaises(FormatError, msg="(row {})".format(idx)): - generate_and_write_rsa_keypair(**kwargs) + _generate_and_write_rsa_keypair(**kwargs) # TEST: Import errors @@ -218,14 +242,14 @@ def test_rsa(self): ([fn_encrypted], {}, CryptoError, "Password was not given but private key is encrypted"), # Error on encrypted but empty pw passed - ([fn_encrypted], {"password": ""}, ValueError, - "Password must be 1 or more character"), + ([fn_encrypted], {"password": ""}, CryptoError, + "Password was not given but private key is encrypted"), # Error on encrypted but bad pw passed ([fn_encrypted], {"password": "bad pw"}, CryptoError, "Bad decrypt. Incorrect password?"), # Error on pw and prompt ([fn_default], {"password": pw, "prompt": True}, ValueError, - "Passing 'password' and 'prompt' True is not allowed.")]): + "passing 'password' and 'prompt=True' is not allowed")]): with self.assertRaises(err, msg="(row {})".format(idx)) as ctx: import_rsa_privatekey_from_file(*args, **kwargs) @@ -258,16 +282,19 @@ def test_rsa(self): with self.assertRaises(FormatError): import_rsa_privatekey_from_file(fn_default, password=123456) + # bad prompt + with self.assertRaises(FormatError): + import_rsa_privatekey_from_file(fn_default, prompt="not-a-bool") + def test_ed25519(self): - """Test ed25519 key generation and import interface functions. """ + """Test ed25519 key _generation and import interface functions. """ # TEST: Generate default keys and import # Assert location and format fn_default = "default" - fn_default_ret = generate_and_write_ed25519_keypair( - filepath=fn_default, password="") + fn_default_ret = _generate_and_write_ed25519_keypair(filepath=fn_default) pub = import_ed25519_publickey_from_file(fn_default + ".pub") priv = import_ed25519_privatekey_from_file(fn_default) @@ -284,14 +311,14 @@ def test_ed25519(self): # Assert importable with empty prompt password and without password fn_empty_prompt = "empty_prompt" with mock.patch("securesystemslib.interface.get_password", return_value=""): - generate_and_write_ed25519_keypair(filepath=fn_empty_prompt) + _generate_and_write_ed25519_keypair(filepath=fn_empty_prompt) import_ed25519_privatekey_from_file(fn_empty_prompt, prompt=True) import_ed25519_privatekey_from_file(fn_empty_prompt) # TEST: Generate keys with auto-filename, i.e. keyid # Assert filename is keyid - fn_keyid = generate_and_write_ed25519_keypair(password="") + fn_keyid = _generate_and_write_ed25519_keypair() pub = import_ed25519_publickey_from_file(fn_keyid + ".pub") priv = import_ed25519_privatekey_from_file(fn_keyid) self.assertTrue( @@ -303,10 +330,10 @@ def test_ed25519(self): fn_encrypted = "encrypted" fn_prompt = "prompt" # ... a passed pw ... - generate_and_write_ed25519_keypair(filepath=fn_encrypted, password=pw) + _generate_and_write_ed25519_keypair(filepath=fn_encrypted, password=pw) with mock.patch("securesystemslib.interface.get_password", return_value=pw): # ... and a prompted pw. - generate_and_write_ed25519_keypair(filepath=fn_prompt) + _generate_and_write_ed25519_keypair(filepath=fn_prompt, prompt=True) # Assert that both private keys are importable using the prompted pw ... import_ed25519_privatekey_from_file(fn_prompt, prompt=True) @@ -343,13 +370,28 @@ def test_ed25519(self): # TEST: Generation errors + for idx, (kwargs, err_msg) in enumerate([ + # Error on empty password + ({"password": ""}, + "encryption password must be 1 or more characters long"), + # Error on 'password' and 'prompt=True' + ({"password": pw, "prompt": True}, + "passing 'password' and 'prompt=True' is not allowed")]): + + with self.assertRaises(ValueError, msg="(row {})".format(idx)) as ctx: + _generate_and_write_ed25519_keypair(**kwargs) + + self.assertEqual(err_msg, str(ctx.exception), + "expected: '{}' got: '{}' (row {})".format( + err_msg, ctx.exception, idx)) # Error on bad argument format for idx, kwargs in enumerate([ {"filepath": 123456}, # Not a string - {"password": 123456}]): # Not a string + {"password": 123456}, # Not a string + {"prompt": "not-a-bool"}]): with self.assertRaises(FormatError, msg="(row {})".format(idx)): - generate_and_write_ed25519_keypair(**kwargs) + _generate_and_write_ed25519_keypair(**kwargs) # TEST: Import errors @@ -381,15 +423,15 @@ def test_ed25519(self): ([fn_encrypted], {}, CryptoError, "Malformed Ed25519 key JSON, possibly due to encryption, " "but no password provided?"), - # Error on encrypted but empty pw passed - ([fn_encrypted], {"password": ""}, ValueError, - "Password must be 1 or more character"), + # Error on encrypted but empty pw + ([fn_encrypted], {"password": ""}, CryptoError, + "Decryption failed."), # Error on encrypted but bad pw passed ([fn_encrypted], {"password": "bad pw"}, CryptoError, "Decryption failed."), # Error on pw and prompt ([fn_default], {"password": pw, "prompt": True}, ValueError, - "Passing 'password' and 'prompt' True is not allowed.")]): + "passing 'password' and 'prompt=True' is not allowed")]): with self.assertRaises(err, msg="(row {})".format(idx)) as ctx: import_ed25519_privatekey_from_file(*args, **kwargs) @@ -420,23 +462,20 @@ def test_ed25519(self): with self.assertRaises(FormatError): import_ed25519_privatekey_from_file(fn_default, password=123456) + # Error on bad prompt format + with self.assertRaises(FormatError): + import_ed25519_privatekey_from_file(fn_default, prompt="not-a-bool") def test_ecdsa(self): - """Test ecdsa key generation and import interface functions. """ - # NOTE: Unlike rsa and ed25519, the ecdsa (key creation and private key - # import) interface only supports encrypted keys, even if the passed or - # prompted password is an empty string. - - # TEST: Generate pw encrypted keys and import + """Test ecdsa key _generation and import interface functions. """ + # TEST: Generate default keys and import # Assert location and format - pw = "pw" fn_default = "default" - fn_default_ret = generate_and_write_ecdsa_keypair( - filepath=fn_default, password=pw) + fn_default_ret = _generate_and_write_ecdsa_keypair(filepath=fn_default) pub = import_ecdsa_publickey_from_file(fn_default + ".pub") - priv = import_ecdsa_privatekey_from_file(fn_default, password=pw) + priv = import_ecdsa_privatekey_from_file(fn_default) self.assertEqual(fn_default, fn_default_ret) self.assertTrue(ECDSAKEY_SCHEMA.matches(pub)) @@ -445,13 +484,20 @@ def test_ecdsa(self): # NOTE: There is no private key schema, at least check it has a value self.assertTrue(priv["keyval"]["private"]) + # TEST: Generate unencrypted keys with empty prompt + # Assert importable with empty prompt password and without password + fn_empty_prompt = "empty_prompt" + with mock.patch("securesystemslib.interface.get_password", return_value=""): + _generate_and_write_ecdsa_keypair(filepath=fn_empty_prompt) + import_ecdsa_privatekey_from_file(fn_empty_prompt, prompt=True) + import_ecdsa_privatekey_from_file(fn_empty_prompt) + # TEST: Generate keys with auto-filename, i.e. keyid # Assert filename is keyid - fn_keyid = generate_and_write_ecdsa_keypair(password=pw) + fn_keyid = _generate_and_write_ecdsa_keypair() pub = import_ecdsa_publickey_from_file(fn_keyid + ".pub") - priv = import_ecdsa_privatekey_from_file( - fn_keyid, password=pw) + priv = import_ecdsa_privatekey_from_file(fn_keyid) self.assertTrue( os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"]) @@ -461,14 +507,14 @@ def test_ecdsa(self): fn_encrypted = "encrypted" fn_prompt = "prompt" # ... a passed pw ... - generate_and_write_ecdsa_keypair(filepath=fn_encrypted, password=pw) + _generate_and_write_ecdsa_keypair(filepath=fn_encrypted, password=pw) with mock.patch("securesystemslib.interface.get_password", return_value=pw): # ... and a prompted pw. - generate_and_write_ecdsa_keypair(filepath=fn_prompt) + _generate_and_write_ecdsa_keypair(filepath=fn_prompt, prompt=True) # Assert that both private keys are importable using the prompted pw ... - import_ecdsa_privatekey_from_file(fn_prompt) - import_ecdsa_privatekey_from_file(fn_encrypted) + import_ecdsa_privatekey_from_file(fn_prompt, prompt=True) + import_ecdsa_privatekey_from_file(fn_encrypted, prompt=True) # ... and the passed pw. import_ecdsa_privatekey_from_file(fn_prompt, password=pw) @@ -494,21 +540,36 @@ def test_ecdsa(self): # TEST: Generation errors + for idx, (kwargs, err_msg) in enumerate([ + # Error on empty password + ({"password": ""}, + "encryption password must be 1 or more characters long"), + # Error on 'password' and 'prompt=True' + ({"password": pw, "prompt": True}, + "passing 'password' and 'prompt=True' is not allowed")]): + + with self.assertRaises(ValueError, msg="(row {})".format(idx)) as ctx: + _generate_and_write_ecdsa_keypair(**kwargs) + + self.assertEqual(err_msg, str(ctx.exception), + "expected: '{}' got: '{}' (row {})".format( + err_msg, ctx.exception, idx)) # Error on bad argument format for idx, kwargs in enumerate([ {"filepath": 123456}, # Not a string - {"password": 123456}]): # Not a string + {"password": 123456}, # Not a string + {"prompt": "not-a-bool"}]): with self.assertRaises(FormatError, msg="(row {})".format(idx)): - generate_and_write_ecdsa_keypair(**kwargs) + _generate_and_write_ecdsa_keypair(**kwargs) # TEST: Import errors # Error on public key import... for idx, (fn, err_msg) in enumerate([ - # Error on invalid custom json key format - (fn_default, "Cannot deserialize to a Python object"), + # Error on invalid json (custom key format) + (fn_encrypted, "Cannot deserialize to a Python object"), # Error on invalid custom key format (self.path_no_key, "Missing key")]): with self.assertRaises(Error, msg="(row {})".format(idx)) as ctx: @@ -522,14 +583,23 @@ def test_ecdsa(self): # Error on private key import... for idx, (args, kwargs, err, err_msg) in enumerate([ # Error on not an ecdsa private key - ([self.path_ed25519], {"password": "password"}, FormatError, - "Invalid key type loaded"), - # Error on encrypted but empty pw passed - ([fn_default], {"password": ""}, CryptoError, + ([self.path_ed25519], {}, Error, + "Cannot deserialize to a Python object"), + # Error on not encrypted + ([fn_default], {"password": pw}, CryptoError, + "Invalid encrypted file."), + # Error on encrypted but no pw + ([fn_encrypted], {}, Error, + "Cannot deserialize to a Python object"), + # Error on encrypted but empty pw + ([fn_encrypted], {"password": ""}, CryptoError, + "Decryption failed."), + # Error on encrypted but bad pw passed + ([fn_encrypted], {"password": "bad pw"}, CryptoError, "Decryption failed."), - # Error on encrypted but wrong pw passed - ([fn_default], {"password": "bad pw"}, CryptoError, - "Decryption failed.")]): + # Error on pw and prompt + ([fn_default], {"password": pw, "prompt": True}, ValueError, + "passing 'password' and 'prompt=True' is not allowed")]): with self.assertRaises(err, msg="(row {})".format(idx)) as ctx: import_ecdsa_privatekey_from_file(*args, **kwargs) @@ -538,6 +608,16 @@ def test_ecdsa(self): "expected: '{}' got: '{}' (row {})".format( err_msg, ctx.exception, idx)) + # Error on encrypted but bad pw prompted + err_msg = ("Decryption failed") + with self.assertRaises(CryptoError) as ctx, mock.patch( + "securesystemslib.interface.get_password", return_value="bad_pw"): + import_ecdsa_privatekey_from_file(fn_encrypted, prompt=True) + + self.assertTrue(err_msg in str(ctx.exception), + "expected: '{}' got: '{}'".format(err_msg, ctx.exception)) + + # Error on bad path format with self.assertRaises(FormatError): import_ecdsa_publickey_from_file(123456) @@ -548,6 +628,74 @@ def test_ecdsa(self): with self.assertRaises(FormatError): # bad password import_ecdsa_privatekey_from_file(fn_default, password=123456) + # Error on bad prompt format + with self.assertRaises(FormatError): + import_ecdsa_privatekey_from_file(fn_default, prompt="not-a-bool") + + + + def test_generate_keypair_wrappers(self): + """Basic tests for thin wrappers around _generate_and_write_*_keypair. + See 'test_rsa', 'test_ed25519' and 'test_ecdsa' for more thorough key + generation tests for each key type. + + """ + key_pw = "pw" + for idx, (gen, gen_prompt, gen_plain, import_priv, schema) in enumerate([ + ( + generate_and_write_rsa_keypair, + generate_and_write_rsa_keypair_with_prompt, + generate_and_write_unencrypted_rsa_keypair, + import_rsa_privatekey_from_file, + RSAKEY_SCHEMA + ), + ( + generate_and_write_ed25519_keypair, + generate_and_write_ed25519_keypair_with_prompt, + generate_and_write_unencrypted_ed25519_keypair, + import_ed25519_privatekey_from_file, + ED25519KEY_SCHEMA + ), + ( + generate_and_write_ecdsa_keypair, + generate_and_write_ecdsa_keypair_with_prompt, + generate_and_write_unencrypted_ecdsa_keypair, + import_ecdsa_privatekey_from_file, + ECDSAKEY_SCHEMA)]): + + assert_msg = "(row {})".format(idx) + # Test generate_and_write_*_keypair creates an encrypted private key + fn_encrypted = gen(key_pw) + priv = import_priv(fn_encrypted, key_pw) + self.assertTrue(schema.matches(priv), assert_msg) + + # Test generate_and_write_*_keypair errors if password is None or empty + with self.assertRaises(FormatError, msg=assert_msg): + fn_encrypted = gen(None) + with self.assertRaises(ValueError, msg=assert_msg): + fn_encrypted = gen("") + + # Test generate_and_write_*_keypair_with_prompt creates encrypted private + # key + with mock.patch( + "securesystemslib.interface.get_password", return_value=key_pw): + fn_prompt = gen_prompt() + priv = import_priv(fn_prompt, key_pw) + self.assertTrue(schema.matches(priv), assert_msg) + + # Test generate_and_write_*_keypair_with_prompt creates unencrypted + # private key if no password is entered + with mock.patch( + "securesystemslib.interface.get_password", return_value=""): + fn_empty_prompt = gen_prompt() + priv = import_priv(fn_empty_prompt) + self.assertTrue(schema.matches(priv), assert_msg) + + # Test generate_and_write_unencrypted_*_keypair doesn't encrypt + fn_unencrypted = gen_plain() + priv = import_priv(fn_unencrypted) + self.assertTrue(schema.matches(priv), assert_msg) + def test_import_publickeys_from_file(self): @@ -593,6 +741,37 @@ def test_import_publickeys_from_file(self): [KEY_TYPE_ED25519]) + def test_import_privatekey_from_file(self): + """Test generic private key import function. """ + + pw = "password" + for idx, (path, key_type, key_schema) in enumerate([ + (self.path_rsa, None, RSAKEY_SCHEMA), # default key type + (self.path_rsa, KEY_TYPE_RSA, RSAKEY_SCHEMA), + (self.path_ed25519, KEY_TYPE_ED25519, ED25519KEY_SCHEMA), + (self.path_ecdsa, KEY_TYPE_ECDSA, ECDSAKEY_SCHEMA)]): + + # Successfully import key per supported type, with ... + # ... passed password + key = import_privatekey_from_file(path, key_type=key_type, password=pw) + self.assertTrue(key_schema.matches(key), "(row {})".format(idx)) + + # ... entered password on mock-prompt + with mock.patch("securesystemslib.interface.get_password", return_value=pw): + key = import_privatekey_from_file(path, key_type=key_type, prompt=True) + self.assertTrue(key_schema.matches(key), "(row {})".format(idx)) + + # Error on wrong key for default key type + with self.assertRaises(Error): + import_privatekey_from_file(self.path_ed25519, password=pw) + + # Error on unsupported key type + with self.assertRaises(FormatError): + import_privatekey_from_file( + self.path_rsa, key_type="KEY_TYPE_UNSUPPORTED", password=pw) + + + # Run the test cases. if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini index c42e9a59..ca8bc98a 100644 --- a/tox.ini +++ b/tox.ini @@ -22,6 +22,7 @@ commands = [testenv:purepy27] deps = -r{toxinidir}/requirements-min.txt + -r{toxinidir}/requirements-test.txt commands = python -m tests.check_public_interfaces