Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.2.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
36 changes: 33 additions & 3 deletions securesystemslib/formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@
import time
import six

# Try to import pyspx to get access to lengths of SPX signatures and keys
try:
import pyspx.shake256_192s as pyspx

# pyspx's 'cffi' dependency may raise an 'IOError' exception when importing
except (ImportError, IOError): # pragma: no cover
pass

import securesystemslib.schema as SCHEMA
import securesystemslib.exceptions

Expand Down Expand Up @@ -227,7 +235,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 +257,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 @@ -292,14 +300,23 @@
# An ED25519 raw signature, which must be 64 bytes.
ED25519SIGNATURE_SCHEMA = SCHEMA.LengthBytes(64)

# Lengths of SPX raw keys and signatures
try:
SPXPUBLIC_SCHEMA = SCHEMA.LengthBytes(pyspx.crypto_sign_PUBLICKEYBYTES)
SPXSEED_SCHEMA = SCHEMA.LengthBytes(pyspx.crypto_sign_SECRETKEYBYTES)
SPXSIGNATURE_SCHEMA = SCHEMA.LengthBytes(pyspx.crypto_sign_BYTES)

except NameError: # pragma: no cover
pass # raised when pyspx was not available on import

# An ECDSA signature.
ECDSASIGNATURE_SCHEMA = SCHEMA.AnyBytes()

# Required installation libraries expected by the repository tools and other
# 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 +331,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
258 changes: 256 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,260 @@ def import_ecdsa_privatekey_from_file(filepath, password=None):
return key_object


def generate_and_write_spx_keypair(filepath=None, password=None):
Copy link
Contributor Author

@awwad awwad Nov 20, 2018

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 the generate_... functions.

The same goes for import_..._privatekey_....

"""
<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'
Copy link

Choose a reason for hiding this comment

The 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?

Choose a reason for hiding this comment

The 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):
Copy link
Contributor Author

@awwad awwad Nov 20, 2018

Choose a reason for hiding this comment

The 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 + '\': ',
Copy link

Choose a reason for hiding this comment

The 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 " file '" + ... vs. 'file \''?

Choose a reason for hiding this comment

The 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
Expand Down
Loading