async def test_process_chain_of_trust( monkeypatch, sample_nv1, m_request, m_trust_data, m_expiry, count_loaded_delegations, image: str, delegations: list, key: str, targets: list, delegation_count: int, exception, ): monkeypatch.setenv("DELEGATION_COUNT", "0") with exception: with aioresponses() as aio: aio.get(re.compile(r".*"), callback=fix.async_callback, repeat=True) signed_targets = ( await sample_nv1._NotaryV1Validator__process_chain_of_trust( Image(image), delegations, key)) assert signed_targets == targets assert delegation_count == int(os.getenv("DELEGATION_COUNT"))
def test_get_matching_rule_error(pol, mock_policy): p = pol.ImagePolicy() p.policy["rules"] = p.policy["rules"][1:] with pytest.raises(BaseConnaisseurException) as err: p.get_matching_rule(Image("reg.io/image")) assert ("no matching rule for image " '"reg.io/image:latest" could be found.') in str(err.value)
def test_get_trusted_digest( mock_trust_data, mock_keystore, mock_request, image: str, policy_rule: dict, digest: str, ): assert val.get_trusted_digest("host", Image(image), policy_rule) == digest
def test_process_chain_of_trust( napi, mock_keystore, mock_request, mock_trust_data, image: str, req_delegations: dict, targets: list, ): assert napi.process_chain_of_trust("host", Image(image), req_delegations) == targets
async def test_update_with_delegation_trust_data( m_request, m_trust_data, m_expiry, alice_key_store, sample_nv1, delegations, ): assert ( await sample_nv1._NotaryV1Validator__update_with_delegation_trust_data( {}, delegations, alice_key_store, Image("alice-image")) is None)
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 test_process_chain_of_trust_error( mock_keystore, mock_request, mock_trust_data, image: str, req_delegations: list, error: str, ): with pytest.raises(BaseConnaisseurException) as err: val.process_chain_of_trust("host", Image(image), req_delegations) assert error in str(err.value)
def test_process_chain_of_trust( monkeypatch, mock_keystore, mock_request, mock_trust_data, image: str, req_delegations: dict, targets: list, root_pub: str, ): if root_pub: monkeypatch.setenv("ROOT_PUB", root_pub) assert val.process_chain_of_trust("host", Image(image), req_delegations) == targets
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 test_get_trusted_digest_error( monkeypatch, mock_trust_data, mock_keystore, mock_request, image: str, policy: dict, error: str, root_pub: str, ): if root_pub: monkeypatch.setenv("ROOT_PUB", root_pub) with pytest.raises(BaseConnaisseurException) as err: val.get_trusted_digest("host", Image(image), policy) assert error in str(err.value)
async def test_get_delegation_trust_data( monkeypatch, sample_notaries, m_request, m_trust_data, index, image, output, exception, log_lvl, ): monkeypatch.setenv("LOG_LEVEL", log_lvl) with exception: with aioresponses() as aio: aio.get(re.compile(r".*"), callback=fix.async_callback) no = notary.Notary(**sample_notaries[index]) td = await no.get_delegation_trust_data(Image(image), "targets/phbelitz") assert output is bool(td)
async def test_get_trust_data( sample_notaries, m_request, m_trust_data, index, image, role, output, exception, ): with exception: with aioresponses() as aio: aio.get(re.compile(r".*"), callback=fix.async_callback, repeat=True) no = notary.Notary(**sample_notaries[index]) td = await no.get_trust_data(Image(image), role) assert td.signed == output["signed"] assert td.signatures == output["signatures"]
async def test_validate( sample_nv1, m_trust_data, m_request, m_expiry, image: str, key: str, delegations: list, digest: str, exception, ): with exception: with aioresponses() as aio: aio.get(re.compile(r".*"), callback=fix.async_callback, repeat=True) signed_digest = await sample_nv1.validate(Image(image), key, delegations) assert signed_digest == digest
def test_get_matching_rule_error(): with pytest.raises(exc.NoMatchingPolicyRuleError): c = co.Config() c.policy = c.policy[1:] assert c.get_policy_rule(Image("reg.io/image"))
def test_get_policy_rule(image: str, rule): c = co.Config() assert str(c.get_policy_rule(Image(image))) == rule
def test_get_trust_data_error(napi, mock_request, mock_trust_data): with pytest.raises(BaseConnaisseurException) as err: napi.get_trust_data("host", Image("empty.io/image:tag"), TUFRole("targets")) assert 'no trust data for image "empty.io/image:tag".' in str(err.value)
def test_get_trust_data(napi, mock_request, mock_trust_data, image: str, role: str, out: dict): trust_data_ = napi.get_trust_data("host", Image(image), TUFRole(role)) assert trust_data_.signed == out["signed"] assert trust_data_.signatures == out["signatures"]
def test_search_image_targets_for_tag(napi, image: str, digest: str): data = trust_data("tests/data/sample_releases.json")["signed"]["targets"] assert napi.search_image_targets_for_tag(data, Image(image)) == digest
def admit(request: dict): """ Admits a request, parses all image names from it, validates the images, optionally mutates the request and sends back a response. The request is only allowed, if all images are successfully validated. """ uid = request["request"]["uid"] request_object = request["request"]["object"] object_kind = request_object["kind"] # container specifications differ from deployment objects, # Pod/Deployment/Job/... containers = get_container_specs(request_object) patches = [] # child resources have mutated image names, as their parents got mutated # before their creation. this may result in mismatch of rules or duplicate # lookups for already approved images. so child resources are automatically # approved without further check ups, when their parents were approved # earlier. acceptable_images = [] owner_references = request_object["metadata"].get("ownerReferences", []) namespace = request_object["metadata"].get("namespace", request["request"]["namespace"]) if owner_references: for index, owner in enumerate(owner_references): if owner["kind"] in ( "Pod", "Deployment", "ReplicationController", "ReplicaSet", "DaemonSet", "StatefulSet", "Job", "CronJob", ): acceptable_images += get_parent_images(request, index, namespace) policy = ImagePolicy() # validate all images from the request for index, container in enumerate(containers): try: logging_context = create_logging_context(request, container["image"]) # child approval if container["image"] in acceptable_images: msg = 'automatic child approval for "{}".'.format( container["image"]) logging.info(str({"message": msg, "context": logging_context})) continue image = Image(container["image"]) policy_rule = policy.get_matching_rule(image) verify = policy_rule.get("verify", True) # if image doesn't need verification, continue if not verify: msg = 'no verification for image "{}".'.format(str(image)) logging.info(str({"message": msg, "context": logging_context})) continue msg = 'start verification of image "{}".'.format(str(image)) logging.debug( str({ "message": msg, "context": dict(logging_context, matching_rule=policy_rule.get("pattern")), })) # get signed digest and update image reference with the digest trusted_digest = get_trusted_digest( os.environ.get("NOTARY_SERVER"), image, policy_rule) image.set_digest(trusted_digest) patches += [ get_json_patch(object_kind=object_kind, index=index, image_name=str(image)) ] msg = 'successful verification of image "{}"'.format(str(image)) logging.info(str({"message": msg, "context": logging_context})) except BaseConnaisseurException as err: err.context.update(logging_context) raise err return get_admission_review(uid, True, patch=patches)
def containers(self): return {(container_type, index): Image(container["image"]) for container_type in ["containers", "initContainers"] for index, container in enumerate( self.spec.get(container_type, []))}
def test_search_image_targets_for_tag(sample_nv1, image: str, digest: str): data = fix.get_td("sample_releases")["signed"]["targets"] assert (sample_nv1._NotaryV1Validator__search_image_targets_for_tag( data, Image(image)) == digest)
root_cert_pem = base64.b64decode(bytearray(root_cert_base64, "utf-8")) root_cert = x509.load_pem_x509_certificate(root_cert_pem, default_backend()) root_public_bytes = root_cert.public_key().public_bytes( Encoding.PEM, PublicFormat.SubjectPublicKeyInfo) root_public_key = root_public_bytes.decode("utf-8") return root_key_id, root_public_key if __name__ == "__main__": parser = argparse.ArgumentParser( description=("Gets the root public key and key ID " "from a notary server for a specific image.")) parser.add_argument( "--server", "-s", help="address of the notary server", type=str, default="notary.docker.io", ) parser.add_argument("--image", "-i", help="name of the image", type=str, required=True) args = parser.parse_args() root_key_id, root_key = asyncio.run( get_pub_root_key(args.server, Image(args.image))) print(f"KeyID: {root_key_id}\nKey: {root_key}")
def test_get_matching_rule(pol, mock_policy, image: str, rule: dict): p = pol.ImagePolicy() assert p.get_matching_rule(Image(image)) == rule
async def test_validate(approve, out, exception): with exception: val = st.StaticValidator("sample", approve) assert await val.validate(Image("sample")) == out
assert obj.kind == static_k8s[index]["kind"] assert obj.api_version == static_k8s[index]["apiVersion"] assert obj.namespace == static_k8s[index]["namespace"] assert obj.name == static_k8s[index]["name"] @pytest.mark.parametrize( "index, parent_list, exception", [ (0, {}, fix.no_exc()), ( 1, { ("containers", 0): Image("securesystemsengineering/charlie-image@sha256" ":91ac9b26df583762234c1cdb2fc930364754ccc59bc7" "52a2bfe298d2ea68f9ff"), }, fix.no_exc(), ), (2, {}, pytest.raises(exc.ParentNotFoundError)), (3, {}, fix.no_exc()), ], ) def test_k8s_object_parent_containers(adm_req_sample_objects, m_request, index, parent_list, exception): obj = wl.WorkloadObject(adm_req_sample_objects[index], "default") with exception: assert obj.parent_containers == parent_list