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)
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"}
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 )
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"})
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" }
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).", }
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" }
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`"
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.", }
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"
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
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" }
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)
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
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
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()], )
def test_unsecure_read_revoked_user_certificate_bad_data(): with pytest.raises(DataError): RevokedUserCertificateContent.unsecure_load(b"dummy")
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)
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