Пример #1
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
Пример #2
0
    def __init__(self, image: str):
        # e.g. example.com, super.example.com:3498
        domain_re = r"(?:[a-z0-9-]{1,63}\.){1,62}[a-z0-9-]{1,63}(?::[0-9]{1,5})?"
        # e.g. library/, library/alpine/,
        repo_re = r"(?:[\w-]+\/)+"
        # e.g. alpine, nginx, hello-world
        image_re = r"[\w.-]+"
        # e.g. :v1, :3.7-alpine, @sha256:3e7a89...
        tag_re = r"(?:(?:@sha256:([a-f0-9]{64}))|(?:\:([\w.-]+)))"

        # e.g. docker.io/library/python:3.7-alpine
        regex = f"^({domain_re}/)?({repo_re})?({image_re})({tag_re})?$"

        match = re.search(regex, image)
        if not match:
            raise InvalidFormatException(
                '"{}" is not a valid image format.'.format(image)
            )

        self.registry, self.repository, self.name, self.digest, self.tag = (
            match.group(1),
            match.group(2),
            match.group(3),
            match.group(5),
            match.group(6),
        )
        # strip trailing "/" or set to default "docker.io" registry
        self.registry = (self.registry or "docker.io").rstrip("/")
        # strip trailing "/"
        self.repository = (self.repository or "/").rstrip("/")

        if not (self.tag or self.digest):
            self.tag = "latest"
Пример #3
0
    def __init__(self, role: str):
        regex = "^(root|(targets(/[^/\\s]+)?)|snapshot|timestamp)$"
        match = re.match(regex, role)

        if not match:
            msg = "{role} is not a valid TUF role."
            raise InvalidFormatException(message=msg, role=str(role))

        self.role = role
Пример #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
Пример #5
0
    def __init__(self, role: str):
        regex = "^(root|(targets(/[^/\\s]+)?)|snapshot|timestamp)$"
        match = re.match(regex, role)

        if not match:
            raise InvalidFormatException(
                '"{}" is not a valid TUF role.'.format(role))

        self.role = role
Пример #6
0
    def __init__(self):
        # load policy from k8s
        image_policy = ImagePolicy.get_image_policy()

        # validate policy
        with open(self.JSON_SCHEMA_PATH, "r") as schema_file:
            schema = json.load(schema_file)
        try:
            validate(instance=image_policy, schema=schema)
        except ValidationError:
            raise InvalidFormatException("invalid format for image policy.")

        self.policy = image_policy
Пример #7
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
Пример #8
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
Пример #9
0
    def __get_pubkey_config(key: str):
        """
        Return a tuple of the used Cosign verification command (flag-value list), a
        dict of potentially required environment variables and public key in binary
        PEM format to be used as stdin to Cosign based on the format of the input
        key (reference).

        Raise InvalidFormatException if none of the supported patterns is matched.
        """
        try:
            # key is ecdsa public key
            pkey = load_key(key).to_pem()  # raises if invalid
            return ["--key", "/dev/stdin"], {}, pkey
        except ValueError:
            pass

        # key is KMS reference
        if re.match(r"^\w{2,20}://[\w:/-]{3,255}$", key):
            return ["--key", key], {}, b""

        msg = "Public key (reference) '{input_str}' does not match expected patterns."
        raise InvalidFormatException(message=msg, input_str=key)
Пример #10
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