From 6ebcb3d0c7e3d70ea06f3a0a1b3ba33fb1b8e841 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 15 Sep 2017 11:46:49 -0400 Subject: [PATCH 1/6] Quickfix unexpected encrypted PEM format This quickfix changes the PEM format from PKSC8 to PKSC5 (TraditionalOpenSSL) in pyca_crypto's variant of `create_rsa_encrypted_pem`. PKSC5 has the PEM headers expected by other PEM parsing functions, e.g. `is_pem_private` and `extract_pem`. See secure-systems-lab/securesystemslib#54 for more details --- securesystemslib/pyca_crypto_keys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/securesystemslib/pyca_crypto_keys.py b/securesystemslib/pyca_crypto_keys.py index ceef7c05..291c0272 100755 --- a/securesystemslib/pyca_crypto_keys.py +++ b/securesystemslib/pyca_crypto_keys.py @@ -540,7 +540,7 @@ def create_rsa_encrypted_pem(private_key, passphrase): encrypted_pem = \ private_key.private_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, + format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.BestAvailableEncryption(passphrase.encode('utf-8'))) return encrypted_pem.decode() From cd938203e1e7b6a3e42eca7245bd6f757dce21bf Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 15 Sep 2017 12:00:16 -0400 Subject: [PATCH 2/6] Update tests to catch unexpected PEM formats Test the output of `create_rsa_encrypted_pem` in conjunction with PEM parsing functions `extract_pem` and `is_private_pem`. --- tests/test_keys.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_keys.py b/tests/test_keys.py index 1359594d..8404b261 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -419,6 +419,7 @@ def test_create_rsa_encrypted_pem(self): scheme = 'rsassa-pss-sha256' encrypted_pem = KEYS.create_rsa_encrypted_pem(private, passphrase) self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches(encrypted_pem)) + self.assertTrue(KEYS.is_pem_private(encrypted_pem)) # Try to import the encrypted PEM file. rsakey = KEYS.import_rsakey_from_private_pem(encrypted_pem, scheme, passphrase) @@ -682,6 +683,13 @@ def test_extract_pem(self): private_pem=False) self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches(public_pem)) + # Test encrypted private pem + private_pem_encrypted = KEYS.create_rsa_encrypted_pem(private_pem, "pw") + private_pem_encrypted = KEYS.extract_pem(private_pem_encrypted, + private_pem=True) + self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches( + private_pem_encrypted)) + # Test for an invalid PEM. pem_header = '-----BEGIN RSA PRIVATE KEY-----' pem_footer = '-----END RSA PRIVATE KEY-----' @@ -723,7 +731,7 @@ def test_is_pem_public(self): public_pem = self.rsakey_dict['keyval']['public'] self.assertTrue(KEYS.is_pem_public(public_pem)) - # Tesst for a valid non-public PEM string. + # Test for a valid non-public PEM string. private_pem = self.rsakey_dict['keyval']['private'] self.assertFalse(KEYS.is_pem_public(private_pem)) @@ -737,9 +745,11 @@ def test_is_pem_private(self): # Test for a valid PEM string. private_pem = self.rsakey_dict['keyval']['private'] private_pem_ec = self.ecdsakey_dict['keyval']['private'] + private_pem_encrypted = KEYS.create_rsa_encrypted_pem(private_pem, "pw") self.assertTrue(KEYS.is_pem_private(private_pem)) self.assertTrue(KEYS.is_pem_private(private_pem_ec, 'ec')) + self.assertTrue(KEYS.is_pem_private(private_pem_encrypted)) # Test for a valid non-private PEM string. public_pem = self.rsakey_dict['keyval']['public'] From f63f9724a69d568de41d02c0b55b4a87a07fde8a Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 15 Sep 2017 13:20:25 -0400 Subject: [PATCH 3/6] Update outdated docstring/comments As @vladimir-v-diaz points out, comments and docstrings for securesystemslib/pyca_crypto_keys.py's `create_rsa_encrypted_pem` are outdated. This commit updates docstring, comments and some indentation guideline mismatches. --- securesystemslib/pyca_crypto_keys.py | 51 +++++++++++++--------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/securesystemslib/pyca_crypto_keys.py b/securesystemslib/pyca_crypto_keys.py index 291c0272..715a0029 100755 --- a/securesystemslib/pyca_crypto_keys.py +++ b/securesystemslib/pyca_crypto_keys.py @@ -473,11 +473,13 @@ def verify_rsa_signature(signature, signature_scheme, public_key, data): def create_rsa_encrypted_pem(private_key, passphrase): """ - Return a string in PEM format, where the private part of the RSA key is - encrypted. The private part of the RSA key is encrypted by the Triple - Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the - mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 - is used to strengthen 'passphrase'. + Return a string in PEM format (TraditionalOpenSSL), where the + private part of the RSA key is encrypted using the best available + encryption for a given key’s backend. This is a curated encryption choice + and the algorithm may change over time. + + c.f. cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/ + #cryptography.hazmat.primitives.serialization.BestAvailableEncryption >>> public, private = generate_rsa_public_and_private(2048) >>> passphrase = 'secret' @@ -491,26 +493,23 @@ def create_rsa_encrypted_pem(private_key, passphrase): passphrase: The passphrase, or password, to encrypt the private part of the RSA - key. 'passphrase' is not used directly as the encryption key, a stronger - encryption key is derived from it. + key. - securesystemslib.exceptions.FormatError, if the arguments are improperly formatted. + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - securesystemslib.exceptions.CryptoError, if an RSA key in encrypted PEM format cannot be created. + securesystemslib.exceptions.CryptoError, if the passed RSA key cannot be + deserialized by pyca cryptography. ValueError, if 'private_key' is unset. - - PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual - generation of the PEM-formatted output. - A string in PEM format, where the private RSA key is encrypted. - Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. + A string in PEM format (TraditionalOpenSSL), where the private RSA key is + encrypted. Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. """ - # Does 'private_key' have the correct format? # This check will ensure 'private_key' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'securesystemslib.exceptions.FormatError' if the check fails. @@ -519,29 +518,25 @@ def create_rsa_encrypted_pem(private_key, passphrase): # Does 'passphrase' have the correct format? securesystemslib.formats.PASSWORD_SCHEMA.check_match(passphrase) - # 'private_key' is in PEM format and unencrypted. The extracted key will be - # imported and converted to PyCrypto's RSA key object (i.e., - # Crypto.PublicKey.RSA). Use PyCrypto's exportKey method, with a passphrase - # specified, to create the string. PyCrypto uses PBKDF1+MD5 to strengthen - # 'passphrase', and 3DES with CBC mode for encryption. 'private_key' may - # still be a NULL string after the 'securesystemslib.formats.PEMRSA_SCHEMA' - # (i.e., 'private_key' has variable size and can be an empty string. + # 'private_key' may still be a NULL string after the + # 'securesystemslib.formats.PEMRSA_SCHEMA' so we need an additional check if len(private_key): try: private_key = load_pem_private_key(private_key.encode('utf-8'), password=None, backend=default_backend()) except ValueError: - raise securesystemslib.exceptions.CryptoError('The private key (in PEM format) could not be' - ' deserialized.') + raise securesystemslib.exceptions.CryptoError('The private key' + ' (in PEM format) could not be deserialized.') else: raise ValueError('The required private key is unset.') - encrypted_pem = \ - private_key.private_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(passphrase.encode('utf-8'))) + encrypted_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.BestAvailableEncryption( + passphrase.encode('utf-8'))) return encrypted_pem.decode() From a6b9877ad6885afce3d38b2904a13b7cd026096b Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 15 Sep 2017 13:29:35 -0400 Subject: [PATCH 4/6] Rename test variables and fix indentation Following @vladimir-v-diaz's review suggestions, this commit fixes some variable names and indentations in `test_keys.py` --- tests/test_keys.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_keys.py b/tests/test_keys.py index 8404b261..58954fe5 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -684,11 +684,11 @@ def test_extract_pem(self): self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches(public_pem)) # Test encrypted private pem - private_pem_encrypted = KEYS.create_rsa_encrypted_pem(private_pem, "pw") - private_pem_encrypted = KEYS.extract_pem(private_pem_encrypted, - private_pem=True) + encrypted_private_pem = KEYS.create_rsa_encrypted_pem(private_pem, "pw") + encrypted_private_pem_stripped = KEYS.extract_pem(encrypted_private_pem, + private_pem=True) self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches( - private_pem_encrypted)) + encrypted_private_pem_stripped)) # Test for an invalid PEM. pem_header = '-----BEGIN RSA PRIVATE KEY-----' @@ -743,11 +743,12 @@ def test_is_pem_public(self): def test_is_pem_private(self): # Test for a valid PEM string. - private_pem = self.rsakey_dict['keyval']['private'] + private_pem_rsa = self.rsakey_dict['keyval']['private'] private_pem_ec = self.ecdsakey_dict['keyval']['private'] - private_pem_encrypted = KEYS.create_rsa_encrypted_pem(private_pem, "pw") + encrypted_private_pem_rsa = KEYS.create_rsa_encrypted_pem( + private_pem_rsa, "pw") - self.assertTrue(KEYS.is_pem_private(private_pem)) + self.assertTrue(KEYS.is_pem_private(private_pem_rsa)) self.assertTrue(KEYS.is_pem_private(private_pem_ec, 'ec')) self.assertTrue(KEYS.is_pem_private(private_pem_encrypted)) @@ -759,11 +760,11 @@ def test_is_pem_private(self): # Test for unsupported keytype. self.assertRaises(securesystemslib.exceptions.FormatError, - KEYS.is_pem_private, private_pem, 'bad_keytype') + KEYS.is_pem_private, private_pem_rsa, 'bad_keytype') # Test for an invalid PEM string. self.assertRaises(securesystemslib.exceptions.FormatError, - KEYS.is_pem_private, 123) + KEYS.is_pem_private, 123) From 2e29dc63134d785f286f1e5f5681a8edf7e55d16 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 15 Sep 2017 13:20:25 -0400 Subject: [PATCH 5/6] Update outdated docstring/comments As @vladimir-v-diaz points out, comments and docstrings for securesystemslib/pyca_crypto_keys.py's `create_rsa_encrypted_pem` are outdated. This commit updates docstring, comments and some indentation guideline mismatches. --- securesystemslib/pyca_crypto_keys.py | 51 +++++++++++++--------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/securesystemslib/pyca_crypto_keys.py b/securesystemslib/pyca_crypto_keys.py index 291c0272..2f74462c 100755 --- a/securesystemslib/pyca_crypto_keys.py +++ b/securesystemslib/pyca_crypto_keys.py @@ -473,11 +473,13 @@ def verify_rsa_signature(signature, signature_scheme, public_key, data): def create_rsa_encrypted_pem(private_key, passphrase): """ - Return a string in PEM format, where the private part of the RSA key is - encrypted. The private part of the RSA key is encrypted by the Triple - Data Encryption Algorithm (3DES) and Cipher-block chaining (CBC) for the - mode of operation. Password-Based Key Derivation Function 1 (PBKF1) + MD5 - is used to strengthen 'passphrase'. + Return a string in PEM format (TraditionalOpenSSL), where the + private part of the RSA key is encrypted using the best available + encryption for a given key's backend. This is a curated encryption choice + and the algorithm may change over time. + + c.f. cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/ + #cryptography.hazmat.primitives.serialization.BestAvailableEncryption >>> public, private = generate_rsa_public_and_private(2048) >>> passphrase = 'secret' @@ -491,26 +493,23 @@ def create_rsa_encrypted_pem(private_key, passphrase): passphrase: The passphrase, or password, to encrypt the private part of the RSA - key. 'passphrase' is not used directly as the encryption key, a stronger - encryption key is derived from it. + key. - securesystemslib.exceptions.FormatError, if the arguments are improperly formatted. + securesystemslib.exceptions.FormatError, if the arguments are improperly + formatted. - securesystemslib.exceptions.CryptoError, if an RSA key in encrypted PEM format cannot be created. + securesystemslib.exceptions.CryptoError, if the passed RSA key cannot be + deserialized by pyca cryptography. ValueError, if 'private_key' is unset. - - PyCrypto's Crypto.PublicKey.RSA.exportKey() called to perform the actual - generation of the PEM-formatted output. - A string in PEM format, where the private RSA key is encrypted. - Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. + A string in PEM format (TraditionalOpenSSL), where the private RSA key is + encrypted. Conforms to 'securesystemslib.formats.PEMRSA_SCHEMA'. """ - # Does 'private_key' have the correct format? # This check will ensure 'private_key' has the appropriate number # of objects and object types, and that all dict keys are properly named. # Raise 'securesystemslib.exceptions.FormatError' if the check fails. @@ -519,29 +518,25 @@ def create_rsa_encrypted_pem(private_key, passphrase): # Does 'passphrase' have the correct format? securesystemslib.formats.PASSWORD_SCHEMA.check_match(passphrase) - # 'private_key' is in PEM format and unencrypted. The extracted key will be - # imported and converted to PyCrypto's RSA key object (i.e., - # Crypto.PublicKey.RSA). Use PyCrypto's exportKey method, with a passphrase - # specified, to create the string. PyCrypto uses PBKDF1+MD5 to strengthen - # 'passphrase', and 3DES with CBC mode for encryption. 'private_key' may - # still be a NULL string after the 'securesystemslib.formats.PEMRSA_SCHEMA' - # (i.e., 'private_key' has variable size and can be an empty string. + # 'private_key' may still be a NULL string after the + # 'securesystemslib.formats.PEMRSA_SCHEMA' so we need an additional check if len(private_key): try: private_key = load_pem_private_key(private_key.encode('utf-8'), password=None, backend=default_backend()) except ValueError: - raise securesystemslib.exceptions.CryptoError('The private key (in PEM format) could not be' - ' deserialized.') + raise securesystemslib.exceptions.CryptoError('The private key' + ' (in PEM format) could not be deserialized.') else: raise ValueError('The required private key is unset.') - encrypted_pem = \ - private_key.private_bytes(encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.BestAvailableEncryption(passphrase.encode('utf-8'))) + encrypted_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.BestAvailableEncryption( + passphrase.encode('utf-8'))) return encrypted_pem.decode() From 59f587622e578549a8457c4c4b5c5fad9f97b528 Mon Sep 17 00:00:00 2001 From: Lukas Puehringer Date: Fri, 15 Sep 2017 13:29:35 -0400 Subject: [PATCH 6/6] Rename test variables and fix indentation Following @vladimir-v-diaz's review suggestions, this commit fixes some variable names and indentations in `test_keys.py` --- tests/test_keys.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/test_keys.py b/tests/test_keys.py index 8404b261..57f0c2bd 100755 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -684,11 +684,11 @@ def test_extract_pem(self): self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches(public_pem)) # Test encrypted private pem - private_pem_encrypted = KEYS.create_rsa_encrypted_pem(private_pem, "pw") - private_pem_encrypted = KEYS.extract_pem(private_pem_encrypted, - private_pem=True) + encrypted_private_pem = KEYS.create_rsa_encrypted_pem(private_pem, "pw") + encrypted_private_pem_stripped = KEYS.extract_pem(encrypted_private_pem, + private_pem=True) self.assertTrue(securesystemslib.formats.PEMRSA_SCHEMA.matches( - private_pem_encrypted)) + encrypted_private_pem_stripped)) # Test for an invalid PEM. pem_header = '-----BEGIN RSA PRIVATE KEY-----' @@ -743,13 +743,14 @@ def test_is_pem_public(self): def test_is_pem_private(self): # Test for a valid PEM string. - private_pem = self.rsakey_dict['keyval']['private'] + private_pem_rsa = self.rsakey_dict['keyval']['private'] private_pem_ec = self.ecdsakey_dict['keyval']['private'] - private_pem_encrypted = KEYS.create_rsa_encrypted_pem(private_pem, "pw") + encrypted_private_pem_rsa = KEYS.create_rsa_encrypted_pem( + private_pem_rsa, "pw") - self.assertTrue(KEYS.is_pem_private(private_pem)) + self.assertTrue(KEYS.is_pem_private(private_pem_rsa)) self.assertTrue(KEYS.is_pem_private(private_pem_ec, 'ec')) - self.assertTrue(KEYS.is_pem_private(private_pem_encrypted)) + self.assertTrue(KEYS.is_pem_private(encrypted_private_pem_rsa)) # Test for a valid non-private PEM string. public_pem = self.rsakey_dict['keyval']['public'] @@ -759,11 +760,11 @@ def test_is_pem_private(self): # Test for unsupported keytype. self.assertRaises(securesystemslib.exceptions.FormatError, - KEYS.is_pem_private, private_pem, 'bad_keytype') + KEYS.is_pem_private, private_pem_rsa, 'bad_keytype') # Test for an invalid PEM string. self.assertRaises(securesystemslib.exceptions.FormatError, - KEYS.is_pem_private, 123) + KEYS.is_pem_private, 123)