Skip to content
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

sphincs+ support, for post-quantum crypto #169

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6766f12
Added SPX test cases (copy-pasted from ed25510 test cases)
cryptojedi Oct 31, 2018
caa2ca9
Added support for SPX signatures, still completely untested
cryptojedi Oct 31, 2018
4b30a78
Changed from PyNaCl API to simpler (byte-string based) pyspx API
cryptojedi Oct 31, 2018
59b33e6
Added required spx file
cryptojedi Nov 1, 2018
b49d6dc
Fix import of specific SPX instance
joostrijneveld Nov 8, 2018
641fb65
Remove nacl-specific Exception for PySPX
joostrijneveld Nov 8, 2018
451d8cc
Add PySPX as a requirement
joostrijneveld Nov 8, 2018
5eb3cda
Fix tests for SPHINCS+
joostrijneveld Nov 8, 2018
0f56fbe
Fix missing exception handling for invalid schemes
joostrijneveld Nov 12, 2018
ea6c942
Clarify silenced Exception, mark for code coverage
joostrijneveld Nov 12, 2018
7f52678
Fixed merge conflict
cryptojedi Nov 13, 2018
d653691
Fix small copypasting errors
joostrijneveld Nov 29, 2018
9beace8
Restructure pyspx conditional import
lukpueh Jun 17, 2019
5458f62
Update pinned spx dependency in dev requirements
lukpueh Jun 17, 2019
154bee1
Add latest pyspx as optional dependency
lukpueh Jun 17, 2019
335199d
Add Sphincs+/PySPX infos to README
lukpueh Jun 17, 2019
a67a6b3
Remove pubkey arg from spx_keys.create_signature
lukpueh Jun 17, 2019
44db3df
Add minor style/clean-up fixes in spx related code
lukpueh Jun 17, 2019
2c453c1
Fix error message in spx create_signature
lukpueh Jun 18, 2019
b0735d5
Adopt data encoding in spx sig creation function
lukpueh Jun 18, 2019
beab2a1
Fix spx_keys.verify_signature doctest
lukpueh Jul 5, 2019
f6e47b9
Remove duplicate doctest lines
lukpueh Jul 5, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ securesystemslib supports public-key and general-purpose cryptography, such as
`ECDSA
<https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm>`_,
`Ed25519 <http://ed25519.cr.yp.to/>`_, `RSA
<https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29>`_, SHA256, SHA512, etc.
Most of the cryptographic operations are performed by the `cryptography
<https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29>`_,
`SPHINCS+-shake256-192s <https://sphincs.org/>`_, SHA256, SHA512, etc. Most of
the cryptographic operations are performed by the `cryptography
<https://cryptography.io/en/latest/>`_ and `PyNaCl
<https://github.com/pyca/pynacl>`_ libraries, but verification of Ed25519
signatures can be done in pure Python.
signatures can be done in pure Python. The `PySPX
<https://github.com/sphincs/pyspx>`_ Python bindings are used for SPHINCS+
signatures.


The `cryptography` library is used to generate keys and signatures with the
ECDSA and RSA algorithms, and perform general-purpose cryptography such as
Expand All @@ -46,12 +50,14 @@ Installation

The default installation only supports Ed25519 keys and signatures (in pure
Python). Support for RSA, ECDSA, and E25519 via the `cryptography` and
`PyNaCl` libraries is available by installing the `crypto` and `pynacl` extras:
`PyNaCl` libraries is available by installing the `crypto` and `pynacl` extras.
Support for SPHINCS+ signatures is available via the `pyspx` extra.

::

$ pip install securesystemslib[crypto]
$ pip install securesystemslib[pynacl]
$ pip install securesystemslib[pyspx]


Create RSA Keys
Expand Down
1 change: 1 addition & 0 deletions ci-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
cryptography
pynacl
pyspx
tox
coverage
coveralls
Expand Down
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
cryptography==2.3.1
pynacl==1.2.1
pyspx==0.4.0
six==1.11.0
tox==3.2.1
coveralls==1.3.0
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
cryptography
pynacl
pyspx
six
colorama
6 changes: 0 additions & 6 deletions securesystemslib/ed25519_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,6 @@ def create_signature(public_key, private_key, data, scheme):
True
>>> scheme == 'ed25519'
True
>>> signature, scheme = \
create_signature(public, private, data, scheme)
>>> securesystemslib.formats.ED25519SIGNATURE_SCHEMA.matches(signature)
True
>>> scheme == 'ed25519'
True

<Arguments>
public:
Expand Down
19 changes: 16 additions & 3 deletions securesystemslib/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
# Supported TUF key types.
KEYTYPE_SCHEMA = SCHEMA.OneOf(
[SCHEMA.String('rsa'), SCHEMA.String('ed25519'),
SCHEMA.String('ecdsa-sha2-nistp256')])
SCHEMA.String('ecdsa-sha2-nistp256'), SCHEMA.String('spx')])

# A generic TUF key. All TUF keys should be saved to metadata files in this
# format.
Expand All @@ -249,7 +249,7 @@
expires = SCHEMA.Optional(ISO8601_DATETIME_SCHEMA))

# A TUF key object. This schema simplifies validation of keys that may be one
# of the supported key types. Supported key types: 'rsa', 'ed25519'.
# of the supported key types. Supported key types: 'rsa', 'ed25519', 'spx'.
ANYKEY_SCHEMA = SCHEMA.Object(
object_name = 'ANYKEY_SCHEMA',
keytype = KEYTYPE_SCHEMA,
Expand Down Expand Up @@ -299,7 +299,7 @@
# cryptography modules.
REQUIRED_LIBRARIES_SCHEMA = SCHEMA.ListOf(SCHEMA.OneOf(
[SCHEMA.String('general'), SCHEMA.String('ed25519'), SCHEMA.String('rsa'),
SCHEMA.String('ecdsa-sha2-nistp256')]))
SCHEMA.String('ecdsa-sha2-nistp256'), SCHEMA.String('spx')]))

# Ed25519 signature schemes. The vanilla Ed25519 signature scheme is currently
# supported.
Expand All @@ -314,6 +314,19 @@
keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA),
keyval = KEYVAL_SCHEMA)

# SPX signature schemes. The vanilla SPX signature scheme is currently supported
SPX_SIG_SCHEMA = SCHEMA.OneOf([SCHEMA.String('spx')])

# An SPX TUF key.
SPXKEY_SCHEMA = SCHEMA.Object(
object_name = 'SPXKEY_SCHEMA',
keytype = SCHEMA.String('spx'),
scheme = SPX_SIG_SCHEMA,
keyid = KEYID_SCHEMA,
keyid_hash_algorithms = SCHEMA.Optional(HASHALGORITHMS_SCHEMA),
keyval = KEYVAL_SCHEMA)


# Information about target files, like file length and file hash(es). This
# schema allows the storage of multiple hashes for the same file (e.g., sha256
# and sha512 may be computed for the same file and stored).
Expand Down
257 changes: 255 additions & 2 deletions securesystemslib/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']



Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -900,6 +900,259 @@ 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a check to see if a provided filepath is a directory?

And it could either be a warning or it could accept the directory and change the path to directory/keyid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I created an issue for this. Feel free to comment there. #172

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'
' 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):
"""
<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_data = securesystemslib.util.load_json_file(filepath)
spx_key, junk = securesystemslib.keys.format_metadata_to_key(spx_key_data)

# 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_data'.
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 + '\': ',
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
Expand Down
Loading