def get_trusted_digest(host: str, image: Image, policy_rule: dict): """ Searches in given notary server(`host`) for trust data, that belongs to the given `image`, by using the notary API. Also checks whether the given `policy_rule` complies. Returns the signed digest, belonging to the `image` or throws if validation fails. """ # prepend `targets/` to the required delegation roles, if not already present req_delegations = list( map(normalize_delegation, policy_rule.get("delegations", [])) ) # get list of targets fields, containing tag to signed digest mapping from # `targets.json` and all potential delegation roles signed_image_targets = process_chain_of_trust(host, image, req_delegations) # search for digests or tag, depending on given image search_image_targets = ( search_image_targets_for_digest if image.has_digest() else search_image_targets_for_tag ) # filter out the searched for digests, if present digests = list(map(lambda x: search_image_targets(x, image), signed_image_targets)) # in case certain delegations are needed, `signed_image_targets` should only # consist of delegation role targets. if searched for the signed digest, none of # them should be empty if req_delegations and not all(digests): raise NotFoundException( 'not all required delegations have trust data for image "{}".'.format( str(image) ) ) # filter out empty results and squash same elements digests = set(filter(None, digests)) # no digests could be found if not digests: raise NotFoundException( 'could not find signed digest for image "{}" in trust data.'.format( str(image) ) ) # if there is more than one valid digest in the set, no decision can be made, which # to chose if len(digests) > 1: raise AmbiguousDigestError("found multiple signed digests for the same image.") return digests.pop()
def __init__(self): """ Create a Config object, containing all validator configurations. Read a config file, validate its contents and then create Validator objects, storing them. Raise `NotFoundException` if the configuration file is not found. Raise `InvalidFormatException` if the configuration file has an invalid format. """ with open(self.__PATH, "r", encoding="utf-8") as configfile: config_content = yaml.safe_load(configfile) if not config_content: msg = "Error loading connaisseur config file." raise NotFoundException(message=msg) with open(self.__SECRETS_PATH, "r", encoding="utf-8") as secrets_configfile: secrets_config_content = yaml.safe_load(secrets_configfile) config = self.__merge_configs(config_content, secrets_config_content) self.__validate(config) self.validators = [ Validator(**validator) for validator in config.get("validators") ] self.policy = config.get("policy")
async def __get_auth_token(self, url: str): """ Return the JWT from the given `url`, using user and password from environment variables. Raise an exception if a HTTP error status code occurs. """ async with aiohttp.ClientSession() as session: request_kwargs = { "url": url, "ssl": self.cert, "auth": (aiohttp.BasicAuth(**self.auth) if self.auth else None), } async with session.get(**request_kwargs) as response: if response.status >= 500: msg = "Unable to get authentication token from {auth_url}." raise NotFoundException(message=msg, notary_name=self.name, auth_url=url) response.raise_for_status() try: token_key = "access_token" if self.is_acr else "token" token = (await response.json())[token_key] except KeyError as err: msg = "Unable to retrieve authentication token from {auth_url} response." raise NotFoundException(message=msg, notary_name=self.name, auth_url=url) from err token_re = ( r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$" # nosec ) if not re.match(token_re, token): msg = "{validation_kind} has an invalid format." raise InvalidFormatException( message=msg, validation_kind="Authentication token", notary_name=self.name, auth_url=url, ) return token
def __parse_auth(self, header: str): """ Generates an URL from the 'Www-authenticate' header, where a token can be requested. """ auth_types = [ "Basic", "Bearer", "Digest", "HOBA", "Mutual", "Negotiate", "OAuth", "SCRAM-SHA-1", "SCRAM-SHA-256", "vapid", ] auth_type_re = re.compile(f'({"|".join(auth_types)}) realm') params_re = re.compile(r'(\w+)="?([\w./:\-_]+)"?') auth_type = next(iter(auth_type_re.findall(header)), None) params_dict = dict(params_re.findall(header)) if not auth_type or auth_type != "Bearer": msg = ("{auth_type} is an unsupported authentication" " type in notary {notary_name}.") raise UnknownTypeException(message=msg, auth_type=auth_type, notary_name=self.name) try: realm = quote(params_dict.pop("realm"), safe="/:") except KeyError as err: msg = ("Unable to find authentication realm in auth" " header for notary {notary_name}.") raise NotFoundException(message=msg, notary_name=self.name, auth_header=params_dict) from err params = urlencode(params_dict, safe="/:") url = f"{realm}?{params}" if not url.startswith("https"): msg = ("authentication through insecure channel " "for notary {notary_name} is prohibited.") raise InvalidFormatException(message=msg, notary_name=self.name, auth_url=url) if ".." in url or url.count("//") > 1: msg = ("Potential path traversal in authentication" " url for notary {notary_name}.") raise PathTraversalError(message=msg, notary_name=self.name, auth_url=url) return url
def m_get_trusted_digest(host: str, image: Image, policy_rule: dict): if (image.digest == "1337133713371337133713371337133713371337133713371337133713371337" ): return "abcdefghijklmnopqrst" else: raise NotFoundException( 'could not find signed digest for image "{}" in trust data.'. format(str(image)))
def __validate_all_required_delegations_present(required_delegations, present_delegations): if required_delegations: if present_delegations: req_delegations_set = set(required_delegations) delegations_set = set(present_delegations) # make an intersection between required delegations and actually # present ones if not req_delegations_set.issubset(delegations_set): missing = list(req_delegations_set - delegations_set) msg = ("Unable to find delegation roles " "{delegation_roles} in trust data.") raise NotFoundException(message=msg, delegation_roles=str(missing)) else: msg = "Unable to find any delegations in trust data." raise NotFoundException(message=msg)
def get_auth_token(url: str): """ Return the JWT from the given `url`, using user and password from environment variables. Raises an exception if a HTTP error status code occurs. """ user = os.environ.get("NOTARY_USER", False) password = os.environ.get("NOTARY_PASS", "") request_kwargs = {"url": url} if user: request_kwargs["auth"] = requests.auth.HTTPBasicAuth(user, password) if is_notary_selfsigned(): request_kwargs["verify"] = "/etc/certs/notary.crt" response = requests.get(**request_kwargs) if response.status_code >= 500: raise NotFoundException( "unable to get auth token, likely because of missing trust data.", {"auth_url": url}, ) response.raise_for_status() try: if is_acr(): token = response.json()["access_token"] else: token = response.json()["token"] except KeyError: raise NotFoundException( "no token in authentication server response.", {"auth_url": url} ) token_re = r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$" # nosec if not re.match(token_re, token): raise InvalidFormatException( "authentication token has wrong format.", {"auth_url": url} ) return token
def get_key(self, key_id: str): """ Return a public key, given its `key_id`. Raises a `NotFoundException` should the `key_id` not exist. """ try: return self.keys[key_id] except KeyError: raise NotFoundException( 'could not find key id "{}" in keystore.'.format(key_id))
def __get_key(self, key_name: str = None): key_name = key_name or "default" try: key = next(key["key"] for key in self.trust_roots if key["name"] == key_name) except StopIteration as err: msg = 'Trust root "{key_name}" not configured for validator "{validator_name}".' raise NotFoundException(message=msg, key_name=key_name, validator_name=self.name) from err return "".join(key)
def get_hash(self, role: str): """ Returns the hash of the given `role`'s trust data. Raises a `NotFoundException` if the `role` is not found. """ try: return self.hashes[role] except KeyError: raise NotFoundException( 'could not find hash for role "{}" in keystore.'.format(role))
def get_key(self, key_id: str): """ Return a public key, given its `key_id`. Raise a `NotFoundException` should the `key_id` not exist. """ try: return self.keys[key_id] except KeyError as err: msg = "Unable to find key {key_id} in keystore." raise NotFoundException(message=msg, key_id=key_id) from err
def get_hash(self, role: str): """ Return the hash of the given `role`'s trust data. Raise a `NotFoundException` if the `role` is not found. """ try: return self.hashes[role] except KeyError as err: msg = "Unable to find hash for {tuf_role} in keystore." raise NotFoundException(message=msg, tuf_role=role) from err
async def validate(self, image: Image, trust_root: str = None, delegations: list = None, **kwargs): # pylint: disable=arguments-differ if delegations is None: delegations = [] # get the public root key pub_key = self.notary.get_key(trust_root) # prepend `targets/` to the required delegation roles, if not already present req_delegations = list( map(NotaryV1Validator.__normalize_delegation, delegations)) # get list of targets fields, containing tag to signed digest mapping from # `targets.json` and all potential delegation roles signed_image_targets = await self.__process_chain_of_trust( image, req_delegations, pub_key) # search for digests or tag, depending on given image search_image_targets = ( NotaryV1Validator.__search_image_targets_for_digest if image.has_digest() else NotaryV1Validator.__search_image_targets_for_tag) # filter out the searched for digests, if present digests = list( map(lambda x: search_image_targets(x, image), signed_image_targets)) # in case certain delegations are needed, `signed_image_targets` should only # consist of delegation role targets. if searched for the signed digest, none of # them should be empty if req_delegations and not all(digests): msg = "Not all required delegations have trust data for image {image_name}." raise InsufficientTrustDataError(message=msg, image_name=str(image)) # filter out empty results and squash same elements digests = set(filter(None, digests)) # no digests could be found if not digests: msg = "Unable to find signed digest for image {image_name}." raise NotFoundException(message=msg, image_name=str(image)) # if there is more than one valid digest in the set, no decision can be made, # which to chose if len(digests) > 1: msg = "Found multiple signed digests for image {image_name}." raise AmbiguousDigestError(message=msg, image_name=str(image)) return digests.pop()
def parse_auth(auth_header: str): """ Generates an URL from the 'Www-authenticate' header, where a token can be requested. """ auth_types = [ "Basic", "Bearer", "Digest", "HOBA", "Mutual", "Negotiate", "OAuth", "SCRAM-SHA-1", "SCRAM-SHA-256", "vapid", ] auth_type_re = re.compile("({}) realm".format("|".join(auth_types))) params_re = re.compile(r'(\w+)="?([\w\.\/\:\-\_]+)"?') auth_type = next(iter(auth_type_re.findall(auth_header)), None) if not auth_type or auth_type != "Bearer": raise UnsupportedTypeException( "unsupported authentication type for getting trust data.", {"auth_header": auth_header}, ) params_dict = dict(params_re.findall(auth_header)) try: realm = quote(params_dict.pop("realm"), safe="/:") except KeyError: raise NotFoundException( "could not find any realm in authentication header.", {"auth_header": auth_header}, ) params = urlencode(params_dict, safe="/:") url = f"{realm}?{params}" if not url.startswith("https"): raise InvalidFormatException( "authentication through insecure channel.", {"auth_url": url}) if ".." in url or url.count("//") > 1: raise InvalidFormatException("potential path traversal.", {"auth_url": url}) return url
def get_validator(self, validator_name: str = None): """ Return the validator configuration with the given `validator_name`. If `validator_name` is None, return the element with `name=default`, or the only existing element. Raise `NotFoundException` if no matching or default element can be found. """ try: return list( filter( lambda v: v.name == (validator_name or "default"), self.validators ) )[0] except IndexError as err: msg = "Unable to find validator configuration {validator_name}." raise NotFoundException(message=msg, validator_name=validator_name) from err
def get_trust_data(host: str, image: Image, role: TUFRole, token: str = None): """ Request the specific trust data, denoted by the `role` and `image` form the notary server (`host`). Uses a token, should authentication be required. """ if image.repository: url = ( f"https://{host}/v2/{image.registry}/{image.repository}/" f"{image.name}/_trust/tuf/{role.role}.json" ) else: url = ( f"https://{host}/v2/{image.registry}/" f"{image.name}/_trust/tuf/{role.role}.json" ) request_kwargs = {"url": url} if token: request_kwargs["headers"] = {"Authorization": f"Bearer {token}"} if is_notary_selfsigned(): request_kwargs["verify"] = "/etc/certs/notary.crt" response = requests.get(**request_kwargs) if not token and response.status_code == 401: case_insensitive_headers = { k.lower(): response.headers[k] for k in response.headers } if case_insensitive_headers["www-authenticate"]: auth_url = parse_auth(case_insensitive_headers["www-authenticate"]) token = get_auth_token(auth_url) return get_trust_data(host, image, role, token) if response.status_code == 404: raise NotFoundException( 'no trust data for image "{}".'.format(str(image)), {"tuf_role": role.role} ) response.raise_for_status() data = response.json() return TrustData(data, role.role)
def get_key(self, key_name: str = None): """ Return the public root key with name `key_name` in DER format, without any whitespaces. If `key_name` is None, return the top most element of the public root key list. Raise `NotFoundException` if no top most element can be found. """ key_name = key_name or "default" try: key = next(key["key"] for key in self.pub_root_keys if key["name"] == key_name) except StopIteration as err: msg = ( 'Trust root "{key_name}" not configured for validator "{notary_name}".' ) raise NotFoundException(message=msg, key_name=key_name, notary_name=self.name) from err return "".join(key)
async def get_trust_data(self, image: Image, role: TUFRole, token: str = None): im_repo = f"{image.repository}/" if image.repository else "" url = (f"https://{self.host}/v2/{image.registry}/{im_repo}" f"{image.name}/_trust/tuf/{str(role)}.json") async with aiohttp.ClientSession() as session: request_kwargs = { "url": url, "ssl": self.cert, "headers": ({ "Authorization": f"Bearer {token}" } if token else None), } async with session.get(**request_kwargs) as response: status = response.status if (status == 401 and not token and ("www-authenticate" in [k.lower() for k in response.headers])): auth_url = self.__parse_auth({ k.lower(): v for k, v in response.headers.items() }["www-authenticate"]) token = await self.__get_auth_token(auth_url) return await self.get_trust_data(image, role, token) if status == 404: msg = "Unable to get {tuf_role} trust data from {notary_name}." raise NotFoundException(message=msg, notary_name=self.name, tuf_role=str(role)) response.raise_for_status() data = await response.text() return TrustData(json.loads(data), str(role))
def get_matching_rule(self, image: Image): """ Returns for a given `image` the most specific matching rule. """ rules = [rule["pattern"] for rule in self.policy["rules"]] best_match = Match("", "") for rule in rules: rule_with_tag = f"{rule}:*" if ":" not in rule else rule if fnmatch.fnmatch(str(image), rule_with_tag): match = Match(rule, str(image)) best_match = match.compare(best_match) if not best_match: raise NotFoundException( 'no matching rule for image "{}" could be found.'.format(str(image)) ) most_specific_rule = next( filter(lambda x: x["pattern"] == best_match.key, self.policy["rules"]), None ) return most_specific_rule
def process_chain_of_trust(host: str, image: Image, req_delegations: list): """ Processes the whole chain of trust, provided by the notary server (`host`) for any given `image`. The 'root', 'snapshot', 'timestamp', 'targets' and potentially 'targets/releases' are requested in this order and afterwards validated, also according to the `policy_rule`. Returns the the signed image targets, which contain the digests. Raises `NotFoundExceptions` should no required delegetions be present in the trust data, or no image targets be found. """ tuf_roles = ["root", "snapshot", "timestamp", "targets"] trust_data = {} key_store = KeyStore() # get all trust data and collect keys (from root and targets), as well as # hashes (from snapshot and timestamp) for role in tuf_roles: trust_data[role] = get_trust_data(host, image, TUFRole(role)) key_store.update(trust_data[role]) # if the 'targets.json' has delegation roles defined, get their trust data # as well if trust_data["targets"].has_delegations(): for delegation in trust_data["targets"].get_delegations(): trust_data[delegation] = get_trust_data(host, image, TUFRole(delegation)) # validate all trust data's signatures, expiry dates and hashes for role in trust_data: trust_data[role].validate(key_store) # validate needed delegations if req_delegations: if trust_data["targets"].has_delegations(): delegations = trust_data["targets"].get_delegations() req_delegations_set = set(req_delegations) delegations_set = set(delegations) delegations_set.discard("targets/releases") # make an intersection between required delegations and actually # present ones if not req_delegations_set.issubset(delegations_set): missing = list(req_delegations_set - delegations_set) raise NotFoundException( "could not find delegation roles {} in trust data.".format( str(missing))) else: raise NotFoundException( "could not find any delegations in trust data.") # get a list from all `targets` fields from `targets.json` + delegation roles or # just the delegation roles, should there be required delegation according to the # policy if req_delegations: image_targets = [ trust_data[target_role].signed.get("targets", {}) for target_role in req_delegations ] else: image_targets = [ trust_data[target_role].signed.get("targets", {}) for target_role in trust_data if re.match("targets(/[^/\\s]+)?", target_role) ] if not any(image_targets): raise NotFoundException( "could not find any image digests in trust data.") return image_targets
def get_digest(self, tag: str): try: return self.signed.get("targets", {})[tag]["hashes"]["sha256"] except KeyError as err: msg = "Unable to find digest for tag {tag}." raise NotFoundException(message=msg, tag=tag) from err
async def __process_chain_of_trust(self, image: Image, req_delegations: list, pub_root_key: str): # pylint: disable=too-many-branches """ Process the whole chain of trust, provided by the notary server (`notary_config`) for any given `image`. Request and validate the 'root', 'snapshot', 'timestamp', 'targets' and potentially 'targets/releases'. Additionally, check whether all required delegations are valid. Return the signed image targets, which contain the digests. Raise `NotFoundExceptions` should no required delegations be present in the trust data, or no image targets be found. """ key_store = KeyStore(pub_root_key) tuf_roles = ["root", "snapshot", "timestamp", "targets"] # load all trust data t_start = dt.datetime.now() trust_data_list = await asyncio.gather(*[ self.notary.get_trust_data(image, TUFRole(role)) for role in tuf_roles ]) duration = (dt.datetime.now() - t_start).total_seconds() logging.debug("Pulled trust data for image %s in %s seconds.", image, duration) trust_data = { tuf_roles[i]: trust_data_list[i] for i in range(len(tuf_roles)) } # validate signature and expiry data of and load root file # this does NOT conclude the validation of the root file. To prevent # rollback/freeze attacks, the hash still needs to be validated against # the snapshot file root_trust_data = trust_data["root"] root_trust_data.validate_signature(key_store) root_trust_data.validate_expiry() key_store.update(root_trust_data) # validate timestamp file to prevent freeze attacks # validates signature and expiry data # there is no hash to verify it against since it is short lived # TODO should we ensure short expiry duration here? timestamp_trust_data = trust_data["timestamp"] timestamp_trust_data.validate(key_store) # validate snapshot file signature against the key defined in the root file # and its hash against the one from the timestamp file # and validate expiry snapshot_trust_data = trust_data["snapshot"] snapshot_trust_data.validate_signature(key_store) timestamp_key_store = KeyStore() timestamp_key_store.update(timestamp_trust_data) snapshot_trust_data.validate_hash(timestamp_key_store) snapshot_trust_data.validate_expiry() # now snapshot and timestamp files are validated, we can be safe against # rollback and freeze attacks if the root file matches the hash of the snapshot # file (or the root key has been compromised, which Connaisseur cannot defend # against) snapshot_key_store = KeyStore() snapshot_key_store.update(snapshot_trust_data) root_trust_data.validate_hash(snapshot_key_store) # if we are safe at this point, we can add the snapshot data to the main KeyStore # and proceed with validating the targets file and (potentially) delegation files key_store.update(snapshot_trust_data) targets_trust_data = trust_data["targets"] targets_trust_data.validate(key_store) key_store.update(targets_trust_data) # if the 'targets.json' has delegation roles defined, get their trust data # as well if trust_data["targets"].has_delegations() or req_delegations: # if no delegations are required, take the "targets/releases" per default req_delegations = req_delegations or ["targets/releases"] # validate existence of required delegations NotaryV1Validator.__validate_all_required_delegations_present( req_delegations, trust_data["targets"].get_delegations()) # download only the required delegation files await self.__update_with_delegation_trust_data( trust_data, req_delegations, key_store, image) # if certain delegations are required, then only take the targets fields of the # required delegation JSONs. otherwise take the targets field of the targets JSON, # as long as no delegations are defined in the targets JSON. should there be # delegations defined in the targets JSON the targets field of the releases # JSON will be used. unfortunately there is a case, where delegations could have # been added to a repository, but no signatures were created using the # delegations. in this special case, the releases JSON doesn't exist yet and # the targets JSON must be used instead if req_delegations == [ "targets/releases" ] and ("targets/releases" not in trust_data or not trust_data["targets/releases"]): req_delegations = ["targets"] elif not all(target_role in trust_data and trust_data[target_role] for target_role in req_delegations): tuf_roles = [ target_role for target_role in req_delegations if target_role not in trust_data or not trust_data[target_role] ] msg = ("Unable to find trust data for delegation " "roles {tuf_roles} and image {image_name}.") raise NotFoundException(message=msg, tuf_roles=str(tuf_roles), image_name=str(image)) image_targets = [ trust_data[target_role].signed.get("targets", {}) for target_role in req_delegations ] else: image_targets = [trust_data["targets"].signed.get("targets", {})] if not any(image_targets): msg = "Unable to find any image digests in trust data." raise NotFoundException(message=msg) return image_targets
def __get_cosign_validated_digests(self, image: str, key: str): """ Get and process Cosign validation output for a given `image` and `key` and either return a list of valid digests or raise a suitable exception in case no valid signature is found or Cosign fails. """ returncode, stdout, stderr = self.__invoke_cosign(image, key) logging.info( "COSIGN output for image: %s; RETURNCODE: %s; STDOUT: %s; STDERR: %s", image, returncode, stdout, stderr, ) digests = [] if returncode == 0: for sig in stdout.splitlines(): try: sig_data = json.loads(sig) try: digest = sig_data["critical"]["image"].get( "docker-manifest-digest", "") if re.match(r"sha256:[0-9A-Fa-f]{64}", digest) is None: msg = "Digest '{digest}' does not match expected digest pattern." raise InvalidFormatException(message=msg, digest=digest) except Exception as err: msg = ( "Could not retrieve valid and unambiguous digest from data " "received by Cosign: {err_type}: {err}") raise UnexpectedCosignData(message=msg, err_type=type(err).__name__, err=str(err)) from err # remove prefix 'sha256' digests.append(digest.removeprefix("sha256:")) except json.JSONDecodeError: logging.info("non-json signature data from Cosign: %s", sig) pass elif "Error: no matching signatures:\nfailed to verify signature\n" in stderr: msg = "Failed to verify signature of trust data." raise ValidationError( message=msg, trust_data_type="dev.cosignproject.cosign/signature", stderr=stderr, ) elif "Error: no matching signatures:\n\nmain.go:" in stderr: msg = 'No trust data for image "{image}".' raise NotFoundException( message=msg, trust_data_type="dev.cosignproject.cosign/signature", stderr=stderr, image=str(image), ) else: msg = 'Unexpected Cosign exception for image "{image}": {stderr}.' raise CosignError( message=msg, trust_data_type="dev.cosignproject.cosign/signature", stderr=stderr, image=str(image), ) if not digests: msg = ("Could not extract any digest from data received by Cosign " "despite successful image verification.") raise UnexpectedCosignData(message=msg) return digests
def get_digest(self, tag: str): try: return self.signed.get("targets", {})[tag]["hashes"]["sha256"] except KeyError: raise NotFoundException( 'could not find digest for tag "{}".'.format(tag))
def process_chain_of_trust( host: str, image: Image, req_delegations: list ): # pylint: disable=too-many-branches """ Processes the whole chain of trust, provided by the notary server (`host`) for any given `image`. The 'root', 'snapshot', 'timestamp', 'targets' and potentially 'targets/releases' are requested in this order and afterwards validated, also according to the `policy_rule`. Returns the signed image targets, which contain the digests. Raises `NotFoundExceptions` should no required delegetions be present in the trust data, or no image targets be found. """ tuf_roles = ["root", "snapshot", "timestamp", "targets"] trust_data = {} key_store = KeyStore() # get all trust data and collect keys (from root and targets), as well as # hashes (from snapshot and timestamp) for role in tuf_roles: trust_data[role] = get_trust_data(host, image, TUFRole(role)) key_store.update(trust_data[role]) # if the 'targets.json' has delegation roles defined, get their trust data # as well if trust_data["targets"].has_delegations(): for delegation in trust_data["targets"].get_delegations(): trust_data[delegation] = get_delegation_trust_data( host, image, TUFRole(delegation) ) # validate all trust data's signatures, expiry dates and hashes. # when delegations are added to the repository, but weren't yet used for signing, the # delegation files don't exist yet and are `None`. in this case they can't be # validated and must be skipped for role in trust_data: if trust_data[role] is not None: trust_data[role].validate(key_store) # validate needed delegations if req_delegations: if trust_data["targets"].has_delegations(): delegations = trust_data["targets"].get_delegations() req_delegations_set = set(req_delegations) delegations_set = set(delegations) delegations_set.discard("targets/releases") # make an intersection between required delegations and actually # present ones if not req_delegations_set.issubset(delegations_set): missing = list(req_delegations_set - delegations_set) raise NotFoundException( "could not find delegation roles {} in trust data.".format( str(missing) ) ) else: raise NotFoundException("could not find any delegations in trust data.") # if certain delegations are required, then only take the targets fields of the # required delegation JSON's. otherwise take the targets field of the targets JSON, as # long as no delegations are defined in the targets JSON. should there be delegations # defined in the targets JSON the targets field of the releases JSON will be used. # unfortunately there is a case, where delegations could have been added to a # repository, but no signatures were created using the delegations. in this special # case, the releases JSON doesn't exist yet and the targets JSON must be used instead if req_delegations: if not all(trust_data[target_role] for target_role in req_delegations): tuf_roles = [ target_role for target_role in req_delegations if not trust_data[target_role] ] msg = f"no trust data for delegation roles {tuf_roles} for image {image}" raise NotFoundException(msg, {"tuf_roles": tuf_roles}) image_targets = [ trust_data[target_role].signed.get("targets", {}) for target_role in req_delegations ] else: targets_key = ( "targets/releases" if trust_data["targets"].has_delegations() and trust_data["targets/releases"] else "targets" ) image_targets = [trust_data[targets_key].signed.get("targets", {})] if not any(image_targets): raise NotFoundException("could not find any image digests in trust data.") return image_targets