From 9ee6998b823b285ef188267334a034ccae1d9dc8 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 9 Oct 2020 13:53:59 +0200 Subject: [PATCH 01/13] Add and adopt decryption password interface helper Add helper for private key import interface functions to use the, passed decryption password, or to prompt for a password based on a passed 'prompt' boolean, or to treat the key as unencrypted. The helper aggregates and replaces repetitive code in 'interface.import_{rsa, ed25519, ecdsa}_privatekey_from_file' functions, to make the password handling consistent across these functions. This commit only adopts the helper for rsa and ed25519 import functions. The ecdsa function requires more invasive changes (see subsequent commit). **Change of behavior**: Passing an empty string for a password no longer raises a FormatError, instead it is left to the underlying decryption function to fail with something like a "wrong password error", because there shouldn't be a difference between a wrong and a wrong empty password. See test changes for change of behavior. --- securesystemslib/interface.py | 101 ++++++++++++---------------------- tests/test_interface.py | 21 ++++--- 2 files changed, 48 insertions(+), 74 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 84a1d942..415c7c3c 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -122,6 +122,38 @@ def get_password(prompt='Password: ', confirm=False): +def _get_key_file_decryption_password(password, prompt, path): + """Decryption password helper. + + - Fail if 'password' is passed and 'prompt' is True (precedence unclear) + - Return None on empty pw on prompt (suggests desire to not decrypt) + + """ + 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 + + + def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, password=None): """ @@ -302,42 +334,7 @@ def import_rsa_privatekey_from_file(filepath, password=None, # 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_key_file_decryption_password(password, prompt, filepath) if storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() @@ -643,41 +640,11 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, # 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: diff --git a/tests/test_interface.py b/tests/test_interface.py index 48aa19a1..b13c790f 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -218,14 +218,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,6 +258,10 @@ 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): @@ -381,15 +385,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,6 +424,9 @@ 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): From a8508ee59c2cbfd918d10c424f9d9666b105b1b9 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 9 Oct 2020 13:57:57 +0200 Subject: [PATCH 02/13] Add and adopt encryption password interface helper Add helper for key pair generation interface functions to use the passed encryption password, or to prompt for a password based on a passed 'prompt' boolean, or to not encrypt the key. This helper aggregates and replaces repetitive code in 'interface.generate_and_write_{rsa, ed25519, ecdsa}_keypair' functions, to make the password handling consistent accross these functions and with private key import functions. The commit adopts the helper for rsa and ed25519 generation functions, adding an additional optional 'prompt' parameter. The ecdsa function requires more invasive changes (see subsequent commit). **Change of behavior**: - Passing None as 'password' no longer opens a prompt, but just not encrypts the key. To open a prompt the new boolean kwarg 'prompt' must be used. - Passing an empty password no longer writes the key unencrypted but raises a ValueError instead. The goal of these changes is to require the caller to express the encryption desire more explicitly. Also see test changes for change of behavior. --- securesystemslib/interface.py | 81 +++++++++++++++++++---------------- tests/test_interface.py | 52 ++++++++++++++++------ 2 files changed, 83 insertions(+), 50 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 415c7c3c..a969dad7 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -120,6 +120,41 @@ def get_password(prompt='Password: ', confirm=False): +def _get_key_file_encryption_password(password, prompt, path): + """Encryption password helper. + + - Fail if 'password' is passed and 'prompt' is True (precedence unclear) + - Fail if empty 'password' arg is passed (encryption desire unclear) + - Return None on empty pw on prompt (suggests desire to not encrypt) + + """ + 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): @@ -155,7 +190,7 @@ def _get_key_file_decryption_password(password, prompt, path): def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, - password=None): + password=None, prompt=False): """ Generate an RSA key pair. The public portion of the generated RSA key is @@ -198,6 +233,7 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, # Does 'bits' have the correct format? # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(bits) + password = _get_key_file_encryption_password(password, prompt, filepath) # Generate the public and private RSA keys. rsa_key = securesystemslib.keys.generate_rsa_key(bits) @@ -214,25 +250,9 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, # 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: - - # 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): + if password is not None: private = securesystemslib.keys.create_rsa_encrypted_pem(private, password) else: @@ -421,7 +441,8 @@ def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', -def generate_and_write_ed25519_keypair(filepath=None, password=None): +def generate_and_write_ed25519_keypair(filepath=None, password=None, + prompt=False): """ Generate an Ed25519 keypair, where the encrypted key (using 'password' as @@ -460,7 +481,8 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None): The 'filepath' of the written key. """ - # Generate a new Ed25519 key object. + password = _get_key_file_encryption_password(password, prompt, filepath) + ed25519_key = securesystemslib.keys.generate_ed25519_key() if not filepath: @@ -476,23 +498,6 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None): # 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. - if password is None: - - # 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) - - 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) - # If the parent directory of filepath does not exist, # create it (and all its parent directories, if necessary). securesystemslib.util.ensure_parent_dir(filepath) @@ -522,7 +527,7 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None): file_object = tempfile.TemporaryFile() # Encrypt the private key if 'password' is set. - if len(password): + if password is not None: ed25519_key = securesystemslib.keys.encrypt_key(ed25519_key, password) else: diff --git a/tests/test_interface.py b/tests/test_interface.py index b13c790f..47724114 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -105,8 +105,7 @@ def test_rsa(self): # 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 +122,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 +139,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 @@ -161,7 +160,7 @@ def test_rsa(self): 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,13 +184,28 @@ 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) @@ -270,8 +284,7 @@ def test_ed25519(self): # 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) @@ -295,7 +308,7 @@ def test_ed25519(self): # 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( @@ -310,7 +323,7 @@ def test_ed25519(self): 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) @@ -347,11 +360,26 @@ 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) From bc62907b6da8d222d1c0093e0f7fd9be86736156 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 9 Oct 2020 14:01:51 +0200 Subject: [PATCH 03/13] Adopt password helper in ecdsa interface functions Use recently added encryption (key generation) and decryption (key import) password helper for ecdsa interface functions for consistency with rsa and ed25519 interface functions. This also updates 'generate_and_write_ecdsa_keypair' and 'import_ecdsa_privatekey_from_file' to accept an additional optional 'prompt' parameter, and to also handle unencrypted keys. --- securesystemslib/interface.py | 62 +++++++++--------------- tests/test_interface.py | 89 +++++++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index a969dad7..42049b72 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -662,7 +662,8 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, -def generate_and_write_ecdsa_keypair(filepath=None, password=None): +def generate_and_write_ecdsa_keypair(filepath=None, password=None, + prompt=False): """ Generate an ECDSA keypair, where the encrypted key (using 'password' as the @@ -701,6 +702,8 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None): The 'filepath' of the written key. """ + password = _get_key_file_encryption_password(password, prompt, filepath) + # 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() @@ -716,23 +719,6 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None): # 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. - if password is None: - - # 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) - - 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) - # If the parent directory of filepath does not exist, # create it (and all its parent directories, if necessary). securesystemslib.util.ensure_parent_dir(filepath) @@ -759,10 +745,16 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None): # Write the encrypted key string, conformant to # 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA', to ''. file_object = tempfile.TemporaryFile() + + if password is not None: + ecdsa_key = securesystemslib.keys.encrypt_key(ecdsa_key, password) + + else: + ecdsa_key = json.dumps(ecdsa_key) + # 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 @@ -814,7 +806,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, storage_backend=None): """ @@ -858,37 +850,25 @@ def import_ecdsa_privatekey_from_file(filepath, password=None, # 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: - - # 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) + 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) + 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 diff --git a/tests/test_interface.py b/tests/test_interface.py index 47724114..ea7d9d81 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -459,19 +459,13 @@ def test_ed25519(self): 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: 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)) @@ -480,13 +474,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"]) @@ -499,11 +500,11 @@ def test_ecdsa(self): 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) @@ -529,11 +530,26 @@ 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) @@ -542,8 +558,8 @@ def test_ecdsa(self): # 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: @@ -557,14 +573,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) @@ -573,6 +598,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) @@ -583,6 +618,10 @@ 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_import_publickeys_from_file(self): From 7d00ecb4ab368529d40373ec075591ab164734ff Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 16 Oct 2020 11:27:36 +0200 Subject: [PATCH 04/13] Clean up interface function bodies Minor non-behavior changing clean-ups regarding vertical space and grouping, as well as code comments in interface functions. Also removes seemingly random logger statements. --- securesystemslib/interface.py | 194 ++++++++-------------------------- 1 file changed, 44 insertions(+), 150 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 42049b72..c261b803 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -97,9 +97,6 @@ def get_password(prompt='Password: ', confirm=False): 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) @@ -189,6 +186,7 @@ def _get_key_file_decryption_password(password, prompt, path): + def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, password=None, prompt=False): """ @@ -230,49 +228,34 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, The 'filepath' of the written key. """ - # Does 'bits' have the correct format? - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(bits) + password = _get_key_file_encryption_password(password, prompt, filepath) - # 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) - - # Encrypt the private key if 'password' is set. + # 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) @@ -345,29 +328,20 @@ 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) + 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 @@ -408,25 +382,16 @@ def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', 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) @@ -485,58 +450,35 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None, ed25519_key = securesystemslib.keys.generate_ed25519_key() + # Use passed 'filepath' or keyid as file name if not filepath: filepath = os.path.join(os.getcwd(), ed25519_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? - # 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 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. + # 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) @@ -569,23 +511,15 @@ def import_ed25519_publickey_from_file(filepath): An ED25519 key object conformant to '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) - # 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, junk = 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) @@ -596,6 +530,7 @@ def import_ed25519_publickey_from_file(filepath): + def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, storage_backend=None): """ @@ -639,24 +574,19 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, 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) password = _get_key_file_decryption_password(password, prompt, filepath) if storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() - # 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) @@ -704,56 +634,37 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None, password = _get_key_file_encryption_password(password, prompt, filepath) - # 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() + # Use passed 'filepath' or keyid as file name if not filepath: filepath = os.path.join(os.getcwd(), ecdsa_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? - # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. securesystemslib.formats.PATH_SCHEMA.check_match(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 ''. - file_object = tempfile.TemporaryFile() - + # 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) - # Raise 'securesystemslib.exceptions.CryptoError' if 'ecdsa_key' cannot be - # encrypted. + # Write private key to + file_object = tempfile.TemporaryFile() file_object.write(ecdsa_key.encode('utf-8')) securesystemslib.util.persist_temp_file(file_object, filepath) @@ -786,19 +697,13 @@ def import_ecdsa_publickey_from_file(filepath): An ECDSA key object conformant to '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) - # 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. + # 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, junk = \ - securesystemslib.keys.format_metadata_to_key(ecdsa_key_metadata) + ecdsa_key, junk = securesystemslib.keys.format_metadata_to_key( + ecdsa_key_metadata) return ecdsa_key @@ -843,11 +748,6 @@ def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False, 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) password = _get_key_file_decryption_password(password, prompt, filepath) @@ -855,21 +755,15 @@ def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False, if storage_backend is None: storage_backend = securesystemslib.storage.FilesystemBackend() - # Store the encrypted contents of 'filepath' prior to calling the decryption - # routine. with storage_backend.get(filepath) as file_object: 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. + # 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 # in order to support key files generated with older versions of From 2379b7f5d2bb7009a47d31b08a114d72ac1882b5 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 16 Oct 2020 11:49:40 +0200 Subject: [PATCH 05/13] Reformat, update and clean-up interface docstrings - Change docstring format to Google Style better auto-docs rendering (http://secure-systems-lab/code-style-guidelines#20) - Document recent interface function changes (args, errors) - Thoroughly document which exceptions may be raised - Correct mistakes about used encryption. - Generally overhaul docstrings with the goal to make them more concise, but more helpful for a user too, e.g. by mentioning signing schemes. --- securesystemslib/gpg/functions.py | 62 ++-- securesystemslib/interface.py | 525 ++++++++++++------------------ 2 files changed, 235 insertions(+), 352 deletions(-) diff --git a/securesystemslib/gpg/functions.py b/securesystemslib/gpg/functions.py index 8c157c51..caae6a70 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 c261b803..3729725b 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -73,29 +73,16 @@ 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. """ securesystemslib.formats.TEXT_SCHEMA.check_match(prompt) securesystemslib.formats.BOOLEAN_SCHEMA.check_match(confirm) @@ -189,45 +176,41 @@ def _get_key_file_decryption_password(password, prompt, path): def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, password=None, prompt=False): - """ - - 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. - """ + """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: + Writes key files to disk. + + Returns: + The private key filepath. + + """ securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(bits) password = _get_key_file_encryption_password(password, prompt, filepath) @@ -268,64 +251,31 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, def import_rsa_privatekey_from_file(filepath, password=None, scheme='rsassa-pss-sha256', prompt=False, storage_backend=None): - """ - - Import the PEM file in 'filepath' containing the private key. - - If password is passed use passed password for decryption. - If prompt is True use entered password for decryption. - If no password is passed 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. - - - filepath: - file, an RSA encrypted PEM file. Unlike the public RSA PEM - key file, 'filepath' does not have an extension. + """Imports PEM-encoded RSA private key from file storage. - password: - The passphrase to decrypt 'filepath'. + 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. - scheme: - The signature scheme used by the imported key. - - prompt: - If True the user is prompted for a passphrase to decrypt 'filepath'. - Default is False. - - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. - - - ValueError, if 'password' is passed and 'prompt' is True. - - ValueError, if 'password' is passed and it is an empty string. - - securesystemslib.exceptions.FormatError, if the arguments are improperly - formatted. - - securesystemslib.exceptions.FormatError, if the entered password is - improperly formatted. - - IOError, if 'filepath' can't be loaded. - - securesystemslib.exceptions.CryptoError, if a password is available - and 'filepath' is not a valid key file encrypted using that password. - - securesystemslib.exceptions.CryptoError, if no password is available - and 'filepath' is not a valid non-encrypted key file. + 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. - - The contents of 'filepath' are read, optionally decrypted, and returned. + 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. - - An RSA key object, conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'. + Returns: + An RSA private key object conformant with 'RSAKEY_SCHEMA'. """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) @@ -351,36 +301,26 @@ def import_rsa_privatekey_from_file(filepath, password=None, 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. - - - filepath: - .pub file, an RSA PEM file. - - scheme: - The signature scheme used by the imported key. + """Imports PEM-encoded RSA public key from file storage. - storage_backend: - An object which implements - securesystemslib.storage.StorageBackendInterface. When no object is - passed a FilesystemBackend will be instantiated and used. + The expected key format is X.509 SubjectPublicKeyInfo. - - 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'. """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme) @@ -408,44 +348,37 @@ def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', def generate_and_write_ed25519_keypair(filepath=None, password=None, prompt=False): - """ - - 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. - """ + """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. + + 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'. + 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/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. + + Side Effects: + Writes key files to disk. + + Returns: + The private key filepath. + + """ password = _get_key_file_encryption_password(password, prompt, filepath) ed25519_key = securesystemslib.keys.generate_ed25519_key() @@ -488,28 +421,21 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None, 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. + """Imports custom JSON-formatted ed25519 public key from disk. - If the key object in 'filepath' contains a private key, it is discarded. + NOTE: The signing scheme is set at key generation (see generate function). - - filepath: - .pub file, a public key file. + Arguments: + filepath: The path to read the file from. - - securesystemslib.exceptions.FormatError, if 'filepath' is improperly - formatted or is an unexpected key type. + Raises: + FormatError: Argument is malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. - - The contents of 'filepath' is read and saved. + Returns: + An ed25519 public key object conformant with 'ED25519KEY_SCHEMA'. - - An ED25519 key object conformant to - 'securesystemslib.formats.ED25519KEY_SCHEMA'. """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) @@ -533,46 +459,34 @@ 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'. """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) password = _get_key_file_decryption_password(password, prompt, filepath) @@ -594,44 +508,37 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, def generate_and_write_ecdsa_keypair(filepath=None, password=None, prompt=False): - """ - - 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. - """ + """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. + + 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'. + 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: + Writes key files to disk. + + Returns: + The private key filepath. + + """ password = _get_key_file_encryption_password(password, prompt, filepath) ecdsa_key = securesystemslib.keys.generate_ecdsa_key() @@ -674,28 +581,21 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None, 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. + """Imports custom JSON-formatted ecdsa public key from disk. - If the key object in 'filepath' contains a private key, it is discarded. + NOTE: The signing scheme is set at key generation (see generate function). - - filepath: - .pub file, a public key file. + Arguments: + filepath: The path to read the file from. - - securesystemslib.exceptions.FormatError, if 'filepath' is improperly - formatted or is an unexpected key type. + Raises: + FormatError: Argument is malformed. + StorageError: Key file cannot be read. + Error: Public key is malformed. - - The contents of 'filepath' is read and saved. + Returns: + An ecdsa public key object conformant with 'ECDSAKEY_SCHEMA'. - - An ECDSA key object conformant to - 'securesystemslib.formats.ECDSAKEY_SCHEMA'. """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) @@ -713,40 +613,32 @@ def import_ecdsa_publickey_from_file(filepath): def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False, storage_backend=None): - """ - - Import the encrypted ECDSA key file in 'filepath', decrypt it, and return - the key object in 'securesystemslib.formats.ECDSAKEY_SCHEMA' format. + """Imports custom JSON-formatted ecdsa private key from file storage. - The 'cryptography' library is currently supported and performs the actual - cryptographic routine. + If a password is passed or entered on the prompt, the private key is + decrypted, otherwise it is treated as unencrypted. - - filepath: - file, an ECDSA encrypted key file. + NOTE: The signing scheme is set at key generation (see generate function). - 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. - - 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 'ecdsa'). + 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. - securesystemslib.exceptions.CryptoError, if 'filepath' cannot be decrypted. + 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. - - 'password' is used to decrypt the 'filepath' key file. + Returns: + An ecdsa private key object conformant with 'ED25519KEY_SCHEMA'. - - An ECDSA key object of the form: 'securesystemslib.formats.ECDSAKEY_SCHEMA'. """ securesystemslib.formats.PATH_SCHEMA.check_match(filepath) @@ -786,22 +678,25 @@ def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False, 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. From 7d37d5575bc8fd24a73bfc02bd0d6acda92f98d1 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Mon, 12 Oct 2020 15:14:59 +0200 Subject: [PATCH 06/13] Adopt recent interface changes in README.rst Add 'prompt' argument and updates comments in sample code snippets. --- README.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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: From 698f54faef4de2a7214f6b0b831f3472214f2313 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 14 Oct 2020 12:56:31 +0200 Subject: [PATCH 07/13] Add generic import_privatekey_from_file function Add convenience dispatcher for other private key import interface functions, to import any of the supported private key types from file (rsa, ecdsa, ed25519). This transfers a similar function, currently implemented in in-toto.util, in order to close in-toto/in-toto#80. Caveat: - The key type must be specified via argument (or defaults to RSA). In the future we might want to let the parser infer the key type, as we do in the related in-toto-golang implementation. See https://github.com/in-toto/in-toto-golang/blob/5fba7c22a062a30b6e373f33362d647eabf15822/in_toto/keylib.go#L281-L361 - Currently, the function does not support a signing scheme parameter and thus assigns the default value from import_rsa_privatekey_from_file to the returned key. For the other keep types, the scheme is encoded in the on-disk format. In the future we might want to consolidate this as part of #251. For now the primary goal is to have a simple interface that is enough to close in-toto/in-toto#80. --- securesystemslib/interface.py | 55 +++++++++++++++++++++++++++++++++++ tests/test_interface.py | 34 +++++++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 3729725b..fe4b2816 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -732,6 +732,61 @@ 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_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: + 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/test_interface.py b/tests/test_interface.py index ea7d9d81..49adfd6f 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -69,7 +69,8 @@ generate_and_write_ecdsa_keypair, import_ecdsa_publickey_from_file, import_ecdsa_privatekey_from_file, - import_publickeys_from_file) + import_publickeys_from_file, + import_privatekey_from_file) @@ -667,6 +668,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() From 1db7f213d8a5b207b4ff4a3e1d41e459760690d7 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 28 Oct 2020 10:22:22 +0100 Subject: [PATCH 08/13] Fix docstring and rename junk vars in interface --- securesystemslib/interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index fe4b2816..3595acd6 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -442,7 +442,7 @@ def import_ed25519_publickey_from_file(filepath): # 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, _ = securesystemslib.keys.format_metadata_to_key( ed25519_key_metadata) # Check that the generic loading functions indeed loaded an ed25519 key @@ -602,7 +602,7 @@ def import_ecdsa_publickey_from_file(filepath): # 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, junk = securesystemslib.keys.format_metadata_to_key( + ecdsa_key, _ = securesystemslib.keys.format_metadata_to_key( ecdsa_key_metadata) return ecdsa_key @@ -741,7 +741,7 @@ def import_privatekey_from_file(filepath, key_type=None, password=None, decrypted, otherwise it is treated as unencrypted. 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 + 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). From fae4ebc0dd8082f10c5e7d27c0974d5f90b784d2 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 4 Nov 2020 13:27:50 +0100 Subject: [PATCH 09/13] Add blank lines and minor doc fixes in interface Make vertical space between functions consistent and add missing side effects to docstrings. --- securesystemslib/interface.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 3595acd6..44e0a6e1 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -173,7 +173,6 @@ def _get_key_file_decryption_password(password, prompt, path): - 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. @@ -205,7 +204,8 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, StorageError: Key files cannot be written. Side Effects: - Writes key files to disk. + Prompts user for a password if 'prompt' is True. + Writes key files to disk. Returns: The private key filepath. @@ -247,7 +247,6 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, - def import_rsa_privatekey_from_file(filepath, password=None, scheme='rsassa-pss-sha256', prompt=False, storage_backend=None): @@ -297,8 +296,6 @@ def import_rsa_privatekey_from_file(filepath, password=None, - - def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', storage_backend=None): """Imports PEM-encoded RSA public key from file storage. @@ -344,8 +341,6 @@ 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. @@ -373,6 +368,7 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None, StorageError: Key files cannot be written. Side Effects: + Prompts user for a password if 'prompt' is True. Writes key files to disk. Returns: @@ -419,7 +415,6 @@ def generate_and_write_ed25519_keypair(filepath=None, password=None, - def import_ed25519_publickey_from_file(filepath): """Imports custom JSON-formatted ed25519 public key from disk. @@ -454,9 +449,6 @@ def import_ed25519_publickey_from_file(filepath): - - - def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, storage_backend=None): """Imports custom JSON-formatted ed25519 private key from file storage. @@ -504,8 +496,6 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, - - def generate_and_write_ecdsa_keypair(filepath=None, password=None, prompt=False): """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. @@ -533,6 +523,7 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None, StorageError: Key files cannot be written. Side Effects: + Prompts user for a password if 'prompt' is True. Writes key files to disk. Returns: @@ -579,7 +570,6 @@ def generate_and_write_ecdsa_keypair(filepath=None, password=None, - def import_ecdsa_publickey_from_file(filepath): """Imports custom JSON-formatted ecdsa public key from disk. @@ -609,8 +599,6 @@ def import_ecdsa_publickey_from_file(filepath): - - def import_ecdsa_privatekey_from_file(filepath, password=None, prompt=False, storage_backend=None): """Imports custom JSON-formatted ecdsa private key from file storage. @@ -787,6 +775,7 @@ def import_privatekey_from_file(filepath, key_type=None, password=None, 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: From dd2c5fc3e59476820b1dcc9513519f1ac58f4a9c Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 4 Nov 2020 13:05:05 +0100 Subject: [PATCH 10/13] Make interface.generate_*_keypair funcs protected 'interface.generate_and_write{rsa,ed25519.ecdsa}_keypair' functions were recently changed to disable default password prompts and generally make the private key encryption behavior more explicit. This change also made it so that a default call of the functions would leave private keys unencrypted, which is not desirable as per the principle of 'secure defaults' (see heated discussion in #288). This commit makes the corresponding functions protected, so that they may still be used internally (in securesystemslib/in-toto/tuf). Corresponding public interface functions that neither offer unexpected prompts, and encrypt by default will be added in subsequent commits. --- securesystemslib/interface.py | 6 ++-- tests/check_public_interfaces.py | 6 ++-- tests/test_interface.py | 56 ++++++++++++++++---------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 44e0a6e1..dbce936d 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -173,7 +173,7 @@ def _get_key_file_decryption_password(password, prompt, path): -def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, +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. @@ -341,7 +341,7 @@ def import_rsa_publickey_from_file(filepath, scheme='rsassa-pss-sha256', -def generate_and_write_ed25519_keypair(filepath=None, password=None, +def _generate_and_write_ed25519_keypair(filepath=None, password=None, prompt=False): """Generates ed25519 key pair and writes custom JSON-formatted keys to disk. @@ -496,7 +496,7 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False, -def generate_and_write_ecdsa_keypair(filepath=None, password=None, +def _generate_and_write_ecdsa_keypair(filepath=None, password=None, prompt=False): """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index dcec9c00..5005c707 100644 --- a/tests/check_public_interfaces.py +++ b/tests/check_public_interfaces.py @@ -64,7 +64,7 @@ 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): @@ -75,7 +75,7 @@ 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( @@ -88,7 +88,7 @@ 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( diff --git a/tests/test_interface.py b/tests/test_interface.py index 49adfd6f..334775bd 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -60,13 +60,13 @@ from securesystemslib.exceptions import Error, FormatError, CryptoError from securesystemslib.interface import ( - generate_and_write_rsa_keypair, + _generate_and_write_rsa_keypair, import_rsa_privatekey_from_file, import_rsa_publickey_from_file, - generate_and_write_ed25519_keypair, + _generate_and_write_ed25519_keypair, import_ed25519_publickey_from_file, import_ed25519_privatekey_from_file, - generate_and_write_ecdsa_keypair, + _generate_and_write_ecdsa_keypair, import_ecdsa_publickey_from_file, import_ecdsa_privatekey_from_file, import_publickeys_from_file, @@ -101,12 +101,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) + 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 +123,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, prompt=True) + _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() + 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 +140,7 @@ def test_rsa(self): # Assert length bits = 4096 fn_bits = "bits" - generate_and_write_rsa_keypair(filepath=fn_bits, 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 +158,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, prompt=True) + _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) @@ -194,7 +194,7 @@ def test_rsa(self): "passing 'password' and 'prompt=True' is not allowed")]): with self.assertRaises(ValueError, msg="(row {})".format(idx)) as ctx: - generate_and_write_rsa_keypair(**kwargs) + _generate_and_write_rsa_keypair(**kwargs) self.assertEqual(err_msg, str(ctx.exception), "expected: '{}' got: '{}' (row {})".format( @@ -208,7 +208,7 @@ def test_rsa(self): {"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 @@ -280,12 +280,12 @@ def test_rsa(self): 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) + 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) @@ -302,14 +302,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() + 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( @@ -321,10 +321,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, prompt=True) + _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) @@ -370,7 +370,7 @@ def test_ed25519(self): "passing 'password' and 'prompt=True' is not allowed")]): with self.assertRaises(ValueError, msg="(row {})".format(idx)) as ctx: - generate_and_write_ed25519_keypair(**kwargs) + _generate_and_write_ed25519_keypair(**kwargs) self.assertEqual(err_msg, str(ctx.exception), "expected: '{}' got: '{}' (row {})".format( @@ -382,7 +382,7 @@ def test_ed25519(self): {"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 @@ -459,11 +459,11 @@ def test_ed25519(self): def test_ecdsa(self): - """Test ecdsa key generation and import interface functions. """ + """Test ecdsa 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_ecdsa_keypair(filepath=fn_default) + 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) @@ -479,14 +479,14 @@ def test_ecdsa(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_ecdsa_keypair(filepath=fn_empty_prompt) + _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() + fn_keyid = _generate_and_write_ecdsa_keypair() pub = import_ecdsa_publickey_from_file(fn_keyid + ".pub") priv = import_ecdsa_privatekey_from_file(fn_keyid) self.assertTrue( @@ -498,10 +498,10 @@ 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, prompt=True) + _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, prompt=True) @@ -540,7 +540,7 @@ def test_ecdsa(self): "passing 'password' and 'prompt=True' is not allowed")]): with self.assertRaises(ValueError, msg="(row {})".format(idx)) as ctx: - generate_and_write_ecdsa_keypair(**kwargs) + _generate_and_write_ecdsa_keypair(**kwargs) self.assertEqual(err_msg, str(ctx.exception), "expected: '{}' got: '{}' (row {})".format( @@ -552,7 +552,7 @@ def test_ecdsa(self): {"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 From edcf74e9ce61caabbbae725d500dba46683a96f1 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Wed, 4 Nov 2020 15:02:12 +0100 Subject: [PATCH 11/13] Add thin interface._generate_*_keypair wrappers Based on discussions in #288, add the following keypair generation interface functions for each key type: - generate_and_write_*_keypair - generate_and_write_*_keypair_with_prompt - generate_and_write_unencrypted_*_keypair Other than the underlying `_generate_*_keypair` functions, these thin wrappers respect the principle of 'secure defaults', i.e. encrypt private keys per default, and still don't surprise callers with prompts, both unless made explicit through the function name. --- securesystemslib/interface.py | 284 +++++++++++++++++++++++++++++++ tests/check_public_interfaces.py | 55 +++++- tests/test_interface.py | 73 ++++++++ tox.ini | 1 + 4 files changed, 411 insertions(+), 2 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index dbce936d..7b7a342b 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -247,6 +247,109 @@ 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. + + """ + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=password, prompt=False) + + + +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. + + 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. + + 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. + + Side Effects: + Prompts user for a password. + Writes key files to disk. + + Returns: + The private key filepath. + + """ + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=None, prompt=True) + + + +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. + + 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. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. + + Side Effects: + Writes unencrypted key files to disk. + + Returns: + The private key filepath. + + """ + return _generate_and_write_rsa_keypair( + filepath=filepath, bits=bits, password=None, prompt=False) + + + def import_rsa_privatekey_from_file(filepath, password=None, scheme='rsassa-pss-sha256', prompt=False, storage_backend=None): @@ -415,6 +518,96 @@ 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. + + """ + 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. + + 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: + Writes unencrypted key files to disk. + + Returns: + The private key filepath. + + """ + 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. @@ -570,6 +763,97 @@ def _generate_and_write_ecdsa_keypair(filepath=None, password=None, +def generate_and_write_ecdsa_keypair(password, 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 passed + password strengthened in PBKDF2-HMAC-SHA256. + + NOTE: The custom key format includes 'ecdsa-sha2-nistp256' 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/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_ecdsa_keypair( + filepath=filepath, password=password, prompt=False) + + + +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. + + """ + return _generate_and_write_ecdsa_keypair( + filepath=filepath, password=None, prompt=True) + + + +def generate_and_write_unencrypted_ecdsa_keypair(filepath=None): + """Generates ecdsa key pair and writes custom JSON-formatted keys to disk. + + 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: + Writes unencrypted key files to disk. + + Returns: + The private key filepath. + + """ + return _generate_and_write_ecdsa_keypair( + filepath=filepath, password=None, prompt=False) + + + def import_ecdsa_publickey_from_file(filepath): """Imports custom JSON-formatted ecdsa public key from disk. diff --git a/tests/check_public_interfaces.py b/tests/check_public_interfaces.py index 5005c707..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 @@ -66,6 +70,25 @@ def test_interface(self): securesystemslib.exceptions.UnsupportedLibraryError): 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): path = os.path.join(self.temp_dir, 'rsa_key') @@ -78,6 +101,21 @@ def test_interface(self): 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') @@ -91,13 +129,26 @@ def test_interface(self): 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 334775bd..6c9c4f6e 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -61,12 +61,21 @@ 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, @@ -625,6 +634,70 @@ def test_ecdsa(self): + 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): """Test import multiple public keys with different types. """ 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 From 1cb5bcdd0be6a9e245135fb1164be99b48da9529 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Thu, 5 Nov 2020 13:40:26 +0100 Subject: [PATCH 12/13] Fix interface encryption password helper call time The _get_key_file_encryption_password helper needs to be called after the passed or keyid-based filepath has been determined, i.e. after key creation in the latter case, because it might be displayed on the password prompt. Plus remove obsolete quotes. --- securesystemslib/interface.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 7b7a342b..ed6cdb60 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -122,7 +122,7 @@ def _get_key_file_encryption_password(password, prompt, path): 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) + "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. @@ -158,7 +158,7 @@ def _get_key_file_decryption_password(password, prompt, path): 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) + "(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. @@ -213,8 +213,6 @@ def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, """ securesystemslib.formats.RSAKEYBITS_SCHEMA.check_match(bits) - password = _get_key_file_encryption_password(password, prompt, filepath) - # 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'] @@ -226,6 +224,8 @@ def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, securesystemslib.formats.PATH_SCHEMA.check_match(filepath) + password = _get_key_file_encryption_password(password, prompt, filepath) + # 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) @@ -478,8 +478,6 @@ def _generate_and_write_ed25519_keypair(filepath=None, password=None, The private key filepath. """ - password = _get_key_file_encryption_password(password, prompt, filepath) - ed25519_key = securesystemslib.keys.generate_ed25519_key() # Use passed 'filepath' or keyid as file name @@ -488,6 +486,8 @@ def _generate_and_write_ed25519_keypair(filepath=None, password=None, securesystemslib.formats.PATH_SCHEMA.check_match(filepath) + password = _get_key_file_encryption_password(password, prompt, filepath) + # Create intermediate directories as required securesystemslib.util.ensure_parent_dir(filepath) @@ -723,8 +723,6 @@ def _generate_and_write_ecdsa_keypair(filepath=None, password=None, The private key filepath. """ - password = _get_key_file_encryption_password(password, prompt, filepath) - ecdsa_key = securesystemslib.keys.generate_ecdsa_key() # Use passed 'filepath' or keyid as file name @@ -733,6 +731,8 @@ def _generate_and_write_ecdsa_keypair(filepath=None, password=None, securesystemslib.formats.PATH_SCHEMA.check_match(filepath) + password = _get_key_file_encryption_password(password, prompt, filepath) + # Create intermediate directories as required securesystemslib.util.ensure_parent_dir(filepath) From 9a74f30976bf517a7489ec311fd4e13868c1697e Mon Sep 17 00:00:00 2001 From: lukpueh Date: Fri, 6 Nov 2020 09:25:35 +0100 Subject: [PATCH 13/13] Refine interface helper docstrings Clarify how 'password' and 'prompt' arguments affect encryption and decryption of private keys. Co-authored-by: Trishank Karthik Kuppusamy --- securesystemslib/interface.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index ed6cdb60..27d9e3b6 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -105,12 +105,18 @@ def get_password(prompt='Password: ', confirm=False): def _get_key_file_encryption_password(password, prompt, path): - """Encryption password helper. - - - Fail if 'password' is passed and 'prompt' is True (precedence unclear) - - Fail if empty 'password' arg is passed (encryption desire unclear) - - Return None on empty pw on prompt (suggests desire to not encrypt) - + """Encryption password helper for `_generate_and_write_*_keypair` functions. + + 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) """ securesystemslib.formats.BOOLEAN_SCHEMA.check_match(prompt) @@ -142,10 +148,17 @@ def _get_key_file_encryption_password(password, prompt, path): def _get_key_file_decryption_password(password, prompt, path): - """Decryption password helper. - - - Fail if 'password' is passed and 'prompt' is True (precedence unclear) - - Return None on empty pw on prompt (suggests desire to not decrypt) + """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) @@ -179,7 +192,7 @@ def _generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, 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 + 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.