diff --git a/CHANGELOG.md b/CHANGELOG.md index 841331d1..b4109158 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,13 +13,17 @@ and this project adheres to [Semantic Versioning][semver]. ### Changed +- Docstirngs logging improvements ([325]) - Keystore path in roles_key_info calculated relative to where the json file is ([321]) - Try to sign using a yubikey before asking the user if they want to use a yubikey ([320]) - Split `developer_tool` into separate modules ([314], [321]) ### Fixed +- Fix create repository ([325]) + +[325]: https://github.com/openlawlibrary/taf/pull/325 [321]: https://github.com/openlawlibrary/taf/pull/321 [320]: https://github.com/openlawlibrary/taf/pull/320 [314]: https://github.com/openlawlibrary/taf/pull/314 diff --git a/setup.py b/setup.py index 33d30138..53f3a6c5 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ def finalize_options(self): "pygit2==1.9.*", "pyOpenSSL==22.1.*", "cattrs==1.*", + "logdecorator==2.*", ], "extras_require": { "ci": ci_require, diff --git a/taf/api/keystore.py b/taf/api/keystore.py index 29c6c538..a0036ac4 100644 --- a/taf/api/keystore.py +++ b/taf/api/keystore.py @@ -1,3 +1,5 @@ +from logging import DEBUG, INFO +from logdecorator import log_on_start, log_on_end from pathlib import Path from tuf.repository_tool import ( generate_and_write_rsa_keypair, @@ -7,25 +9,64 @@ from taf.api.roles import _initialize_roles_and_keystore from taf.constants import DEFAULT_ROLE_SETUP_PARAMS from taf.keys import get_key_name +from taf.log import taf_logger + + +@log_on_start(DEBUG, "Generating '{key_path:s}'", logger=taf_logger) +@log_on_end(INFO, "Generated '{key_path:s}", logger=taf_logger) +def _generate_rsa_key(key_path, password, bits=None): + """ + Generate public and private key + + Arguments: + key_path (optional): The path to write the private key to. + password (optional): An encryption password. + bits (optional): The number of bits of the generated RSA key. + + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: Arguments are malformed. + StorageError: Key files cannot be written. + + Side Effects: + Writes key files to disk. + Overwrites files if they already exist. + + Returns: + None + """ + if password: + generate_and_write_rsa_keypair(filepath=key_path, bits=bits, password=password) + else: + generate_and_write_unencrypted_rsa_keypair(filepath=key_path, bits=bits) def generate_keys(keystore, roles_key_infos, delegated_roles_key_infos=None): """ - - Generate public and private keys and writes them to disk. Names of keys correspond to names - of the TUF roles. If more than one key should be generated per role, a counter is appended - to the role's name. E.g. root1, root2, root3 etc. - - keystore: - Location where the generated files should be saved - roles_key_infos: - A dictionary whose keys are role names, while values contain information about the keys. - This includes: - - passwords of the keystore files - - number of keys per role (optional, defaults to one if not provided) - - key length (optional, defaults to TUF's default value, which is 3072) - Names of the keys are set to names of the roles plus a counter, if more than one key - should be generated. + Generate public and private keys and writes them to disk. Names of keys correspond to names + of TUF roles. If more than one key should be generated per role, a counter is appended + to the role's name. E.g. root1, root2, root3 etc. + + Arguments: + keystore: Location where the generated files should be saved + roles_key_infos: A dictionary whose keys are role names, while values contain information about the keys. + This includes: + - passwords of the keystore files + - number of keys per role (optional, defaults to one if not provided) + - key length (optional, defaults to TUF's default value, which is 3072) + Names of the keys are set to names of the roles plus a counter, if more than one key + should be generated. + Raises: + UnsupportedLibraryError: pyca/cryptography is not available. + FormatError: One or more keys not properly specified + StorageError: Key files cannot be written. + + Side Effects: + Writes key files to disk. + Overwrites files if they already exist. + + Returns: + None """ if delegated_roles_key_infos is not None: roles_key_infos = delegated_roles_key_infos @@ -43,15 +84,9 @@ def generate_keys(keystore, roles_key_infos, delegated_roles_key_infos=None): for key_num in range(num_of_keys): if not is_yubikey: key_name = get_key_name(role_name, key_num, num_of_keys) + key_path = str(Path(keystore, key_name)) password = passwords[key_num] - path = str(Path(keystore, key_name)) - print(f"Generating {path}") - if password: - generate_and_write_rsa_keypair( - filepath=path, bits=bits, password=password - ) - else: - generate_and_write_unencrypted_rsa_keypair(filepath=path, bits=bits) + _generate_rsa_key(key_path, password, bits) if key_info.get("delegations"): delegations_info = {"roles": key_info["delegations"]} generate_keys(keystore, roles_key_infos, delegations_info) diff --git a/taf/api/metadata.py b/taf/api/metadata.py index f2da2d54..f454df1b 100644 --- a/taf/api/metadata.py +++ b/taf/api/metadata.py @@ -1,28 +1,33 @@ import datetime +from logging import ERROR, INFO from pathlib import Path +from logdecorator import log_on_end, log_on_error from taf.exceptions import TargetsMetadataUpdateError from taf.git import GitRepository from taf.keys import load_signing_keys from taf.constants import DEFAULT_RSA_SIGNATURE_SCHEME from taf.repository_tool import Repository, is_delegated_role +from taf.log import taf_logger def check_expiration_dates( repo_path, interval=None, start_date=None, excluded_roles=None ): """ - - Check if any metadata files (roles) are expired or will expire in the next days. - Prints a list of expired roles. - - repo_path: - Authentication repository's location - interval: - Number of days ahead to check for expiration - start_date: - Date from which to start checking for expiration - excluded_roles: - List of roles to exclude from the check + Check if any metadata files (roles) are expired or will expire in the next days. + Prints a list of expired roles. + + Arguments: + repo_path: Authentication repository's location. + interval: Number of days ahead to check for expiration. + start_date: Date from which to start checking for expiration. + excluded_roles: List of roles to exclude from the check. + + Side Effects: + Prints lists of roles that expired or are about to expire. + + Returns: + None """ repo_path = Path(repo_path) taf_repo = Repository(repo_path) @@ -59,6 +64,29 @@ def update_metadata_expiration_date( start_date=None, no_commit=False, ): + """ + Update expiration dates of the specified roles and all other roles that need + to be signed in order to guarantee validity of the repository e.g. snapshot + and timestamp need to be signed after a targets role is updated. + + Arguments: + repo_path: Authentication repository's location. + roles: A list of roles whose expiration dates should be updated. + interval: Number of days added to the start date in order to calculate the + expiration date. + keystore (optional): Keystore directory's path + scheme (optional): Signature scheme. + start_date (optional): Date to which expiration interval is added. + Set to today if not specified. + no_commit (optional): Prevents automatic commit if set to True + + Side Effects: + Updates metadata files, saves changes to disk and commits changes + unless no_commit is set to True. + + Returns: + None + """ if start_date is None: start_date = datetime.datetime.now() @@ -80,27 +108,11 @@ def update_metadata_expiration_date( for role in roles_to_update: try: - keys, yubikeys = load_signing_keys( - taf_repo, - role, - loaded_yubikeys=loaded_yubikeys, - keystore=keystore, - scheme=scheme, + _update_expiration_date_of_role( + taf_repo, role, loaded_yubikeys, keystore, start_date, interval, scheme ) - # sign with keystore - if len(keys): - taf_repo.update_role_keystores( - role, keys, start_date=start_date, interval=interval - ) - if len(yubikeys): # sign with yubikey - taf_repo.update_role_yubikeys( - role, yubikeys, start_date=start_date, interval=interval - ) - except Exception as e: - print(f"Could not update expiration date of {role}. {str(e)}") + except Exception: return - else: - print(f"Updated expiration date of {role}") if no_commit: print("\nNo commit was set. Please commit manually. \n") @@ -110,9 +122,53 @@ def update_metadata_expiration_date( auth_repo.commit(commit_message) +@log_on_end(INFO, "Updated expiration date of {role:s}", logger=taf_logger) +@log_on_error( + ERROR, + "Could not update expiration date of {role:s} {e!r}", + logger=taf_logger, + reraise=True, +) +def _update_expiration_date_of_role( + taf_repo, role, loaded_yubikeys, keystore, start_date, interval, scheme +): + keys, yubikeys = load_signing_keys( + taf_repo, + role, + loaded_yubikeys=loaded_yubikeys, + keystore=keystore, + scheme=scheme, + ) + # sign with keystore + if len(keys): + taf_repo.update_role_keystores( + role, keys, start_date=start_date, interval=interval + ) + if len(yubikeys): # sign with yubikey + taf_repo.update_role_yubikeys( + role, yubikeys, start_date=start_date, interval=interval + ) + + def update_snapshot_and_timestamp( - taf_repo, keystore, roles_infos, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, write_all=True + taf_repo, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, write_all=True ): + """ + Sign snapshot and timestamp metadata files. + + Arguments: + taf_repo: Authentication repository. + keystore: Keystore directory's path. + scheme (optional): Signature scheme. + write_all (optional): If True, writes authentication repository's + changes to disk. + + Side Effects: + Updates metadata files, saves changes to disk if write_all is True + + Returns: + None + """ loaded_yubikeys = {} for role in ("snapshot", "timestamp"): @@ -135,13 +191,30 @@ def update_target_metadata( added_targets_data, removed_targets_data, keystore, - roles_infos, write=False, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, ): - """Update given targets data with an appropriate role, as well as snapshot and - timestamp roles. + """Given dictionaries containing targets that should be added and targets that should + be removed, update and sign target metadata files and, if write is True, also + sign snapshot and timestamp. + + Sing snapshot and timestamp metadata files + + Arguments: + taf_repo: Authentication repository. + added_targets_data(dict): Dictionary containing targets data that should be added. + removed_targets_data(dict): Dictionary containing targets data that should be removed. + keystore: Keystore directory's path. + write (optional): If True, updates snapshot and timestamp and write changes to disk. + scheme (optional): Signature scheme. + + Side Effects: + Updates metadata files, saves changes to disk if write_all is True + + Returns: + None """ + added_targets_data = {} if added_targets_data is None else added_targets_data removed_targets_data = {} if removed_targets_data is None else removed_targets_data @@ -181,4 +254,4 @@ def update_target_metadata( ) if write: - update_snapshot_and_timestamp(taf_repo, keystore, roles_infos, scheme=scheme) + update_snapshot_and_timestamp(taf_repo, keystore, scheme=scheme) diff --git a/taf/api/repository.py b/taf/api/repository.py index d89deb9d..c1ae91a0 100644 --- a/taf/api/repository.py +++ b/taf/api/repository.py @@ -1,5 +1,7 @@ from functools import partial +from logging import INFO import click +from logdecorator import log_on_end, log_on_start from collections import defaultdict from pathlib import Path @@ -12,28 +14,33 @@ from taf.keys import get_key_name, load_sorted_keys_of_new_roles from taf.repository_tool import Repository, yubikey_signature_provider from tuf.repository_tool import create_new_repository +from taf.log import taf_logger +@log_on_start( + INFO, "Creating a new authentication repository {repo_path:s}", logger=taf_logger +) +@log_on_end(INFO, "Finished creating a new repository", logger=taf_logger) def create_repository( repo_path, keystore=None, roles_key_infos=None, commit=False, test=False ): """ - - Create a new authentication repository. Generate initial metadata files. - The initial targets metadata file is empty (does not specify any targets). - - repo_path: - Authentication repository's location - targets_directory: - Directory which contains target repositories - keystore: - Location of the keystore files - roles_key_infos: - A dictionary whose keys are role names, while values contain information about the keys. - commit: - Indicates if the changes should be automatically committed - test: - Indicates if the created repository is a test authentication repository + Create a new authentication repository. Generate initial metadata files. + If target files already exist, add corresponding targets information to + targets metadata files. + + Arguments: + repo_path: Authentication repository's location. + keystore: Location of the keystore files. + roles_key_infos: A dictionary whose keys are role names, while values contain information about the keys. + commit: Specifies if the changes should be automatically committed. + test: Specifies if the created repository is a test authentication repository. + + Side Effects: + Creates a new authentication repository (initializes a new git repository and generates tuf metadata) + + Returns: + None """ yubikeys = defaultdict(dict) auth_repo = AuthenticationRepository(path=repo_path) @@ -49,7 +56,7 @@ def create_repository( repository = create_new_repository(str(auth_repo.path)) roles_infos = roles_key_infos.get("roles") signing_keys, verification_keys = load_sorted_keys_of_new_roles( - auth_repo, roles_infos, repository, keystore, yubikeys + auth_repo, roles_infos, keystore, yubikeys ) # set threshold and register keys of main roles # we cannot do the same for the delegated roles until delegations are created @@ -79,14 +86,17 @@ def create_repository( taf_repository = Repository(repo_path) taf_repository._tuf_repository = repository register_target_files( - repo_path, keystore, roles_key_infos, commit=commit, taf_repo=taf_repository + repo_path, + keystore, + roles_key_infos, + commit=False, + taf_repo=taf_repository, + write=True, ) except TargetsMetadataUpdateError: # if there are no target files repository.writeall() - print("Created new authentication repository") - if commit: auth_repo.init_repo() commit_message = input("\nEnter commit message and press ENTER\n\n") @@ -94,6 +104,21 @@ def create_repository( def _check_if_can_create_repository(auth_repo): + """ + Check if a new authentication repository can be created at the specified location. + A repository can be created if there is not directory at the repository's location + or if it does exists, is not the root of a git repository. + + Arguments: + auth_repo: Authentication repository. + + + Side Effects: + None + + Returns: + True if a new authentication repository can be created, False otherwise. + """ repo_path = Path(auth_repo.path) if repo_path.is_dir(): # check if there is non-empty metadata directory @@ -119,6 +144,25 @@ def _setup_role( signing_keys=None, parent=None, ): + """ + Set up a role, which can either be one of the main TUF roles, or a delegated role. + Define threshold and signing and verification keys of the role and link it with the repository. + + Arguments: + role_name: Role's name, either one of the main TUF roles or a delegated role. + threshold: Signatures threshold. + is_yubikey: Indicates if the role's metadata file should be signed using Yubikeys or not. + repository: TUF repository + verification_keys: Public keys used to verify the signatures + signing_keys (optional): Signing (private) keys, which only need to be specified if Yubikeys are not used + parent: The role's parent role + + Side Effects: + Adds a new role to the TUF repository and sets up its threshold and signing and verification keys + + Returns: + None + """ role_obj = _role_obj(role_name, repository, parent) role_obj.threshold = threshold if not is_yubikey: diff --git a/taf/api/roles.py b/taf/api/roles.py index 25b04e7c..72e89201 100644 --- a/taf/api/roles.py +++ b/taf/api/roles.py @@ -1,9 +1,11 @@ +from logging import DEBUG import os import click from collections import defaultdict from functools import partial import json from pathlib import Path +from logdecorator import log_on_end, log_on_start from taf.hosts import REPOSITORIES_JSON_PATH from tuf.repository_tool import TARGETS_DIRECTORY_NAME import tuf.roledb @@ -28,11 +30,14 @@ yubikey_signature_provider, ) from taf.utils import get_key_size, read_input_dict +from taf.log import taf_logger MAIN_ROLES = ["root", "snapshot", "timestamp", "targets"] +@log_on_start(DEBUG, "Adding a new role {role:s}", logger=taf_logger) +@log_on_end(DEBUG, "Finished adding a new role", logger=taf_logger) def add_role( auth_path: str, role: str, @@ -42,11 +47,33 @@ def add_role( threshold: int, yubikey: bool, keystore: str, - scheme: str, + scheme: str = DEFAULT_RSA_SIGNATURE_SCHEME, auth_repo: AuthenticationRepository = None, commit=True, ): - + """ + Add a new delegated target role and update and sign metadata files. + Automatically commit the changes if commit is set to True. + + Arguments: + auth_path: Path to the authentication repository. + role: Name of the role which is to be added. + parent_role: Name of the target role that is the new role's parent. Can be targets or another delegated role. + paths: A list of target paths that are delegated to the new role. + keys_number: Total number of signing keys + threshold: Signature's threshold. + yubikey: Specifies if the metadata file should be signed using a YubiKey. + keystore: Location of the keystore files. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function. + commit: Specifies if the changes should be automatically committed. + + Side Effects: + Initializes a new delegated targets role, signs metadata files, write changes to the disk and optionally commits. + + Returns: + None + """ yubikeys = defaultdict(dict) if auth_repo is None: auth_repo = AuthenticationRepository(path=auth_path) @@ -78,16 +105,37 @@ def add_role( _create_delegations( roles_infos, auth_repo, verification_keys, signing_keys, existing_roles ) - _update_role(auth_repo, parent_role, keystore, roles_infos, scheme=scheme) + _update_role(auth_repo, parent_role, keystore, scheme=scheme) if commit: - update_snapshot_and_timestamp(auth_repo, keystore, roles_infos, scheme=scheme) + update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) +@log_on_start(DEBUG, "Adding new paths to role {role:s}", logger=taf_logger) +@log_on_end(DEBUG, "Finished adding new paths to role", logger=taf_logger) def add_role_paths( paths, delegated_role, keystore, commit=True, auth_repo=None, auth_path=None ): + """ + Adds additional delegated target paths to the specified role. That means that + the role will be responsible for sining target files at those location going forward. + + Arguments: + paths: A list of additional target paths that should be delegated to the role. + delegated_role: Name of the targets role to which the new paths should be delegated. + auth_path: Path to the authentication repository. + keystore: Location of the keystore files. + auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function. + commit: Specified if the changes should be automatically committed. + + Side Effects: + Updates the specified target role's parent and other metadata files (snapshot and timestamp), + signs them, writes changes to disk and optionally commits everything. + + Returns: + None + """ if auth_repo is None: auth_repo = AuthenticationRepository(path=auth_path) parent_role = auth_repo.find_delegated_roles_parent(delegated_role) @@ -95,13 +143,15 @@ def add_role_paths( parent_role_obj.add_paths(paths, delegated_role) _update_role(auth_repo, parent_role, keystore) if commit: - update_snapshot_and_timestamp(auth_repo, keystore, None, None) + update_snapshot_and_timestamp(auth_repo, keystore) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) +@log_on_start(DEBUG, "Adding new roles", logger=taf_logger) +@log_on_end(DEBUG, "Finished adding new roles", logger=taf_logger) def add_roles( - repo_path, + auth_path, keystore=None, roles_key_infos=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, @@ -109,18 +159,37 @@ def add_roles( """ Add new target roles and sign all metadata files given information stored in roles_key_infos dictionary or .json file + + Arguments: + auth_path: Path to the authentication repository. + keystore (optional): Location of the keystore files. + roles_key_infos (optional): A dictionary containing information about the roles: + - total number of keys per role + - their parent roles + - threshold of signatures per role + - should keys of a role be on Yubikeys or should a keystore files be used + - scheme (the default scheme is rsa-pkcs1v15-sha256) + - keystore path, if not specified via keystore option + auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + + Side Effects: + Updates metadata files (parent of new roles, snapshot and timestamp) and creates new targets metadata files. + Writes changes to disk. + + Returns: + None """ yubikeys = defaultdict(dict) - auth_repo = AuthenticationRepository(path=repo_path) - repo_path = Path(repo_path) + auth_repo = AuthenticationRepository(path=auth_path) + auth_path = Path(auth_path) roles_key_infos, keystore = _initialize_roles_and_keystore( roles_key_infos, keystore ) new_roles = [] - taf_repo = Repository(repo_path) - existing_roles = taf_repo.get_all_targets_roles() + existing_roles = auth_repo.get_all_targets_roles() main_roles = ["root", "snapshot", "timestamp", "targets"] existing_roles.extend(main_roles) @@ -158,21 +227,23 @@ def add_roles( print("All roles already set up") return - repository = taf_repo._repository + repository = auth_repo._repository roles_infos = roles_key_infos.get("roles") signing_keys, verification_keys = load_sorted_keys_of_new_roles( - auth_repo, roles_infos, taf_repo, keystore, yubikeys, existing_roles + auth_repo, roles_infos, keystore, yubikeys, existing_roles ) _create_delegations( roles_infos, repository, verification_keys, signing_keys, existing_roles ) for parent_role in parent_roles: - _update_role(taf_repo, parent_role, keystore, scheme=scheme) - update_snapshot_and_timestamp(taf_repo, keystore, scheme=scheme) + _update_role(auth_repo, parent_role, keystore, scheme=scheme) + update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) +@log_on_start(DEBUG, "Adding new signing key to roles", logger=taf_logger) +@log_on_end(DEBUG, "Finished adding new signing key to roles", logger=taf_logger) def add_signing_key( - repo_path, + auth_path, roles, pub_key_path=None, keystore=None, @@ -180,14 +251,35 @@ def add_signing_key( scheme=DEFAULT_RSA_SIGNATURE_SCHEME, ): """ - Adds a new signing key to the listed roles. Automatically updates timestamp and - snapshot. + Add a new signing key to the listed roles. Update root metadata if one or more roles is one of the main TUF roles, + parent target role if one of the roles is a delegated target role and timestamp and snapshot in any case. + + Arguments: + auth_path: Path to the authentication repository. + roles: A list of roles whose signing keys need to be extended. + pub_key_path (optional): path to the file containing the public component of the new key. If not provided, + it will be necessary to ender the key when prompted. + keystore (optional): Location of the keystore files. + roles_key_infos (optional): A dictionary containing information about the roles: + - total number of keys per role + - their parent roles + - threshold of signatures per role + - should keys of a role be on Yubikeys or should a keystore files be used + - scheme (the default scheme is rsa-pkcs1v15-sha256) + - keystore path, if not specified via keystore option + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + + Side Effects: + Updates metadata files (parents of the affected roles, snapshot and timestamp). + Writes changes to disk. + + Returns: + None """ - taf_repo = Repository(repo_path) + taf_repo = Repository(auth_path) roles_key_infos, keystore = _initialize_roles_and_keystore( roles_key_infos, keystore, enter_info=False ) - roles_infos = roles_key_infos.get("roles") pub_key_pem = None if pub_key_path is not None: @@ -218,15 +310,33 @@ def add_signing_key( taf_repo.unmark_dirty_roles(list(set(roles) - parent_roles)) for parent_role in parent_roles: - _update_role(taf_repo, parent_role, keystore, roles_infos, scheme) + _update_role(taf_repo, parent_role, keystore, scheme=scheme) - update_snapshot_and_timestamp(taf_repo, keystore, roles_infos, scheme=scheme) + update_snapshot_and_timestamp(taf_repo, keystore, scheme=scheme) def _enter_roles_infos(keystore, roles_key_infos): """ Ask the user to enter information taf roles and keys, including the location of keystore directory if not entered through an input parameter + + Arguments: + keystore: Location of the keystore files. + roles_key_infos: A dictionary containing information about the roles: + - total number of keys per role + - their parent roles + - threshold of signatures per role + - should keys of a role be on Yubikeys or should a keystore files be used + - scheme (the default scheme is rsa-pkcs1v15-sha256) + - keystore path, if not specified via keystore option + + Side Effects: + None + + Returns: + A dictionary containing entered information about taf roles and keys (total number of keys per role, + parent roles of roles, threshold of signatures per role, indicator if metadata should be signed using + a yubikey for each role, key length and signing scheme for each role) """ mandatory_roles = ["root", "targets", "snapshot", "timestamp"] role_key_infos = defaultdict(dict) @@ -345,8 +455,28 @@ def _read_val(input_type, name, param=None, required=False): def _initialize_roles_and_keystore(roles_key_infos, keystore, enter_info=True): """ - Read or enter roles information and try to extract keystore path from - that json + Read information about roles and keys from a json file or ask the user to enter + this information if not specified through a json file enter_info is True + + Arguments: + roles_key_infos: A dictionary containing information about the roles: + - total number of keys per role + - their parent roles + - threshold of signatures per role + - should keys of a role be on Yubikeys or should a keystore files be used + - scheme (the default scheme is rsa-pkcs1v15-sha256) + - keystore path, if not specified via keystore option + keystore: Location of the keystore files. + enter_info (optional): Indicates if the user should be asked to enter information about the. + roles and keys if not specified. Set to True by default. + + Side Effects: + None + + Returns: + A dictionary containing entered information about taf roles and keys (total number of keys per role, + parent roles of roles, threshold of signatures per role, indicator if metadata should be signed using + a yubikey for each role, key length and signing scheme for each role) and keystore file path """ roles_key_infos_dict = read_input_dict(roles_key_infos) if keystore is None: @@ -374,6 +504,28 @@ def _initialize_roles_and_keystore(roles_key_infos, keystore, enter_info=True): def _create_delegations( roles_infos, repository, verification_keys, signing_keys, existing_roles=None ): + """ + Initialize new delegated target roles, update authentication repository object + + Arguments: + roles_infos: A dictionary containing information about the roles: + - total number of keys per role + - their parent roles + - threshold of signatures per role + - should keys of a role be on Yubikeys or should a keystore files be used + - scheme (the default scheme is rsa-pkcs1v15-sha256) + - keystore path, if not specified via keystore option + repository: Authentication repository. + verification_keys: A dictionary containing mappings of role names to their verification (public) keys. + signing_keys: A dictionary containing mappings of role names to their signing (private) keys. + existing_roles: A list of already initialized roles. + + Side Effects: + Updates authentication repository object + + Returns: + None + """ if existing_roles is None: existing_roles = [] for role_name, role_info in roles_infos.items(): @@ -420,7 +572,11 @@ def _get_roles_key_size(role, keystore, keys_num): def _role_obj(role, repository, parent=None): - repository = repository._tuf_repository + """ + Return role TUF object based on its name + """ + if isinstance(repository, Repository): + repository = repository._repository if role == "targets": return repository.targets elif role == "snapshot": @@ -436,6 +592,8 @@ def _role_obj(role, repository, parent=None): return parent(role) +@log_on_start(DEBUG, "Removing role {role:s}", logger=taf_logger) +@log_on_end(DEBUG, "Finished removing the role", logger=taf_logger) def remove_role( auth_path: str, role: str, @@ -445,6 +603,27 @@ def remove_role( remove_targets: bool = False, auth_repo: AuthenticationRepository = None, ): + """ + Remove a delegated target role and update and sign metadata files. + Automatically commit the changes if commit is set to True. + It is not possible to remove any of the main TUF roles + + Arguments: + auth_path: Path to the authentication repository. + role: Name of the role which is to be removed. + keystore: Location of the keystore files. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + commit (optional): Specifies if the changes should be automatically committed. Set to True by default + remove_targets (optional): Indicates if target files should be removed to, or signed by the parent role. + Set to False by default. + auth_repo (optional): Instance of the authentication repository. Will be created if not passed into the function. + + Side Effects: + Updates metadata files, optionally deletes target files, writes changes to disk and optionally commits. + + Returns: + None + """ if role in MAIN_ROLES: print( f"Cannot remove role {role}. It is one of the roles required by the TUF specification" @@ -486,7 +665,6 @@ def remove_role( added_targets_data, removed_targets_data, keystore, - roles_infos=None, write=False, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, ) @@ -504,13 +682,32 @@ def remove_role( json.dumps(repositories_json, indent=4) ) - update_snapshot_and_timestamp(auth_repo, keystore, None, scheme) + update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) if commit: commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) +@log_on_start(DEBUG, "Removing paths", logger=taf_logger) +@log_on_end(DEBUG, "Finished removing paths", logger=taf_logger) def remove_paths(paths, keystore, commit=True, auth_repo=None, auth_path=None): + """ + Remove delegated paths. Update parent roles of the roles associated with the removed paths, + as well as snapshot and timestamp. Optionally commit the changes. + + Arguments: + paths: Paths to be removed. + keystore: Location of the keystore files. + commit (optional): Specifies if the changes should be automatically committed. Set to True by default + auth_repo (optional): Instance of the authentication repository. Needs to be specified if auth_path is not. + auth_path: Path to the authentication repository. Needs to be specified if auth_repo is None. + + Side Effects: + Updates metadata files, writes changes to disk and optionally commits them. + + Returns: + None + """ if auth_repo is None: auth_repo = AuthenticationRepository(path=auth_path) for path in paths: @@ -521,7 +718,7 @@ def remove_paths(paths, keystore, commit=True, auth_repo=None, auth_path=None): _remove_path_from_role_info(path, parent_role, delegated_role, auth_repo) _update_role(auth_repo, parent_role, keystore) if commit: - update_snapshot_and_timestamp(auth_repo, keystore, None, None) + update_snapshot_and_timestamp(auth_repo, keystore) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) @@ -550,6 +747,9 @@ def _setup_role( signing_keys=None, parent=None, ): + """ + Initialize a new role, add signing and verification keys. + """ role_obj = _role_obj(role_name, repository, parent) role_obj.threshold = threshold if not is_yubikey: @@ -565,9 +765,12 @@ def _setup_role( ) -def _update_role( - taf_repo, role, keystore, roles_infos=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME -): +def _update_role(taf_repo, role, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME): + """ + Update the specified role's metadata's expiration date, load the signing keys + from either a keystore file or yubikey and sign the file without updating + snapshot and timestamp and writing changes to disk + """ keystore_keys, yubikeys = load_signing_keys(taf_repo, role, keystore, scheme=scheme) if len(keystore_keys): taf_repo.update_role_keystores(role, keystore_keys, write=False) diff --git a/taf/api/targets.py b/taf/api/targets.py index 9761dc1e..73fc99c3 100644 --- a/taf/api/targets.py +++ b/taf/api/targets.py @@ -1,8 +1,10 @@ +from logging import DEBUG, INFO import click import os import json from collections import defaultdict from pathlib import Path +from logdecorator import log_on_end, log_on_start from taf.api.metadata import update_snapshot_and_timestamp, update_target_metadata from taf.api.roles import ( _initialize_roles_and_keystore, @@ -15,11 +17,14 @@ from taf.git import GitRepository import taf.repositoriesdb as repositoriesdb +from taf.log import taf_logger from taf.auth_repo import AuthenticationRepository from taf.repository_tool import Repository from tuf.repository_tool import TARGETS_DIRECTORY_NAME +@log_on_start(DEBUG, "Adding target repository {target_name:s}", logger=taf_logger) +@log_on_end(DEBUG, "Finished adding target repository", logger=taf_logger) def add_target_repo( auth_path: str, target_path: str, @@ -30,6 +35,29 @@ def add_target_repo( scheme: str = DEFAULT_RSA_SIGNATURE_SCHEME, custom=None, ): + """ + Add a new target repository by adding it to repositories.json, creating a delegation (if targets is not + its signing role) and adding and signing initial target files if the repository already exists on the filesystem. + Also saves custom information about the repositories to repositories.json if it is provided. + + Arguments: + auth_path: Path to the authentication repository. + target_path: Path to the target repository which is to be added. + target_name (optional): Target repository's name. If not provided, it is determined based on the target path (the last two directories). + role: Name of the role which will be responsible for signing the new target file. + library_dir (optional): Path to the library's root directory. Determined based on the authentication repository's path if not provided. + threshold: Signature's threshold. + keystore: Location of the keystore files. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + custom (optional): Additional data that will be added to repositories.json if specified. + + Side Effects: + Updates metadata and repositories.json, adds a new target file if repository exists and writes changes to disk + and commits changes. + + Returns: + None + """ auth_repo = AuthenticationRepository(path=auth_path) if not auth_repo.is_git_repository_root: print(f"{auth_path} is not a git repository!") @@ -108,19 +136,35 @@ def add_target_repo( added_targets_data, removed_targets_data, keystore, - roles_infos=None, write=False, scheme=scheme, ) # update snapshot and timestamp calls write_all, so targets updates will be saved too - update_snapshot_and_timestamp(auth_repo, keystore, None, scheme=scheme) + update_snapshot_and_timestamp(auth_repo, keystore, scheme=scheme) commit_message = input("\nEnter commit message and press ENTER\n\n") auth_repo.commit(commit_message) -def export_targets_history(repo_path, commit=None, output=None, target_repos=None): - auth_repo = AuthenticationRepository(path=repo_path) +def export_targets_history(auth_path, commit=None, output=None, target_repos=None): + """ + Form a dictionary consisting of branches and commits belonging to it for every target repository + and either save it to a file or write to console. + + Arguments: + auth_path: Path to the authentication repository. + commit (optional): Authentication repository's commit which marks the first commit for which the data should be generated. + output (optional): File to which the exported history should be written. + target_repos (optional): A list of target repository names whose history should be generated. All target repositories + will be included if not provided. + + Side Effects: + None + + Returns: + None + """ + auth_repo = AuthenticationRepository(path=auth_path) commits = auth_repo.all_commits_since_commit(commit, auth_repo.default_branch) if not len(target_repos): target_repos = None @@ -152,19 +196,23 @@ def export_targets_history(repo_path, commit=None, output=None, target_repos=Non def list_targets( - repo_path: str, - library_dir: str, + auth_path: str, + library_dir: str = None, ): """ - - Save the top commit of specified target repositories to the corresponding target files and sign - - repo_path: - Authentication repository's location - library_dir: - Directory where target repositories and, optionally, authentication repository are locate + Save the top commit of specified target repositories to the corresponding target files and sign + + Arguments: + auth_path: Authentication repository's location + library_dir (optional): Path to the library's root directory. Determined based on the authentication repository's path if not provided. + + Side Effects: + None + + Returns: + None """ - auth_path = Path(repo_path).resolve() + auth_path = Path(auth_path).resolve() auth_repo = AuthenticationRepository(path=auth_path) top_commit = [auth_repo.head_commit_sha()] if library_dir is None: @@ -211,41 +259,40 @@ def list_targets( print(json.dumps(output, indent=4)) +@log_on_start(INFO, "Signing target files", logger=taf_logger) def register_target_files( - repo_path, + auth_path, keystore=None, roles_key_infos=None, commit=False, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, taf_repo=None, + write=False, ): """ - - Register all files found in the target directory as targets - updates the targets - metadata file, snapshot and timestamp. Sign targets - with yubikey if keystore is not provided - - repo_path: - Authentication repository's path - keystore: - Location of the keystore files - roles_key_infos: - A dictionary whose keys are role names, while values contain information about the keys. - commit_msg: - Commit message. If specified, the changes made to the authentication are committed. - scheme: - A signature scheme used for signing. - taf_repo: - If taf repository is already initialized, it can be passed and used. + Register all files found in the target directory as targets - update the targets + metadata file, snapshot and timestamp and sign them. Commit changes if commit is set to True. + + Arguments: + auth_path: Authentication repository's path. + keystore: Location of the keystore files. + roles_key_infos: A dictionary whose keys are role names, while values contain information about the keys. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + taf_repo (optional): If taf repository is already initialized, it can be passed and used. + write (optional): Write metadata updates to disk if set to True + + Side Effects: + Updates metadata files, writes changes to disk and optionally commits changes. + + Returns: + None """ - print("Signing target files") roles_key_infos, keystore = _initialize_roles_and_keystore( roles_key_infos, keystore, enter_info=False ) - roles_infos = roles_key_infos.get("roles") if taf_repo is None: - repo_path = Path(repo_path).resolve() - taf_repo = Repository(str(repo_path)) + auth_path = Path(auth_path).resolve() + taf_repo = Repository(str(auth_path)) # find files that should be added/modified/removed added_targets_data, removed_targets_data = taf_repo.get_all_target_files_state() @@ -255,21 +302,39 @@ def register_target_files( added_targets_data, removed_targets_data, keystore, - roles_infos, - scheme, + scheme=scheme, ) - if commit: - auth_git_repo = GitRepository(path=taf_repo.path) - commit_message = input("\nEnter commit message and press ENTER\n\n") - auth_git_repo.commit(commit_message) + if write: + taf_repo.writeall() + if commit: + auth_git_repo = GitRepository(path=taf_repo.path) + commit_message = input("\nEnter commit message and press ENTER\n\n") + auth_git_repo.commit(commit_message) +@log_on_start(DEBUG, "Removing target repository {target_name:s}", logger=taf_logger) +@log_on_end(DEBUG, "Finished removing target repository", logger=taf_logger) def remove_target_repo( auth_path: str, target_name: str, keystore: str, ): + """ + Remove target repository from repositories.json, remove delegation, and target files and + commit changes. + + Arguments: + auth_path: Authentication repository's path. + target_name: Name of the target name which is to be removed. + keystore: Location of the keystore files. + + Side Effects: + Updates metadata files, writes changes to disk and optionally commits changes. + + Returns: + None + """ auth_repo = AuthenticationRepository(path=auth_path) removed_targets_data = {} added_targets_data = {} @@ -302,19 +367,18 @@ def remove_target_repo( added_targets_data, removed_targets_data, keystore, - roles_infos=None, write=False, ) update_snapshot_and_timestamp( - auth_repo, keystore, None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME + auth_repo, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME ) auth_repo.commit(f"Remove {target_name} target") # commit_message = input("\nEnter commit message and press ENTER\n\n") remove_paths([target_name], keystore, commit=False, auth_repo=auth_repo) update_snapshot_and_timestamp( - auth_repo, keystore, None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME + auth_repo, keystore, scheme=DEFAULT_RSA_SIGNATURE_SCHEME ) auth_repo.commit(f"Remove {target_name} from delegated paths") # update snapshot and timestamp calls write_all, so targets updates will be saved too @@ -323,6 +387,10 @@ def remove_target_repo( def _save_top_commit_of_repo_to_target( library_dir: Path, repo_name: str, auth_repo_path: Path, add_branch: bool = True ): + """ + Determine the top commit of a target repository and write it to the corresponding + target file. + """ auth_repo_targets_dir = auth_repo_path / TARGETS_DIRECTORY_NAME target_repo_path = library_dir / repo_name namespace_and_name = repo_name.rsplit("/", 1) @@ -334,44 +402,51 @@ def _save_top_commit_of_repo_to_target( _update_target_repos(auth_repo_path, targets_dir, target_repo_path, add_branch) +@log_on_start(DEBUG, "Updating target files", logger=taf_logger) +@log_on_end(DEBUG, "Finished updating target files", logger=taf_logger) def update_target_repos_from_repositories_json( - repo_path, + auth_path, library_dir, keystore, add_branch=True, scheme=DEFAULT_RSA_SIGNATURE_SCHEME, ): """ - - Create or update target files by reading the latest commit's repositories.json - - repo_path: - Authentication repository's location - library_dir: - Directory where target repositories and, optionally, authentication repository are locate - namespace: - Namespace used to form the full name of the target repositories. Each target repository - add_branch: - Indicates whether to add the current branch's name to the target file + Create or update target files by reading the latest commit's repositories.json + + Arguments: + auth_path: Authentication repository's location. + library_dir: Path to the library's root directory. Determined based on the authentication repository's path if not provided. + keystore: Location of the keystore files. + add_branch: Indicates whether to add the current branch's name to the target file. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + + Side Effects: + Update target and metadata files and writes changes to disk. + + Returns: + None """ - repo_path = Path(repo_path).resolve() + auth_path = Path(auth_path).resolve() if library_dir is None: - library_dir = repo_path.parent.parent + library_dir = auth_path.parent.parent else: library_dir = Path(library_dir) - auth_repo_targets_dir = repo_path / TARGETS_DIRECTORY_NAME + auth_repo_targets_dir = auth_path / TARGETS_DIRECTORY_NAME repositories_json = json.loads( Path(auth_repo_targets_dir / "repositories.json").read_text() ) for repo_name in repositories_json.get("repositories"): _save_top_commit_of_repo_to_target( - library_dir, repo_name, repo_path, add_branch + library_dir, repo_name, auth_path, add_branch ) - register_target_files(repo_path, keystore, None, True, scheme) + register_target_files(auth_path, keystore, None, True, scheme, write=True) +@log_on_start(DEBUG, "Updating target files", logger=taf_logger) +@log_on_end(DEBUG, "Finished updating target files", logger=taf_logger) def update_and_sign_targets( - repo_path: str, + auth_path: str, library_dir: str, target_types: list, keystore: str, @@ -379,26 +454,23 @@ def update_and_sign_targets( scheme: str, ): """ - - Save the top commit of specified target repositories to the corresponding target files and sign - - repo_path: - Authentication repository's location - library_dir: - Directory where target repositories and, optionally, authentication repository are locate - targets: - Types of target repositories whose corresponding target files should be updated and signed - keystore: - Location of the keystore files - roles_key_infos: - A dictionary whose keys are role names, while values contain information about the keys - no_commit: - Indicates that the changes should bot get committed automatically - scheme: - A signature scheme used for signing + Save the top commit of specified target repositories to the corresponding target files and sign. + Arguments: + auth_path: Authentication repository's location. + library_dir (optional): Path to the library's root directory. Determined based on the authentication repository's path if not provided. + target_types: Types of target repositories whose corresponding target files should be updated and signed. + keystore: Location of the keystore files. + roles_key_infos: A dictionary whose keys are role names, while values contain information about the keys. + scheme (optional): Signing scheme. Set to rsa-pkcs1v15-sha256 by default. + + Side Effects: + Update target and metadata files and writes changes to disk. + + Returns: + None """ - auth_path = Path(repo_path).resolve() + auth_path = Path(auth_path).resolve() auth_repo = AuthenticationRepository(path=auth_path) if library_dir is None: library_dir = auth_path.parent.parent @@ -424,7 +496,9 @@ def update_and_sign_targets( for target_name in target_names: _save_top_commit_of_repo_to_target(library_dir, target_name, auth_path, True) print(f"Updated {target_name} target file") - register_target_files(auth_path, keystore, roles_key_infos, True, scheme) + register_target_files( + auth_path, keystore, roles_key_infos, True, scheme, write=True + ) def _update_target_repos(repo_path, targets_dir, target_repo_path, add_branch): diff --git a/taf/api/yubikey.py b/taf/api/yubikey.py index 9fabbd36..1faae9de 100644 --- a/taf/api/yubikey.py +++ b/taf/api/yubikey.py @@ -1,11 +1,23 @@ import click from pathlib import Path -from taf.constants import DEFAULT_RSA_SIGNATURE_SCHEME import taf.yubikey as yk def export_yk_public_pem(path=None): + """ + Export public key from a YubiKey and save it to a file or print to console. + + Arguments: + path (optional): Path to a file to which the public key should be written. + The key is printed to console if file path is not provided. + + Side Effects: + None + + Returns: + None + """ try: pub_key_pem = yk.export_piv_pub_key().decode("utf-8") except Exception: @@ -22,7 +34,20 @@ def export_yk_public_pem(path=None): path.write_text(pub_key_pem) -def setup_signing_yubikey(certs_dir=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME): +def setup_signing_yubikey(certs_dir=None): + """ + Delete everything from the inserted YubiKey, generate a new key and copy it to the YubiKey. + Optionally export and save the certificate to a file. + + Arguments: + certs_dir (optional): Path to a directory where the exported certificate should be stored. + + Side Effects: + None + + Returns: + None + """ if not click.confirm( "WARNING - this will delete everything from the inserted key. Proceed?" ): @@ -38,10 +63,19 @@ def setup_signing_yubikey(certs_dir=None, scheme=DEFAULT_RSA_SIGNATURE_SCHEME): yk.export_yk_certificate(certs_dir, key) -def setup_test_yubikey(key_path=None): +def setup_test_yubikey(key_path): """ - Resets the inserted yubikey, sets default pin and copies the specified key - onto it. + Reset the inserted yubikey, set default pin and copy the specified key + to it. + + Arguments: + key_path: Path to a key which should be copied to a YubiKey. + + Side Effects: + None + + Returns: + None """ if not click.confirm("WARNING - this will reset the inserted key. Proceed?"): return diff --git a/taf/keys.py b/taf/keys.py index ee4865c6..5612f767 100644 --- a/taf/keys.py +++ b/taf/keys.py @@ -70,7 +70,6 @@ def _sort_roles(key_info, repository): _, yubikey_keys = setup_roles_keys( role_name, key_info, - repository, certs_dir=auth_repo.certs_dir, yubikeys=yubikeys, ) @@ -82,7 +81,7 @@ def _sort_roles(key_info, repository): def load_sorted_keys_of_new_roles( - auth_repo, roles_infos, repository, keystore, yubikeys, existing_roles=None + auth_repo, roles_infos, keystore, yubikeys, existing_roles=None ): def _sort_roles(key_info, repository): # load keys not stored on YubiKeys first, to avoid entering pins @@ -106,14 +105,14 @@ def _sort_roles(key_info, repository): if existing_roles is None: existing_roles = [] try: - keystore_roles, yubikey_roles = _sort_roles(roles_infos, repository) + keystore_roles, yubikey_roles = _sort_roles(roles_infos, auth_repo) signing_keys = {} verification_keys = {} for role_name, key_info in keystore_roles: if role_name in existing_roles: continue keystore_keys, _ = setup_roles_keys( - role_name, key_info, repository, keystore=keystore + role_name, key_info, auth_repo.path, keystore=keystore ) for public_key, private_key in keystore_keys: signing_keys.setdefault(role_name, []).append(private_key) @@ -125,7 +124,6 @@ def _sort_roles(key_info, repository): _, yubikey_keys = setup_roles_keys( role_name, key_info, - repository, certs_dir=auth_repo.certs_dir, yubikeys=yubikeys, )