Exemplo n.º 1
0
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()
Exemplo n.º 2
0
    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")
Exemplo n.º 3
0
    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
Exemplo n.º 4
0
    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
Exemplo n.º 5
0
 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)
Exemplo n.º 7
0
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
Exemplo n.º 8
0
    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))
Exemplo n.º 9
0
 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)
Exemplo n.º 10
0
    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))
Exemplo n.º 11
0
    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
Exemplo n.º 12
0
    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()
Exemplo n.º 14
0
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
Exemplo n.º 15
0
    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
Exemplo n.º 16
0
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)
Exemplo n.º 17
0
    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)
Exemplo n.º 18
0
    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))
Exemplo n.º 19
0
    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
Exemplo n.º 20
0
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
Exemplo n.º 21
0
 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
Exemplo n.º 23
0
 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
Exemplo n.º 24
0
 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))
Exemplo n.º 25
0
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