Example #1
0
def test_app_bundle(network, args):
    primary, _ = network.find_nodes()

    LOG.info("Deploying js app bundle archive")
    # Testing the bundle archive support of the Python client here.
    # Plain bundle folders are tested in the npm-based app tests.
    bundle_dir = os.path.join(PARENT_DIR, "js-app-bundle")
    raw_module_name = "/math.js".encode()
    with tempfile.TemporaryDirectory(prefix="ccf") as tmp_dir:
        bundle_path = shutil.make_archive(os.path.join(tmp_dir, "bundle"),
                                          "zip", bundle_dir)
        set_js_proposal = network.consortium.set_js_app_from_dir(
            primary, bundle_path)

        assert (raw_module_name in network.get_ledger_public_state_at(
            set_js_proposal.completed_seqno)["public:ccf.gov.modules"]
                ), "Module was not added"

    LOG.info("Verifying that app was deployed")

    with primary.client("user0") as c:
        valid_body = {"op": "sub", "left": 82, "right": 40}
        r = c.post("/app/compute", valid_body)
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.headers["content-type"] == "application/json"
        assert r.body.json() == {"result": 42}, r.body

        invalid_body = {"op": "add", "left": "1", "right": 2}
        r = c.post("/app/compute", invalid_body)
        assert r.status_code == http.HTTPStatus.BAD_REQUEST, r.status_code
        assert r.headers["content-type"] == "application/json"
        assert r.body.json() == {"error": "invalid operand type"}, r.body

        validate_openapi(c)

    LOG.info("Removing js app")
    remove_js_proposal = network.consortium.remove_js_app(primary)

    LOG.info("Verifying that modules and endpoints were removed")
    with primary.client("user0") as c:
        r = c.post("/app/compute", valid_body)
        assert r.status_code == http.HTTPStatus.NOT_FOUND, r.status_code

    assert (network.get_ledger_public_state_at(
        remove_js_proposal.completed_seqno)["public:ccf.gov.modules"]
            [raw_module_name] is None), "Module was not removed"

    return network
Example #2
0
def test_jwt_with_sgx_key_filter(network, args):
    primary, _ = network.find_nodes()

    oe_cert_path = os.path.join(this_dir, "oe_cert.pem")
    with open(oe_cert_path) as f:
        oe_cert_pem = f.read()
    oe_kid = "oe_kid"

    key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
    non_oe_cert_pem = infra.crypto.generate_cert(key_priv_pem)
    non_oe_kid = "non_oe_kid"

    issuer = "my_issuer"

    LOG.info("Add JWT issuer with SGX key filter")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
        json.dump({"issuer": issuer, "key_filter": "sgx"}, metadata_fp)
        metadata_fp.flush()
        network.consortium.set_jwt_issuer(primary, metadata_fp.name)

    LOG.info("Add multiple certs (1 SGX, 1 non-SGX)")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
        oe_jwks = create_jwks(oe_kid, oe_cert_pem)
        non_oe_jwks = create_jwks(non_oe_kid, non_oe_cert_pem)
        jwks = {"keys": non_oe_jwks["keys"] + oe_jwks["keys"]}
        json.dump(jwks, jwks_fp)
        jwks_fp.flush()
        set_jwt_proposal = network.consortium.set_jwt_public_signing_keys(
            primary, issuer, jwks_fp.name
        )

        stored_jwt_signing_keys = network.get_ledger_public_state_at(
            set_jwt_proposal.completed_seqno
        )["public:ccf.gov.jwt.public_signing_keys"]

        assert non_oe_kid.encode() not in stored_jwt_signing_keys
        assert oe_kid.encode() in stored_jwt_signing_keys

    return network
Example #3
0
def test_cert_store(network, args):
    primary, _ = network.find_nodes()

    cert_name = "mycert"
    raw_cert_name = cert_name.encode()

    LOG.info("Member builds a ca cert update proposal with malformed cert")
    with tempfile.NamedTemporaryFile("w") as f:
        f.write("foo")
        f.flush()
        try:
            ccf.proposal_generator.set_ca_cert_bundle(cert_name, f.name)
        except ValueError:
            pass
        else:
            assert False, "set_ca_cert_bundle should have raised an error"

    LOG.info("Member makes a ca cert update proposal with malformed cert")
    with tempfile.NamedTemporaryFile("w") as f:
        f.write("foo")
        f.flush()
        try:
            network.consortium.set_ca_cert_bundle(
                primary, cert_name, f.name, skip_checks=True
            )
        except (infra.proposal.ProposalNotAccepted, infra.proposal.ProposalNotCreated):
            pass
        else:
            assert False, "Proposal should not have been accepted"

    LOG.info("Member makes a ca cert update proposal with valid certs")
    key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
    cert_pem = infra.crypto.generate_cert(key_priv_pem)
    key2_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
    cert2_pem = infra.crypto.generate_cert(key2_priv_pem)
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as cert_pem_fp:
        cert_pem_fp.write(cert_pem)
        cert_pem_fp.write(cert2_pem)
        cert_pem_fp.flush()
        set_proposal = network.consortium.set_ca_cert_bundle(
            primary, cert_name, cert_pem_fp.name
        )

        stored_cert = json.loads(
            network.get_ledger_public_state_at(set_proposal.completed_seqno)[
                "public:ccf.gov.tls.ca_cert_bundles"
            ][raw_cert_name]
        )
        cert_ref = cert_pem + cert2_pem
        assert (
            cert_ref == stored_cert
        ), f"input certs not equal to stored cert: {cert_ref} != {stored_cert}"

    LOG.info("Member removes a ca cert")
    remove_proposal = network.consortium.remove_ca_cert_bundle(primary, cert_name)

    assert (
        network.get_ledger_public_state_at(remove_proposal.completed_seqno)[
            "public:ccf.gov.tls.ca_cert_bundles"
        ][raw_cert_name]
        == None
    ), "CA bundle was not removed"

    return network
Example #4
0
def test_npm_app(network, args):
    primary, _ = network.find_nodes()

    LOG.info("Building ccf-app npm package (dependency)")
    ccf_pkg_dir = os.path.join(PARENT_DIR, "..", "js", "ccf-app")
    subprocess.run(["npm", "install", "--no-package-lock"],
                   cwd=ccf_pkg_dir,
                   check=True)

    LOG.info("Running ccf-app unit tests")
    subprocess.run(["npm", "test"], cwd=ccf_pkg_dir, check=True)

    LOG.info("Building npm app")
    app_dir = os.path.join(PARENT_DIR, "npm-app")
    assert infra.proc.ccall("npm", "install", path=app_dir).returncode == 0
    assert (infra.proc.ccall("npm", "run", "build", "--verbose",
                             path=app_dir).returncode == 0)

    LOG.info("Deploying npm app")
    bundle_path = os.path.join(
        app_dir, "dist",
        "bundle.json")  # Produced by build step of test npm-app
    network.consortium.set_js_app_from_json(primary, bundle_path)

    LOG.info("Calling npm app endpoints")
    with primary.client("user0") as c:
        body = [1, 2, 3, 4]
        r = c.post("/app/partition", body)
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.body.json() == [[1, 3], [2, 4]], r.body

        r = c.post("/app/proto", body)
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.headers["content-type"] == "application/x-protobuf"
        # We could now decode the protobuf message but given all the machinery
        # involved to make it happen (code generation with protoc) we'll leave it at that.
        assert len(r.body) == 14, len(r.body)

        r = c.get("/app/crypto")
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.body.json()["available"], r.body

        key_size = 256
        r = c.post("/app/generateAesKey", {"size": key_size})
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert len(r.body.data()) == key_size // 8
        assert r.body.data() != b"\x00" * (key_size // 8)

        r = c.post("/app/generateRsaKeyPair", {"size": 2048})
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert infra.crypto.check_key_pair_pem(r.body.json()["privateKey"],
                                               r.body.json()["publicKey"])

        aes_key_to_wrap = infra.crypto.generate_aes_key(256)
        wrapping_key_priv_pem, wrapping_key_pub_pem = infra.crypto.generate_rsa_keypair(
            2048)
        label = "label42"
        r = c.post(
            "/app/wrapKey",
            {
                "key":
                b64encode(aes_key_to_wrap).decode(),
                "wrappingKey":
                b64encode(bytes(wrapping_key_pub_pem, "ascii")).decode(),
                "wrapAlgo": {
                    "name": "RSA-OAEP",
                    "label": b64encode(bytes(label, "ascii")).decode(),
                },
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        unwrapped = infra.crypto.unwrap_key_rsa_oaep(r.body.data(),
                                                     wrapping_key_priv_pem,
                                                     label.encode("ascii"))
        assert unwrapped == aes_key_to_wrap

        aes_wrapping_key = infra.crypto.generate_aes_key(256)
        r = c.post(
            "/app/wrapKey",
            {
                "key": b64encode(aes_key_to_wrap).decode(),
                "wrappingKey": b64encode(aes_wrapping_key).decode(),
                "wrapAlgo": {
                    "name": "AES-KWP"
                },
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        unwrapped = infra.crypto.unwrap_key_aes_pad(r.body.data(),
                                                    aes_wrapping_key)
        assert unwrapped == aes_key_to_wrap

        wrapping_key_priv_pem, wrapping_key_pub_pem = infra.crypto.generate_rsa_keypair(
            2048)
        label = "label44"
        r = c.post(
            "/app/wrapKey",
            {
                "key":
                b64encode(aes_key_to_wrap).decode(),
                "wrappingKey":
                b64encode(bytes(wrapping_key_pub_pem, "ascii")).decode(),
                "wrapAlgo": {
                    "name": "RSA-OAEP-AES-KWP",
                    "aesKeySize": 256,
                    "label": b64encode(bytes(label, "ascii")).decode(),
                },
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        unwrapped = infra.crypto.unwrap_key_rsa_oaep_aes_pad(
            r.body.data(), wrapping_key_priv_pem, label.encode("ascii"))
        assert unwrapped == aes_key_to_wrap

        key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048)
        algorithm = {"name": "RSASSA-PKCS1-v1_5", "hash": "SHA-256"}
        data = "foo".encode()
        signature = infra.crypto.sign(algorithm, key_priv_pem, data)
        r = c.post(
            "/app/verifySignature",
            {
                "algorithm": algorithm,
                "key": key_pub_pem,
                "signature": b64encode(signature).decode(),
                "data": b64encode(data).decode(),
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.body.json() == True, r.body

        r = c.post(
            "/app/verifySignature",
            {
                "algorithm": algorithm,
                "key": key_pub_pem,
                "signature": b64encode(signature).decode(),
                "data": b64encode("bar".encode()).decode(),
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.body.json() == False, r.body

        key_priv_pem, key_pub_pem = infra.crypto.generate_ec_keypair(
            "secp256r1")
        algorithm = {"name": "ECDSA", "hash": "SHA-256"}
        data = "foo".encode()
        signature = infra.crypto.sign(algorithm, key_priv_pem, data)
        r = c.post(
            "/app/verifySignature",
            {
                "algorithm": algorithm,
                "key": key_pub_pem,
                "signature": b64encode(signature).decode(),
                "data": b64encode(data).decode(),
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert r.body.json() == True, r.body

        r = c.post(
            "/app/digest",
            {
                "algorithm": "SHA-256",
                "data": b64encode(bytes("Hello world!", "ascii")).decode(),
            },
        )
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        assert (r.body.text(
        ) == "c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a"
                ), r.body

        r = c.get("/app/log?id=42")
        assert r.status_code == http.HTTPStatus.NOT_FOUND, r.status_code

        r = c.post("/app/log?id=42", {"msg": "Hello!"})
        assert r.status_code == http.HTTPStatus.OK, r.status_code

        r = c.get("/app/log?id=42")
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        body = r.body.json()
        assert body["msg"] == "Hello!", r.body

        r = c.post("/app/log?id=42", {"msg": "Saluton!"})
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        r = c.post("/app/log?id=43", {"msg": "Bonjour!"})
        assert r.status_code == http.HTTPStatus.OK, r.status_code

        r = c.get("/app/log/all")
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        body = r.body.json()
        # Response is list in undefined order
        assert len(body) == 2, body
        assert {"id": 42, "msg": "Saluton!"} in body, body
        assert {"id": 43, "msg": "Bonjour!"} in body, body

        r = c.post("/app/rpc/apply_writes")
        assert r.status_code == http.HTTPStatus.BAD_REQUEST, r.status_code
        val = network.get_ledger_public_state_at(
            r.seqno)["public:apply_writes"]["foo".encode()]
        assert val == b"bar", val

        r = c.get("/app/jwt")
        assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
        body = r.body.json()
        assert body["msg"] == "authorization header missing", r.body

        r = c.get("/app/jwt", headers={"authorization": "Bearer not-a-jwt"})
        assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
        body = r.body.json()
        assert body["msg"].startswith("malformed jwt:"), r.body

        jwt_key_priv_pem, _ = infra.crypto.generate_rsa_keypair(2048)
        jwt_cert_pem = infra.crypto.generate_cert(jwt_key_priv_pem)

        jwt_kid = "my_key_id"
        jwt = infra.crypto.create_jwt({}, jwt_key_priv_pem, jwt_kid)
        r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
        assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
        body = r.body.json()
        assert body["msg"].startswith("token signing key not found"), r.body

        priv_key_pem, _ = infra.crypto.generate_rsa_keypair(2048)
        pem = infra.crypto.generate_cert(priv_key_pem)
        r = c.post("/app/isValidX509CertBundle", pem)
        assert r.body.json(), r.body
        r = c.post("/app/isValidX509CertBundle", pem + "\n" + pem)
        assert r.body.json(), r.body
        r = c.post("/app/isValidX509CertBundle", "garbage")
        assert not r.body.json(), r.body

        priv_key_pem1, _ = infra.crypto.generate_rsa_keypair(2048)
        pem1 = infra.crypto.generate_cert(priv_key_pem1, cn="1", ca=True)
        priv_key_pem2, _ = infra.crypto.generate_rsa_keypair(2048)
        pem2 = infra.crypto.generate_cert(
            priv_key_pem2,
            cn="2",
            ca=True,
            issuer_priv_key_pem=priv_key_pem1,
            issuer_cn="1",
        )
        priv_key_pem3, _ = infra.crypto.generate_rsa_keypair(2048)
        pem3 = infra.crypto.generate_cert(priv_key_pem3,
                                          cn="3",
                                          issuer_priv_key_pem=priv_key_pem2,
                                          issuer_cn="2")
        # validates chains with target being trusted directly
        r = c.post("/app/isValidX509CertChain", {
            "chain": pem3,
            "trusted": pem3
        })
        assert r.body.json(), r.body
        # validates chains without intermediates
        r = c.post("/app/isValidX509CertChain", {
            "chain": pem2,
            "trusted": pem1
        })
        assert r.body.json(), r.body
        # validates chains with intermediates
        r = c.post("/app/isValidX509CertChain", {
            "chain": pem3 + "\n" + pem2,
            "trusted": pem1
        })
        assert r.body.json(), r.body
        # validates partial chains (pem2 is an intermediate)
        r = c.post("/app/isValidX509CertChain", {
            "chain": pem3,
            "trusted": pem2
        })
        assert r.body.json(), r.body
        # fails to reach trust anchor
        r = c.post("/app/isValidX509CertChain", {
            "chain": pem3,
            "trusted": pem1
        })
        assert not r.body.json(), r.body

        r = c.get("/node/quotes/self")
        primary_quote_info = r.body.json()
        if not primary_quote_info["raw"]:
            LOG.info(
                "Skipping /app/verifyOpenEnclaveEvidence test, virtual mode")
        elif args.package == "libjs_v8":
            LOG.info("Skipping /app/verifyOpenEnclaveEvidence test, V8")
        else:
            # See /opt/openenclave/include/openenclave/attestation/sgx/evidence.h
            OE_FORMAT_UUID_SGX_ECDSA = "a3a21e87-1b4d-4014-b70a-a125d2fbcd8c"
            r = c.post(
                "/app/verifyOpenEnclaveEvidence",
                {
                    "format": OE_FORMAT_UUID_SGX_ECDSA,
                    "evidence": primary_quote_info["raw"],
                    "endorsements": primary_quote_info["endorsements"],
                },
            )
            assert r.status_code == http.HTTPStatus.OK, r.status_code
            body = r.body.json()
            assert body["claims"]["unique_id"] == primary_quote_info[
                "mrenclave"], body
            assert "sgx_report_data" in body["customClaims"], body

            # again but without endorsements
            r = c.post(
                "/app/verifyOpenEnclaveEvidence",
                {
                    "format": OE_FORMAT_UUID_SGX_ECDSA,
                    "evidence": primary_quote_info["raw"],
                },
            )
            assert r.status_code == http.HTTPStatus.OK, r.status_code
            body = r.body.json()
            assert body["claims"]["unique_id"] == primary_quote_info[
                "mrenclave"], body
            assert "sgx_report_data" in body["customClaims"], body

        validate_openapi(c)

    LOG.info("Store JWT signing keys")

    issuer = "https://example.issuer"
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
        jwt_cert_der = infra.crypto.cert_pem_to_der(jwt_cert_pem)
        der_b64 = base64.b64encode(jwt_cert_der).decode("ascii")
        data = {
            "issuer": issuer,
            "jwks": {
                "keys": [{
                    "kty": "RSA",
                    "kid": jwt_kid,
                    "x5c": [der_b64]
                }]
            },
        }
        json.dump(data, metadata_fp)
        metadata_fp.flush()
        network.consortium.set_jwt_issuer(primary, metadata_fp.name)

    LOG.info("Calling jwt endpoint after storing keys")
    with primary.client("user0") as c:
        jwt_mismatching_key_priv_pem, _ = infra.crypto.generate_rsa_keypair(
            2048)
        jwt = infra.crypto.create_jwt({}, jwt_mismatching_key_priv_pem,
                                      jwt_kid)
        r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
        assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
        body = r.body.json()
        assert body["msg"] == "jwt validation failed", r.body

        jwt = infra.crypto.create_jwt({}, jwt_key_priv_pem, jwt_kid)
        r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
        assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
        body = r.body.json()
        assert body["msg"] == "jwt invalid, sub claim missing", r.body

        user_id = "user0"
        jwt = infra.crypto.create_jwt({"sub": user_id}, jwt_key_priv_pem,
                                      jwt_kid)
        r = c.get("/app/jwt", headers={"authorization": "Bearer " + jwt})
        assert r.status_code == http.HTTPStatus.OK, r.status_code
        body = r.body.json()
        assert body["userId"] == user_id, r.body

    return network
Example #5
0
def test_jwt_without_key_policy(network, args):
    primary, _ = network.find_nodes()

    key_priv_pem, key_pub_pem = infra.crypto.generate_rsa_keypair(2048)
    cert_pem = infra.crypto.generate_cert(key_priv_pem)
    kid = "my_kid"
    issuer = "my_issuer"
    raw_kid = kid.encode()

    LOG.info("Try to add JWT signing key without matching issuer")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
        json.dump(create_jwks(kid, cert_pem), jwks_fp)
        jwks_fp.flush()
        try:
            network.consortium.set_jwt_public_signing_keys(
                primary, issuer, jwks_fp.name
            )
        except infra.proposal.ProposalNotAccepted:
            pass
        else:
            assert False, "Proposal should not have been created"

    LOG.info("Add JWT issuer")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
        json.dump({"issuer": issuer}, metadata_fp)
        metadata_fp.flush()
        network.consortium.set_jwt_issuer(primary, metadata_fp.name)

    LOG.info("Try to add a public key instead of a certificate")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
        json.dump(create_jwks(kid, key_pub_pem, test_invalid_is_key=True), jwks_fp)
        jwks_fp.flush()
        try:
            network.consortium.set_jwt_public_signing_keys(
                primary, issuer, jwks_fp.name
            )
        except (infra.proposal.ProposalNotAccepted, infra.proposal.ProposalNotCreated):
            pass
        else:
            assert False, "Proposal should not have been created"

    LOG.info("Add JWT signing key with matching issuer")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as jwks_fp:
        json.dump(create_jwks(kid, cert_pem), jwks_fp)
        jwks_fp.flush()
        set_jwt_proposal = network.consortium.set_jwt_public_signing_keys(
            primary, issuer, jwks_fp.name
        )

        stored_jwt_signing_key = network.get_ledger_public_state_at(
            set_jwt_proposal.completed_seqno
        )["public:ccf.gov.jwt.public_signing_keys"][raw_kid]

        stored_cert = infra.crypto.cert_der_to_pem(stored_jwt_signing_key)
        assert infra.crypto.are_certs_equal(
            cert_pem, stored_cert
        ), "input cert is not equal to stored cert"

    LOG.info("Remove JWT issuer")
    remove_jwt_proposal = network.consortium.remove_jwt_issuer(primary, issuer)

    assert (
        network.get_ledger_public_state_at(remove_jwt_proposal.completed_seqno)[
            "public:ccf.gov.jwt.public_signing_keys"
        ][raw_kid]
        is None
    ), "JWT issuer was not removed"

    LOG.info("Add JWT issuer with initial keys")
    with tempfile.NamedTemporaryFile(prefix="ccf", mode="w+") as metadata_fp:
        json.dump({"issuer": issuer, "jwks": create_jwks(kid, cert_pem)}, metadata_fp)
        metadata_fp.flush()
        set_jwt_issuer = network.consortium.set_jwt_issuer(primary, metadata_fp.name)

        stored_jwt_signing_key = network.get_ledger_public_state_at(
            set_jwt_issuer.completed_seqno
        )["public:ccf.gov.jwt.public_signing_keys"][raw_kid]

        stored_cert = infra.crypto.cert_der_to_pem(stored_jwt_signing_key)
        assert infra.crypto.are_certs_equal(
            cert_pem, stored_cert
        ), "input cert is not equal to stored cert"

    return network