-
Notifications
You must be signed in to change notification settings - Fork 54
WIP: sphincs+ support, for post-quantum crypto #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8f7307f
79b4cd7
c43c5b2
94a6e29
a8f0ea7
c81e51f
5eb5f43
6e9cab1
0c8010d
6212fa9
33d70bb
e713a24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| cryptography | ||
| pynacl | ||
| pyspx | ||
| tox | ||
| coverage | ||
| coveralls | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| cryptography==2.3.1 | ||
| pynacl==1.2.1 | ||
| pyspx==0.2.0 | ||
| six==1.11.0 | ||
| tox==3.2.1 | ||
| coveralls==1.3.0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| cryptography | ||
| pynacl | ||
| pyspx | ||
| six | ||
| colorama |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,7 +61,7 @@ | |
| DEFAULT_RSA_KEY_BITS = 3072 | ||
|
|
||
| # Supported key types. | ||
| SUPPORTED_KEY_TYPES = ['rsa', 'ed25519'] | ||
| SUPPORTED_KEY_TYPES = ['rsa', 'ed25519', 'ecdsa-sha2-nistp256', 'spx'] | ||
|
|
||
|
|
||
|
|
||
|
|
@@ -641,7 +641,7 @@ def import_ed25519_privatekey_from_file(filepath, password=None, prompt=False): | |
| # 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' | ||
| password = get_password('Enter a password for an encrypted Ed25519' | ||
| ' file \'' + Fore.RED + filepath + Fore.RESET + '\': ', | ||
| confirm=False) | ||
|
|
||
|
|
@@ -900,6 +900,260 @@ def import_ecdsa_privatekey_from_file(filepath, password=None): | |
| return key_object | ||
|
|
||
|
|
||
| def generate_and_write_spx_keypair(filepath=None, password=None): | ||
| """ | ||
| <Purpose> | ||
| Generate an SPX keypair, where the encrypted key (using 'password' as | ||
| the passphrase) is saved to <'filepath'>. The public key portion of the | ||
| generated SPX 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." | ||
|
|
||
| <Arguments> | ||
| filepath: | ||
| The public and private key files are saved to <filepath>.pub and | ||
| <filepath>, respectively. If the filepath is not given, the public and | ||
| private keys are saved to the current working directory as <KEYID>.pub | ||
| and <KEYID>. KEYID is the generated key's KEYID. | ||
|
|
||
| password: | ||
| The password, or passphrase, to encrypt the private portion of the | ||
| generated SPX key. A symmetric encryption key is derived from | ||
| 'password', so it is not directly used. | ||
|
|
||
| <Exceptions> | ||
| securesystemslib.exceptions.FormatError, if the arguments are improperly | ||
| formatted. | ||
|
|
||
| securesystemslib.exceptions.CryptoError, if 'filepath' cannot be encrypted. | ||
|
|
||
| <Side Effects> | ||
| Writes key files to '<filepath>' and '<filepath>.pub'. | ||
|
|
||
| <Returns> | ||
| The 'filepath' of the written key. | ||
| """ | ||
|
|
||
| # Generate a new SPX key object. | ||
| spx_key = securesystemslib.keys.generate_spx_key() | ||
|
|
||
| if not filepath: | ||
| filepath = os.path.join(os.getcwd(), spx_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 caller does not provide a password argument, prompt for one. | ||
| if password is None: # pragma: no cover | ||
|
|
||
| # It is safe to specify the full path of 'filepath' in the prompt and not | ||
| # worry about leaking sensitive information about the key's location. | ||
| # However, care should be taken when including the full path in exceptions | ||
| # and log files. | ||
| password = get_password('Enter a password for the SPX' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multi-line strings like this are kind of confusing to mentally parse when combined with string concatenation. Is there maybe a more compact way to write these? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This addition is an artifact of code duplication, which seems out of scope for this pull request. |
||
| ' key (' + Fore.RED + filepath + Fore.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) | ||
|
|
||
| # Create a temporary file, write the contents of the public key, and move | ||
| # to final destination. | ||
| file_object = securesystemslib.util.TempFile() | ||
|
|
||
| # Generate the spx public key file contents in metadata format (i.e., | ||
| # does not include the keyid portion). | ||
| keytype = spx_key['keytype'] | ||
| keyval = spx_key['keyval'] | ||
| scheme = spx_key['scheme'] | ||
| spxkey_metadata_format = securesystemslib.keys.format_keyval_to_metadata( | ||
| keytype, scheme, keyval, private=False) | ||
|
|
||
| file_object.write(json.dumps(spxkey_metadata_format).encode('utf-8')) | ||
|
|
||
| # Write the public key (i.e., 'public', which is in PEM format) to | ||
| # '<filepath>.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. | ||
| file_object.move(filepath + '.pub') | ||
|
|
||
| # Write the encrypted key string, conformant to | ||
| # 'securesystemslib.formats.ENCRYPTEDKEY_SCHEMA', to '<filepath>'. | ||
| file_object = securesystemslib.util.TempFile() | ||
|
|
||
| # Encrypt the private key if 'password' is set. | ||
| if len(password): | ||
| spx_key = securesystemslib.keys.encrypt_key(spx_key, password) | ||
|
|
||
| else: | ||
| logger.debug('An empty password was given. ' | ||
| 'Not encrypting the private key.') | ||
| spx_key = json.dumps(spx_key) | ||
|
|
||
| # Raise 'securesystemslib.exceptions.CryptoError' if 'spx_key' cannot be | ||
| # encrypted. | ||
| file_object.write(spx_key.encode('utf-8')) | ||
| file_object.move(filepath) | ||
|
|
||
| return filepath | ||
|
|
||
|
|
||
|
|
||
|
|
||
| def import_spx_publickey_from_file(filepath): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comment requiring no change to this PR: this function can be generalized across all keytypes. |
||
| """ | ||
| <Purpose> | ||
| Load the SPX public key object (conformant to | ||
| 'securesystemslib.formats.KEY_SCHEMA') stored in 'filepath'. Return | ||
| 'filepath' in securesystemslib.formats.SPXKEY_SCHEMA format. | ||
|
|
||
| If the key object in 'filepath' contains a private key, it is discarded. | ||
|
|
||
| <Arguments> | ||
| filepath: | ||
| <filepath>.pub file, a public key file. | ||
|
|
||
| <Exceptions> | ||
| securesystemslib.exceptions.FormatError, if 'filepath' is improperly | ||
| formatted or is an unexpected key type. | ||
|
|
||
| <Side Effects> | ||
| The contents of 'filepath' is read and saved. | ||
|
|
||
| <Returns> | ||
| An SPX key object conformant to | ||
| 'securesystemslib.formats.SPXKEY_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) | ||
|
|
||
| # SPX key objects are saved in json and metadata format. Return the | ||
| # loaded key object in securesystemslib.formats.SPXKEY_SCHEMA' format that | ||
| # also includes the keyid. | ||
| spx_key_metadata = securesystemslib.util.load_json_file(filepath) | ||
| spx_key, junk = \ | ||
| securesystemslib.keys.format_metadata_to_key(spx_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 'spx_key_metadata'. | ||
| if spx_key['keytype'] != 'spx': # pragma: no cover | ||
| message = 'Invalid key type loaded: ' + repr(spx_key['keytype']) | ||
| raise securesystemslib.exceptions.FormatError(message) | ||
|
|
||
| return spx_key | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| def import_spx_privatekey_from_file(filepath, password=None, prompt=False): | ||
| """ | ||
| <Purpose> | ||
| Import the encrypted spx key file in 'filepath', decrypt it, and return | ||
| the key object in 'securesystemslib.formats.SPXKEY_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. | ||
|
|
||
| <Arguments> | ||
| filepath: | ||
| <filepath> file, an RSA encrypted key file. | ||
|
|
||
| password: | ||
| The password, or passphrase, to import the private key (i.e., the | ||
| encrypted key file 'filepath' must be decrypted before the spx key | ||
| object can be returned. | ||
|
|
||
| prompt: | ||
| If True the user is prompted for a passphrase to decrypt 'filepath'. | ||
| Default is False. | ||
|
|
||
| <Exceptions> | ||
| securesystemslib.exceptions.FormatError, if the arguments are improperly | ||
| formatted or the imported key object contains an invalid key type (i.e., | ||
| not 'spx'). | ||
|
|
||
| securesystemslib.exceptions.CryptoError, if 'filepath' cannot be decrypted. | ||
|
|
||
| <Side Effects> | ||
| 'password' is used to decrypt the 'filepath' key file. | ||
|
|
||
| <Returns> | ||
| An spx key object of the form: | ||
| 'securesystemslib.formats.SPXKEY_SCHEMA'. | ||
| """ | ||
|
|
||
| # Does 'filepath' have the correct format? | ||
| # Ensure the arguments have the appropriate number of objects and object | ||
| # types, and that all dict keys are properly named. | ||
| # Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch. | ||
| securesystemslib.formats.PATH_SCHEMA.check_match(filepath) | ||
|
|
||
| if password and prompt: | ||
| raise ValueError("Passing 'password' and 'prompt' True is not allowed.") | ||
|
|
||
| # If 'password' was passed check format and that it is not empty. | ||
| if password is not None: | ||
| securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) | ||
|
|
||
| # TODO: PASSWORD_SCHEMA should be securesystemslib.schema.AnyString(min=1) | ||
| if not len(password): | ||
| raise ValueError('Password must be 1 or more characters') | ||
|
|
||
| elif prompt: | ||
| # Password confirmation disabled here, which should ideally happen only | ||
| # when creating encrypted key files (i.e., improve usability). | ||
| # It is safe to specify the full path of 'filepath' in the prompt and not | ||
| # worry about leaking sensitive information about the key's location. | ||
| # However, care should be taken when including the full path in exceptions | ||
| # and log files. | ||
| # NOTE: A user who gets prompted for a password, can only signal that the | ||
| # key is not encrypted by entering no password in the prompt, as opposed | ||
| # to a programmer who can call the function with or without a 'password'. | ||
| # Hence, we treat an empty password here, as if no 'password' was passed. | ||
| password = get_password('Enter a password for an encrypted SPX' | ||
| ' file \'' + Fore.RED + filepath + Fore.RESET + '\': ', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason you chose single quotes for these literals instead of using double quotes such as in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This addition is an artifact of code duplication, which seems out of scope for this pull request. |
||
| confirm=False) | ||
|
|
||
| # If user sets an empty string for the password, explicitly set the | ||
| # password to None, because some functions may expect this later. | ||
| if len(password) == 0: # pragma: no cover | ||
| password = None | ||
|
|
||
| # Finally, regardless of password, try decrypting the key, if necessary. | ||
| # Otherwise, load it straight from the disk. | ||
| with open(filepath, 'rb') as file_object: | ||
| json_str = file_object.read() | ||
| return securesystemslib.keys.\ | ||
| import_spxkey_from_private_json(json_str, password=password) | ||
|
|
||
|
|
||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| # The interactive sessions of the documentation strings can | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sidenote (doesn't have to be done for this PR): much of the code in this function is duplicated in multiple places:
generate_and_write_..._keypair. This makes it clear that we should modularize the generate functions and do things like argument checking, password prompting and format validation, temp file creation, writing, etc. in one place used by all thegenerate_...functions.The same goes for
import_..._privatekey_....