def revoke_user(self, user, author_rand):
     possible_authors = [
         device
         for device_id, device in self.local_devices.items()
         if device_id.user_id != user and device.profile == UserProfile.ADMIN
     ]
     author = possible_authors[author_rand % len(possible_authors)]
     note(f"revoke user: {user} (author: {author.device_id})")
     revoked_user = RevokedUserCertificateContent(
         author=author.device_id, timestamp=pendulum_now(), user_id=user
     )
     self.revoked_users_content[user] = revoked_user
     self.revoked_users_certifs[user] = revoked_user.dump_and_sign(author.signing_key)
     return user
async def test_backend_close_on_user_revoke(backend, alice_backend_sock,
                                            backend_sock_factory, bob, alice):
    now = pendulum_now()
    bob_revocation = RevokedUserCertificateContent(
        author=alice.device_id, timestamp=now,
        user_id=bob.user_id).dump_and_sign(alice.signing_key)

    async with backend_sock_factory(
            backend, bob, freeze_on_transport_error=False) as bob_backend_sock:
        with backend.event_bus.listen() as spy:
            rep = await user_revoke(alice_backend_sock,
                                    revoked_user_certificate=bob_revocation)
            assert rep == {"status": "ok"}
            await spy.wait_with_timeout(
                BackendEvent.USER_REVOKED,
                {
                    "organization_id": bob.organization_id,
                    "user_id": bob.user_id
                },
            )
            # `user.revoked` event schedules connection cancellation, so wait
            # for things to settle down to make sure the cancellation is done
            await trio.testing.wait_all_tasks_blocked()
        # Bob cannot send new command
        with pytest.raises(TransportError):
            await ping(bob_backend_sock)
Example #3
0
async def test_user_revoke_unknown(backend, alice_backend_sock, alice,
                                   mallory):
    revoked_user_certificate = RevokedUserCertificateContent(
        author=alice.device_id,
        timestamp=pendulum_now(),
        user_id=mallory.user_id).dump_and_sign(alice.signing_key)

    rep = await user_revoke(alice_backend_sock,
                            revoked_user_certificate=revoked_user_certificate)
    assert rep == {"status": "not_found"}
Example #4
0
 async def bind_revocation(self, user_id: UserID, certifier: LocalDevice):
     revoked_user_certificate = RevokedUserCertificateContent(
         author=certifier.device_id, timestamp=pendulum.now(), user_id=user_id
     ).dump_and_sign(certifier.signing_key)
     await self.backend.user.revoke_user(
         certifier.organization_id, user_id, revoked_user_certificate, certifier.device_id
     )
     self.certificates_store.store_revoked_user(
         certifier.organization_id, user_id, revoked_user_certificate
     )
Example #5
0
    async def api_user_revoke(self, client_ctx, msg):
        if client_ctx.profile != UserProfile.ADMIN:
            return {
                "status": "not_allowed",
                "reason":
                f"User `{client_ctx.device_id.user_id}` is not admin",
            }

        msg = user_revoke_serializer.req_load(msg)

        try:
            data = RevokedUserCertificateContent.verify_and_load(
                msg["revoked_user_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )

        except DataError as exc:
            return {
                "status": "invalid_certification",
                "reason": f"Invalid certification data ({exc}).",
            }

        if not timestamps_in_the_ballpark(data.timestamp, pendulum.now()):
            return {
                "status": "invalid_certification",
                "reason": f"Invalid timestamp in certification.",
            }

        if data.user_id == client_ctx.user_id:
            return {
                "status": "not_allowed",
                "reason": "Cannot do self-revocation"
            }

        try:
            await self.revoke_user(
                organization_id=client_ctx.organization_id,
                user_id=data.user_id,
                revoked_user_certificate=msg["revoked_user_certificate"],
                revoked_user_certifier=data.author,
                revoked_on=data.timestamp,
            )

        except UserNotFoundError:
            return {"status": "not_found"}

        except UserAlreadyRevokedError:
            return {
                "status": "already_revoked",
                "reason": f"User `{data.user_id}` already revoked"
            }

        return user_revoke_serializer.rep_dump({"status": "ok"})
Example #6
0
async def test_user_revoke_not_admin(backend, backend_sock_factory,
                                     bob_backend_sock, alice, bob):
    now = pendulum_now()
    alice_revocation = RevokedUserCertificateContent(
        author=bob.device_id, timestamp=now,
        user_id=alice.user_id).dump_and_sign(bob.signing_key)

    rep = await user_revoke(bob_backend_sock,
                            revoked_user_certificate=alice_revocation)
    assert rep == {
        "status": "not_allowed",
        "reason": "User `bob` is not admin"
    }
Example #7
0
async def test_user_revoke_invalid_certified(backend, alice_backend_sock,
                                             alice2, bob):
    revoked_user_certificate = RevokedUserCertificateContent(
        author=alice2.device_id, timestamp=pendulum_now(),
        user_id=bob.user_id).dump_and_sign(alice2.signing_key)

    rep = await user_revoke(alice_backend_sock,
                            revoked_user_certificate=revoked_user_certificate)
    assert rep == {
        "status": "invalid_certification",
        "reason":
        "Invalid certification data (Signature was forged or corrupt).",
    }
Example #8
0
async def test_cannot_self_revoke(backend, backend_sock_factory,
                                  alice_backend_sock, alice):
    now = pendulum_now()
    alice_revocation = RevokedUserCertificateContent(
        author=alice.device_id, timestamp=now,
        user_id=alice.user_id).dump_and_sign(alice.signing_key)

    rep = await user_revoke(alice_backend_sock,
                            revoked_user_certificate=alice_revocation)
    assert rep == {
        "status": "not_allowed",
        "reason": "Cannot do self-revocation"
    }
Example #9
0
def test_build_revoked_user_certificate(alice, bob, mallory):
    now = pendulum_now()
    certif = RevokedUserCertificateContent(author=alice.device_id,
                                           timestamp=now,
                                           user_id=bob.user_id).dump_and_sign(
                                               alice.signing_key)
    assert isinstance(certif, bytes)

    unsecure = RevokedUserCertificateContent.unsecure_load(certif)
    assert isinstance(unsecure, RevokedUserCertificateContent)
    assert unsecure.user_id == bob.user_id
    assert unsecure.timestamp == now
    assert unsecure.author == alice.device_id

    verified = RevokedUserCertificateContent.verify_and_load(
        certif,
        author_verify_key=alice.verify_key,
        expected_author=alice.device_id)
    assert verified == unsecure

    with pytest.raises(DataError) as exc:
        RevokedUserCertificateContent.verify_and_load(
            certif,
            author_verify_key=alice.verify_key,
            expected_author=mallory.device_id)
    assert str(exc.value
               ) == "Invalid author: expected `mallory@dev1`, got `alice@dev1`"

    with pytest.raises(DataError) as exc:
        RevokedUserCertificateContent.verify_and_load(
            certif,
            author_verify_key=mallory.verify_key,
            expected_author=alice.device_id)
    assert str(exc.value) == "Signature was forged or corrupt"

    with pytest.raises(DataError) as exc:
        RevokedUserCertificateContent.verify_and_load(
            certif,
            author_verify_key=alice.verify_key,
            expected_author=alice.device_id,
            expected_user=mallory.user_id,
        )
    assert str(exc.value) == "Invalid user ID: expected `mallory`, got `bob`"
Example #10
0
async def test_user_revoke_certify_too_old(backend, alice_backend_sock, alice,
                                           bob):
    now = Pendulum(2000, 1, 1)
    revoked_user_certificate = RevokedUserCertificateContent(
        author=alice.device_id, timestamp=now,
        user_id=bob.user_id).dump_and_sign(alice.signing_key)

    with freeze_time(now.add(seconds=INVITATION_VALIDITY + 1)):
        rep = await user_revoke(
            alice_backend_sock,
            revoked_user_certificate=revoked_user_certificate)
        assert rep == {
            "status": "invalid_certification",
            "reason": "Invalid timestamp in certification.",
        }
Example #11
0
async def test_handshake_revoked_device(running_backend, alice, bob):
    revoked_user_certificate = RevokedUserCertificateContent(
        author=alice.device_id, timestamp=pendulum.now(),
        user_id=bob.user_id).dump_and_sign(alice.signing_key)
    await running_backend.backend.user.revoke_user(
        organization_id=alice.organization_id,
        user_id=bob.user_id,
        revoked_user_certificate=revoked_user_certificate,
        revoked_user_certifier=alice.device_id,
    )

    with pytest.raises(BackendConnectionRefused) as exc:
        async with apiv1_backend_authenticated_cmds_factory(
                bob.organization_addr, bob.device_id, bob.signing_key) as cmds:
            await cmds.ping()
    assert str(exc.value) == "Device has been revoked"
Example #12
0
async def _do_revoke_user(core, user_name, button):
    try:
        now = pendulum.now()
        revoked_device_certificate = RevokedUserCertificateContent(
            author=core.device.device_id,
            timestamp=now,
            user_id=UserID(user_name)).dump_and_sign(core.device.signing_key)
        rep = await core.user_fs.backend_cmds.user_revoke(
            revoked_device_certificate)
        if rep["status"] != "ok":
            raise JobResultError(rep["status"])
        return button
    except BackendNotAvailable as exc:
        raise JobResultError("offline") from exc
    except BackendConnectionError as exc:
        raise JobResultError("error") from exc
Example #13
0
async def test_user_revoke_already_revoked(backend, alice_backend_sock, bob,
                                           alice):
    now = pendulum_now()
    bob_revocation = RevokedUserCertificateContent(
        author=alice.device_id, timestamp=now,
        user_id=bob.user_id).dump_and_sign(alice.signing_key)

    rep = await user_revoke(alice_backend_sock,
                            revoked_user_certificate=bob_revocation)
    assert rep["status"] == "ok"

    rep = await user_revoke(alice_backend_sock,
                            revoked_user_certificate=bob_revocation)
    assert rep == {
        "status": "already_revoked",
        "reason": f"User `{bob.user_id}` already revoked"
    }
Example #14
0
    async def revoke_user(self, user_id: UserID) -> None:
        """
        Raises:
            BackendConnectionError
        """
        now = pendulum_now()
        revoked_user_certificate = RevokedUserCertificateContent(
            author=self.device.device_id, timestamp=now, user_id=user_id
        ).dump_and_sign(self.device.signing_key)
        rep = await self._backend_conn.cmds.user_revoke(
            revoked_user_certificate=revoked_user_certificate
        )
        if rep["status"] != "ok":
            raise BackendConnectionError(f"Error while trying to revoke user {user_id}: {rep}")

        # Invalidate potential cache to avoid displaying the user as not-revoked
        self._remote_devices_manager.invalidate_user_cache(user_id)
Example #15
0
async def test_user_revoke_other_organization(
        sock_from_other_organization_factory, backend_sock_factory, backend,
        alice, bob):
    # Organizations should be isolated even for organization admins
    async with sock_from_other_organization_factory(
            backend, mimick=alice.device_id,
            profile=UserProfile.ADMIN) as sock:

        revocation = RevokedUserCertificateContent(
            author=sock.device.device_id,
            timestamp=pendulum_now(),
            user_id=bob.user_id).dump_and_sign(sock.device.signing_key)

        rep = await user_revoke(sock, revoked_user_certificate=revocation)
        assert rep == {"status": "not_found"}

    # Make sure bob still works
    async with backend_sock_factory(backend, bob):
        pass
Example #16
0
async def test_user_revoke_ok(backend, backend_sock_factory, adam_backend_sock,
                              alice, adam):
    now = pendulum_now()
    alice_revocation = RevokedUserCertificateContent(
        author=adam.device_id, timestamp=now,
        user_id=alice.user_id).dump_and_sign(adam.signing_key)

    with backend.event_bus.listen() as spy:
        rep = await user_revoke(adam_backend_sock,
                                revoked_user_certificate=alice_revocation)
        assert rep == {"status": "ok"}
        await spy.wait_with_timeout("user.revoked", {
            "organization_id": alice.organization_id,
            "user_id": alice.user_id
        })

    # Alice cannot connect from now on...
    with pytest.raises(HandshakeRevokedDevice):
        async with backend_sock_factory(backend, alice):
            pass
Example #17
0
    def load_trustchain(
        self,
        users: Sequence[bytes] = (),
        revoked_users: Sequence[bytes] = (),
        devices: Sequence[bytes] = (),
        now: DateTime = None,
    ) -> Tuple[
        List[UserCertificateContent],
        List[RevokedUserCertificateContent],
        List[DeviceCertificateContent],
    ]:
        now = now or pendulum_now()

        users_states = {}
        devices_states = {}
        revoked_users_states = {}

        # Deserialize the certificates and filter the ones we already have in cache
        try:
            for certif in devices:
                unverified_device = DeviceCertificateContent.unsecure_load(certif)
                verified_device = self.get_device(unverified_device.device_id, now)
                if verified_device:
                    devices_states[verified_device.device_id] = CertifState(
                        certif, verified_device, True
                    )
                else:
                    devices_states[unverified_device.device_id] = CertifState(
                        certif, unverified_device, False
                    )

            for certif in users:
                unverified_user = UserCertificateContent.unsecure_load(certif)
                verified_user = self.get_user(unverified_user.user_id, now)
                if verified_user:
                    users_states[verified_user.user_id] = CertifState(certif, verified_user, True)
                else:
                    users_states[unverified_user.user_id] = CertifState(
                        certif, unverified_user, False
                    )

            for certif in revoked_users:
                unverified_revoked_user = RevokedUserCertificateContent.unsecure_load(certif)
                verified_revoked_user = self.get_revoked_user(unverified_revoked_user.user_id, now)
                if verified_revoked_user:
                    revoked_users_states[verified_revoked_user.user_id] = CertifState(
                        certif, verified_revoked_user, True
                    )
                else:
                    revoked_users_states[unverified_revoked_user.user_id] = CertifState(
                        certif, unverified_revoked_user, False
                    )

        except DataError as exc:
            raise TrustchainError(f"Invalid certificate: {exc}") from exc

        def _get_eventually_verified_user(user_id):
            try:
                return users_states[user_id].content
            except KeyError:
                return None

        def _get_eventually_verified_revoked_user(user_id):
            try:
                return revoked_users_states[user_id].content
            except KeyError:
                return None

        def _verify_created_by_root(certif, certif_cls, sign_chain):
            try:
                return certif_cls.verify_and_load(
                    certif, author_verify_key=self.root_verify_key, expected_author=None
                )

            except DataError as exc:
                path = _build_signature_path(*sign_chain, "<Root Key>")
                raise TrustchainError(f"{path}: Invalid certificate: {exc}") from exc

        def _verify_created_by_device(certif, certif_cls, author_id, sign_chain):
            author_device = _recursive_verify_device(author_id, sign_chain)
            try:
                verified = certif_cls.verify_and_load(
                    certif,
                    author_verify_key=author_device.verify_key,
                    expected_author=author_device.device_id,
                )

            except DataError as exc:
                path = _build_signature_path(*sign_chain, author_id)
                raise TrustchainError(f"{path}: Invalid certificate: {exc}") from exc

            # Author is either admin or signing one of it own devices
            verified_user_id = (
                verified.device_id.user_id
                if isinstance(verified, DeviceCertificateContent)
                else verified.user_id
            )
            if author_device.device_id.user_id != verified_user_id:
                author_user = _get_eventually_verified_user(author_id.user_id)
                if not author_user:
                    path = _build_signature_path(*sign_chain, author_id)
                    raise TrustchainError(
                        f"{path}: Missing user certificate for {author_id.user_id}"
                    )
                elif author_user.profile != UserProfile.ADMIN:
                    path = _build_signature_path(*sign_chain, author_id)
                    raise TrustchainError(
                        f"{path}:  Invalid signature given {author_user.user_id} is not admin"
                    )
            # Also make sure author wasn't revoked at creation time
            author_revoked_user = _get_eventually_verified_revoked_user(author_id.user_id)
            if author_revoked_user and verified.timestamp > author_revoked_user.timestamp:
                path = _build_signature_path(*sign_chain, author_id)
                raise TrustchainError(
                    f"{path}: Signature ({verified.timestamp}) is posterior "
                    f"to user revocation ({author_revoked_user.timestamp})"
                )

            return verified

        def _recursive_verify_device(device_id, signed_children=()):
            if device_id in signed_children:
                path = _build_signature_path(*signed_children, device_id)
                raise TrustchainError(f"{path}: Invalid signature loop detected")

            try:
                state = devices_states[device_id]
            except KeyError:
                path = _build_signature_path(*signed_children, device_id)
                raise TrustchainError(f"{path}: Missing device certificate for {device_id}")

            author = state.content.author
            if author is None:
                verified_device = _verify_created_by_root(
                    state.certif, DeviceCertificateContent, sign_chain=(*signed_children, device_id)
                )
            else:
                verified_device = _verify_created_by_device(
                    state.certif,
                    DeviceCertificateContent,
                    author,
                    sign_chain=(*signed_children, device_id),
                )
            return verified_device

        def _verify_user(unverified_content, certif):
            author = unverified_content.author
            user_id = unverified_content.user_id
            if author is None:
                verified_user = _verify_created_by_root(
                    certif, UserCertificateContent, sign_chain=(f"{user_id}'s creation",)
                )
            elif author.user_id == user_id:
                raise TrustchainError(f"{user_id}: Invalid self-signed user certificate")
            else:
                verified_user = _verify_created_by_device(
                    certif, UserCertificateContent, author, sign_chain=(f"{user_id}'s creation",)
                )
            return verified_user

        def _verify_revoked_user(unverified_content, certif):
            author = unverified_content.author
            user_id = unverified_content.user_id
            if author is None:
                verified_revoked_user = _verify_created_by_root(
                    certif, RevokedUserCertificateContent, sign_chain=(f"{user_id}'s revocation",)
                )
            elif author.user_id == user_id:
                raise TrustchainError(f"{user_id}: Invalid self-signed user revocation certificate")
            else:
                verified_revoked_user = _verify_created_by_device(
                    certif,
                    RevokedUserCertificateContent,
                    author,
                    sign_chain=(f"{user_id}'s revocation",),
                )
            return verified_revoked_user

        # Verified what need to be and populate the cache with them

        for certif_state in devices_states.values():
            if not certif_state.verified:
                certif_state.content = _recursive_verify_device(certif_state.content.device_id)
        for certif_state in users_states.values():
            if not certif_state.verified:
                certif_state.content = _verify_user(certif_state.content, certif_state.certif)
        for certif_state in revoked_users_states.values():
            if not certif_state.verified:
                certif_state.content = _verify_revoked_user(
                    certif_state.content, certif_state.certif
                )

        # Finally populate the cache
        for certif_state in devices_states.values():
            if not certif_state.verified:
                self._devices_cache[certif_state.content.device_id] = (now, certif_state.content)
        for certif_state in users_states.values():
            if not certif_state.verified:
                self._users_cache[certif_state.content.user_id] = (now, certif_state.content)
        for certif_state in revoked_users_states.values():
            if not certif_state.verified:
                self._revoked_users_cache[certif_state.content.user_id] = (
                    now,
                    certif_state.content,
                )

        return (
            [state.content for state in users_states.values()],
            [state.content for state in revoked_users_states.values()],
            [state.content for state in devices_states.values()],
        )
Example #18
0
def test_unsecure_read_revoked_user_certificate_bad_data():
    with pytest.raises(DataError):
        RevokedUserCertificateContent.unsecure_load(b"dummy")
Example #19
0
def bob_revocation_from_alice(alice, bob):
    now = pendulum.now()
    return RevokedUserCertificateContent(author=alice.device_id,
                                         timestamp=now,
                                         user_id=bob.user_id).dump_and_sign(
                                             alice.signing_key)
Example #20
0
    def _trustchain_data_factory(todo_devices, todo_users):
        data = TrustchainData(coolorg.organization_id, coolorg.root_verify_key)

        def _get_certifier_id_and_key(certifier):
            if not certifier:
                # Certified by root
                certifier_id = None
                certifier_key = coolorg.root_signing_key
            else:
                certifier_ld = data.get_local_device(certifier)
                if not certifier_ld:
                    raise RuntimeError(
                        f"Missing `{certifier}` to sign creation of `{todo_device}`"
                    )
                certifier_id = certifier_ld.device_id
                certifier_key = certifier_ld.signing_key
            return certifier_id, certifier_key

        # First create all the devices
        for todo_device in todo_devices:
            local_device = local_device_factory(todo_device["id"], org=coolorg)
            data.add_local_device(local_device)
            # Generate device certificate
            certifier_id, certifier_key = _get_certifier_id_and_key(
                todo_device.get("certifier"))
            created_on = todo_device.get("created_on", now)
            device_certificate = DeviceCertificateContent(
                author=certifier_id,
                timestamp=created_on,
                device_id=local_device.device_id,
                device_label=local_device.device_label,
                verify_key=local_device.verify_key,
            )
            data.add_device_certif(
                device_certificate,
                device_certificate.dump_and_sign(certifier_key))

        # Now deal with the users
        for todo_user in todo_users:
            local_user = next((u for u in data.local_devices.values()
                               if str(u.user_id) == todo_user["id"]), None)
            if not local_user:
                raise RuntimeError(
                    f"Missing device for user `{todo_user['id']}`")
            # Generate user certificate
            certifier_id, certifier_key = _get_certifier_id_and_key(
                todo_user.get("certifier"))
            created_on = todo_user.get("created_on", now)
            user_certif = UserCertificateContent(
                author=certifier_id,
                timestamp=created_on,
                user_id=local_user.user_id,
                human_handle=local_device.human_handle,
                public_key=local_user.public_key,
                profile=todo_user.get("profile", UserProfile.STANDARD),
            )
            data.add_user_certif(user_certif,
                                 user_certif.dump_and_sign(certifier_key))
            # Generate user revocation certificate if needed
            revoker = todo_user.get("revoker", None)
            if revoker:
                revoked_on = todo_user.get("revoked_on", now)
                revoker_ld = data.get_local_device(revoker)
                if not revoker_ld:
                    raise RuntimeError(
                        f"Missing `{revoker}` to sign revocation of `{todo_user['id']}`"
                    )
                revoked_user_certificate = RevokedUserCertificateContent(
                    author=revoker_ld.device_id,
                    timestamp=revoked_on,
                    user_id=local_user.user_id)
                data.add_revoked_user_certif(
                    revoked_user_certificate,
                    revoked_user_certificate.dump_and_sign(
                        revoker_ld.signing_key),
                )

        return data