diff --git a/docs/CLI.md b/docs/CLI.md index a1a9ac03..e10dad1f 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -80,10 +80,10 @@ $ repo.py --key --path --pw [my_password], --filen ## Sign metadata ## -Sign, using the specified key argument, the metadata of the role indicated by ---role. If no key argument or --role is given, the Targets role or its key is -used. The Snapshot and Timestamp role are also automatically signed, if -possible. +Sign, using the specified key, the metadata of the role indicated by --role +(must be Targets or a delegated role). If no key argument or --role is given, +the Targets role or its key is used. The Snapshot and Timestamp role are also +automatically signed, if possible. ```Bash $ repo.py --sign $ repo.py --sign @@ -97,8 +97,7 @@ $ repo.py --sign /path/to/timestamp_key --role timestamp ``` Note: In the future, the user might have the option of disabling automatic -signing of Snapshot and Timestamp metadata. Only ECDSA keys are -presently supported with `--sign`, but other key types will be added. +signing of Snapshot and Timestamp metadata. diff --git a/tuf/scripts/repo.py b/tuf/scripts/repo.py index 31ed6d47..236d0b5d 100755 --- a/tuf/scripts/repo.py +++ b/tuf/scripts/repo.py @@ -59,6 +59,7 @@ import tuf.repository_tool as repo_tool import securesystemslib +from colorama import Fore # See 'log.py' to learn how logging is handled in TUF. logger = logging.getLogger('tuf.scripts.repo') @@ -79,7 +80,7 @@ STAGED_METADATA_DIR = 'metadata.staged' METADATA_DIR = 'metadata' - +SUPPORTED_KEY_TYPES = ['ed25519', 'ecdsa-sha2-nistp256', 'rsa'] def process_arguments(parsed_arguments): """ @@ -159,7 +160,7 @@ def delegate(parsed_arguments): parsed_arguments.terminating, list_of_targets=None, path_hash_prefixes=None) - targets_private = repo_tool.import_ecdsa_privatekey_from_file( + targets_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME), parsed_arguments.pw) @@ -174,18 +175,17 @@ def delegate(parsed_arguments): parsed_arguments.terminating, list_of_targets=None, path_hash_prefixes=None) - role_privatekey = repo_tool.import_ecdsa_privatekey_from_file( - parsed_arguments.sign) + role_privatekey = import_privatekey_from_file(parsed_arguments.sign) repository.targets(parsed_arguments.role).load_signing_key(role_privatekey) # Update the required top-level roles, Snapshot and Timestamp, to make a new # release. - snapshot_private = repo_tool.import_ecdsa_privatekey_from_file( + snapshot_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME), parsed_arguments.pw) - timestamp_private = repo_tool.import_ecdsa_privatekey_from_file( + timestamp_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME), parsed_arguments.pw) @@ -207,7 +207,7 @@ def revoke(parsed_arguments): if parsed_arguments.role == 'targets': repository.targets.revoke(parsed_arguments.delegatee) - targets_private = repo_tool.import_ecdsa_privatekey_from_file( + targets_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME), parsed_arguments.pw) @@ -218,17 +218,16 @@ def revoke(parsed_arguments): else: repository.targets(parsed_arguments.role).revoke(parsed_arguments.delegatee) - role_privatekey = repo_tool.import_ecdsa_privatekey_from_file( - parsed_arguments.sign) + role_privatekey = import_privatekey_from_file(parsed_arguments.sign) repository.targets(parsed_arguments.role).load_signing_key(role_privatekey) # Update the required top-level roles, Snapshot and Timestamp, to make a new # release. - snapshot_private = repo_tool.import_ecdsa_privatekey_from_file( + snapshot_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME), parsed_arguments.pw) - timestamp_private = repo_tool.import_ecdsa_privatekey_from_file( + timestamp_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME), parsed_arguments.pw) @@ -279,6 +278,55 @@ def gen_key(parsed_arguments): +def import_privatekey_from_file(keypath, password=None): + # Note: should securesystemslib support this functionality (import any + # privatekey type)? + # If the caller does not provide a password argument, prompt for one. + # Password confirmation is disabled here, which should ideally happen only + # when creating encrypted key files. + 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 = securesystemslib.interface.get_password('Enter a password for' + ' the encrypted key (' + Fore.RED + keypath + Fore.RESET + '): ', + confirm=False) + + # Does 'password' have the correct format? + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + + # Store the encrypted contents of 'filepath' prior to calling the decryption + # routine. + encrypted_key = None + + with open(keypath, 'rb') as file_object: + encrypted_key = file_object.read() + + # 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. + try: + key_object = securesystemslib.keys.decrypt_key(encrypted_key.decode('utf-8'), + password) + + except securesystemslib.exceptions.CryptoError: + key_object = securesystemslib.keys.import_rsakey_from_private_pem( + encrypted_key, 'rsassa-pss-sha256', password) + + if key_object['keytype'] not in SUPPORTED_KEY_TYPES: + raise tuf.exceptions.Error('Trying to import an unsupported key' + ' type: ' + repr(key_object['keytype'] + '.' + ' Supported key types: ' + repr(SUPPORTED_KEY_TYPES))) + + else: + # Add "keyid_hash_algorithms" so that equal keys with different keyids can + # be associated using supported keyid_hash_algorithms. + key_object['keyid_hash_algorithms'] = securesystemslib.settings.HASH_ALGORITHMS + + return key_object + def sign_role(parsed_arguments): @@ -289,11 +337,10 @@ def sign_role(parsed_arguments): # Was a private key path given with --sign? If so, load the specified # private key. Otherwise, load the default key path. if parsed_arguments.sign != '.': - role_privatekey = repo_tool.import_ecdsa_privatekey_from_file( - parsed_arguments.sign) + role_privatekey = import_privatekey_from_file(parsed_arguments.sign) else: - role_privatekey = repo_tool.import_ecdsa_privatekey_from_file( + role_privatekey = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME), parsed_arguments.pw) @@ -308,10 +355,10 @@ def sign_role(parsed_arguments): # Update the required top-level roles, Snapshot and Timestamp, to make a new # release. - snapshot_private = repo_tool.import_ecdsa_privatekey_from_file( + snapshot_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME), parsed_arguments.pw) - timestamp_private = repo_tool.import_ecdsa_privatekey_from_file( + timestamp_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME), parsed_arguments.pw) @@ -393,13 +440,13 @@ def add_targets(parsed_arguments): prompt='Enter a password for the top-level role keys: ', confirm=True) # Load the top-level, non-root, keys to make a new release. - targets_private = repo_tool.import_ecdsa_privatekey_from_file( + targets_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME), parsed_arguments.pw) - snapshot_private = repo_tool.import_ecdsa_privatekey_from_file( + snapshot_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME), parsed_arguments.pw) - timestamp_private = repo_tool.import_ecdsa_privatekey_from_file( + timestamp_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME), parsed_arguments.pw) @@ -491,16 +538,16 @@ def set_top_level_keys(repository): # Import the private keys. They are needed to generate the signatures # included in metadata. - root_private = repo_tool.import_ecdsa_privatekey_from_file( + root_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, ROOT_KEY_NAME), parsed_arguments.pw) - targets_private = repo_tool.import_ecdsa_privatekey_from_file( + targets_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TARGETS_KEY_NAME), parsed_arguments.pw) - snapshot_private = repo_tool.import_ecdsa_privatekey_from_file( + snapshot_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, SNAPSHOT_KEY_NAME), parsed_arguments.pw) - timestamp_private = repo_tool.import_ecdsa_privatekey_from_file( + timestamp_private = import_privatekey_from_file( os.path.join(parsed_arguments.path, KEYSTORE_DIR, TIMESTAMP_KEY_NAME), parsed_arguments.pw)