async def claim_device(cmds: BackendCmdsPool, new_device_id: DeviceID, token: str) -> LocalDevice: """ Raises: InviteClaimError core.backend_connection.BackendConnectionError core.trustchain.TrustChainError """ device_signing_key = SigningKey.generate() answer_private_key = PrivateKey.generate() invitation_creator = await cmds.device_get_invitation_creator(new_device_id ) encrypted_claim = generate_device_encrypted_claim( creator_public_key=invitation_creator.public_key, token=token, device_id=new_device_id, verify_key=device_signing_key.verify_key, answer_public_key=answer_private_key.public_key, ) encrypted_answer = await cmds.device_claim(new_device_id, encrypted_claim) answer = extract_device_encrypted_answer(answer_private_key, encrypted_answer) return LocalDevice( organization_addr=cmds.addr, device_id=new_device_id, signing_key=device_signing_key, private_key=answer["private_key"], user_manifest_access=answer["user_manifest_access"], local_symkey=generate_secret_key(), )
async def _do_wait_peer(self) -> Tuple[SASCode, SASCode, SecretKey]: claimer_private_key = PrivateKey.generate() rep = await self._cmds.invite_1_claimer_wait_peer( claimer_public_key=claimer_private_key.public_key) _check_rep(rep, step_name="step 1") shared_secret_key = generate_shared_secret_key( our_private_key=claimer_private_key, peer_public_key=rep["greeter_public_key"]) claimer_nonce = generate_nonce() rep = await self._cmds.invite_2a_claimer_send_hashed_nonce( claimer_hashed_nonce=HashDigest.from_data(claimer_nonce)) _check_rep(rep, step_name="step 2a") claimer_sas, greeter_sas = generate_sas_codes( claimer_nonce=claimer_nonce, greeter_nonce=rep["greeter_nonce"], shared_secret_key=shared_secret_key, ) rep = await self._cmds.invite_2b_claimer_send_nonce( claimer_nonce=claimer_nonce) _check_rep(rep, step_name="step 2b") return claimer_sas, greeter_sas, shared_secret_key
def generate_BOB_local_device(): return LocalDevice( organization_addr=BackendOrganizationAddr.from_url( "parsec://bob_dev1.example.com:9999/CoolOrg?no_ssl=true&rvk=XYUXM4ZM5SGKSTXNZ4FK7VATZUKZGY7A7LOJ42CXFR32DYL5TO6Qssss" ), device_id=DeviceID("bob@dev1"), device_label=DeviceLabel("My dev1 machine"), human_handle=HumanHandle("*****@*****.**", "Boby McBobFace"), signing_key=SigningKey( unhexlify( "85f47472a2c0f30f01b769617db248f3ec8d96a490602a9262f95e9e43432b30" )), private_key=PrivateKey( unhexlify( "16767ec446f2611f971c36f19c2dc11614d853475ac395d6c1d70ba46d07dd49" )), profile=UserProfile.STANDARD, user_manifest_id=EntryID.from_hex("71568d41afcb4e2380b3d164ace4fb85"), user_manifest_key=SecretKey( unhexlify( "65de53d2c6cd965aa53a1ba5cc7e54b331419e6103466121996fa99a97197a48" )), local_symkey=SecretKey( unhexlify( "93f25b18491016f20b10dcf4eb7986716d914653d6ab4e778701c13435e6bdf0" )), )
async def _alice_nd_claim(): async with apiv1_backend_anonymous_cmds_factory(alice.organization_addr) as cmds: ret = await cmds.device_get_invitation_creator(nd_id) assert ret["status"] == "ok" assert ret["trustchain"] == {"devices": [], "revoked_users": [], "users": []} creator = UserCertificateContent.unsecure_load(ret["user_certificate"]) creator_device = DeviceCertificateContent.unsecure_load(ret["device_certificate"]) assert creator_device.device_id.user_id == creator.user_id answer_private_key = PrivateKey.generate() encrypted_claim = APIV1_DeviceClaimContent( token=token, device_id=nd_id, verify_key=nd_signing_key.verify_key, answer_public_key=answer_private_key.public_key, ).dump_and_encrypt_for(recipient_pubkey=creator.public_key) with trio.fail_after(1): ret = await cmds.device_claim(nd_id, encrypted_claim) assert ret["status"] == "ok" assert ret["device_certificate"] == device_certificate answer = APIV1_DeviceClaimAnswerContent.decrypt_and_load_for( ret["encrypted_answer"], recipient_privkey=answer_private_key ) assert answer == APIV1_DeviceClaimAnswerContent( private_key=alice.private_key, user_manifest_id=alice.user_manifest_id, user_manifest_key=alice.user_manifest_key, )
def generate_ALICE_local_device(): return LocalDevice( organization_addr=BackendOrganizationAddr.from_url( "parsec://alice_dev1.example.com:9999/CoolOrg?no_ssl=true&rvk=XYUXM4ZM5SGKSTXNZ4FK7VATZUKZGY7A7LOJ42CXFR32DYL5TO6Qssss" ), device_id=DeviceID("alice@dev1"), device_label=DeviceLabel("My dev1 machine"), human_handle=HumanHandle("*****@*****.**", "Alicey McAliceFace"), signing_key=SigningKey( unhexlify( "d544f66ece9c85d5b80275db9124b5f04bb038081622bed139c1e789c5217400" )), private_key=PrivateKey( unhexlify( "74e860967fd90d063ebd64fb1ba6824c4c010099dd37508b7f2875a5db2ef8c9" )), profile=UserProfile.ADMIN, user_manifest_id=EntryID.from_hex("a4031e8bcdd84df8ae12bd3d05e6e20f"), user_manifest_key=SecretKey( unhexlify( "26bf35a98c1e54e90215e154af92a1af2d1142cdd0dba25b990426b0b30b0f9a" )), local_symkey=SecretKey( unhexlify( "125a78618995e2e0f9a19bc8617083c809c03deb5457d5b82df5bcaec9966cd4" )), )
async def _do_wait_peer(self) -> Tuple[SASCode, SASCode, SecretKey]: claimer_private_key = PrivateKey.generate() rep = await self._cmds.invite_1_claimer_wait_peer( claimer_public_key=claimer_private_key.public_key) if rep["status"] != "ok": raise InviteError(f"Backend error during step 1: {rep}") shared_secret_key = generate_shared_secret_key( our_private_key=claimer_private_key, peer_public_key=rep["greeter_public_key"]) claimer_nonce = generate_nonce() rep = await self._cmds.invite_2a_claimer_send_hashed_nonce( claimer_hashed_nonce=HashDigest.from_data(claimer_nonce)) if rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError(f"Backend error during step 2a: {rep}") claimer_sas, greeter_sas = generate_sas_codes( claimer_nonce=claimer_nonce, greeter_nonce=rep["greeter_nonce"], shared_secret_key=shared_secret_key, ) rep = await self._cmds.invite_2b_claimer_send_nonce( claimer_nonce=claimer_nonce) if rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError(f"Backend error during step 2b: {rep}") return claimer_sas, greeter_sas, shared_secret_key
def decrypt_verify_and_load_for( cls: Type[BaseSignedDataTypeVar], encrypted: bytes, recipient_privkey: PrivateKey, author_verify_key: VerifyKey, expected_author: DeviceID, expected_timestamp: DateTime, **kwargs, ) -> BaseSignedDataTypeVar: """ Raises: DataError """ try: signed = recipient_privkey.decrypt_from_self(encrypted) except CryptoError as exc: raise DataError(str(exc)) from exc return cls.verify_and_load( signed, author_verify_key=author_verify_key, expected_author=expected_author, expected_timestamp=expected_timestamp, **kwargs, )
async def test_greeter_exchange_bad_access(alice, backend, alice_backend_sock, reason): if reason == "deleted_invitation": invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id ) await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation.token, on=datetime(2000, 1, 2), reason=InvitationDeletedReason.ROTTEN, ) token = invitation.token status = "already_deleted" else: assert reason == "unknown_token" token = InvitationToken.new() status = "not_found" greeter_privkey = PrivateKey.generate() for command, params in [ ( invite_1_greeter_wait_peer, {"token": token, "greeter_public_key": greeter_privkey.public_key}, ), (invite_2a_greeter_get_hashed_nonce, {"token": token}), (invite_2b_greeter_send_nonce, {"token": token, "greeter_nonce": b"<greeter_nonce>"}), (invite_3a_greeter_wait_peer_trust, {"token": token}), (invite_3b_greeter_signify_trust, {"token": token}), (invite_4_greeter_communicate, {"token": token, "payload": b"<payload>"}), ]: async with real_clock_timeout(): rep = await command(alice_backend_sock, **params) assert rep == {"status": status}
async def do_claim_user( self, requested_device_label: Optional[DeviceLabel], requested_human_handle: Optional[HumanHandle], ) -> LocalDevice: # User&device keys are generated here and kept in memory until the end of # the enrollment process. This mean we can lost it if something goes wrong. # This has no impact until step 4 (somewhere between data exchange and # confirmation exchange steps) where greeter upload our certificates in # the server. # This is considered acceptable given 1) the error window is small and # 2) if this occurs the inviter can revoke the user and retry the # enrollment process to fix this private_key = PrivateKey.generate() signing_key = SigningKey.generate() try: payload = InviteUserData( requested_device_label=requested_device_label, requested_human_handle=requested_human_handle, public_key=private_key.public_key, verify_key=signing_key.verify_key, ).dump_and_encrypt(key=self._shared_secret_key) except DataError as exc: raise InviteError( "Cannot generate InviteUserData payload") from exc rep = await self._cmds.invite_4_claimer_communicate(payload=payload) _check_rep(rep, step_name="step 4 (data exchange)") rep = await self._cmds.invite_4_claimer_communicate(payload=b"") _check_rep(rep, step_name="step 4 (confirmation exchange)") try: confirmation = InviteUserConfirmation.decrypt_and_load( rep["payload"], key=self._shared_secret_key) except DataError as exc: raise InviteError( "Invalid InviteUserConfirmation payload provided by peer" ) from exc organization_addr = BackendOrganizationAddr.build( backend_addr=self._cmds.addr.get_backend_addr(), organization_id=self._cmds.addr.organization_id, root_verify_key=confirmation.root_verify_key, ) new_device = generate_new_device( organization_addr=organization_addr, device_id=confirmation.device_id, device_label=confirmation.device_label, human_handle=confirmation.human_handle, profile=confirmation.profile, private_key=private_key, signing_key=signing_key, ) return new_device
async def do_claim_user( self, requested_device_label: Optional[str], requested_human_handle: Optional[HumanHandle]) -> LocalDevice: private_key = PrivateKey.generate() signing_key = SigningKey.generate() try: payload = InviteUserData( requested_device_label=requested_device_label, requested_human_handle=requested_human_handle, public_key=private_key.public_key, verify_key=signing_key.verify_key, ).dump_and_encrypt(key=self._shared_secret_key) except DataError as exc: raise InviteError( "Cannot generate InviteUserData payload") from exc rep = await self._cmds.invite_4_claimer_communicate(payload=payload) if rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError( f"Backend error during step 4 (data exchange): {rep}") rep = await self._cmds.invite_4_claimer_communicate(payload=b"") if rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError( f"Backend error during step 4 (confirmation exchange): {rep}") try: confirmation = InviteUserConfirmation.decrypt_and_load( rep["payload"], key=self._shared_secret_key) except DataError as exc: raise InviteError( "Invalid InviteUserConfirmation payload provided by peer" ) from exc organization_addr = BackendOrganizationAddr.build( backend_addr=self._cmds.addr, organization_id=self._cmds.addr.organization_id, root_verify_key=confirmation.root_verify_key, ) new_device = generate_new_device( organization_addr=organization_addr, device_id=confirmation.device_id, device_label=confirmation.device_label, human_handle=confirmation.human_handle, profile=confirmation.profile, private_key=private_key, signing_key=signing_key, ) return new_device
def test_local_device(): from parsec.core.types.local_device import _RsLocalDevice, LocalDevice, _PyLocalDevice assert LocalDevice is _RsLocalDevice def _assert_local_device_eq(py, rs): assert isinstance(py, _PyLocalDevice) assert isinstance(rs, _RsLocalDevice) assert py.organization_addr == rs.organization_addr assert py.device_id == rs.device_id assert py.device_label == rs.device_label assert py.human_handle == rs.human_handle assert py.signing_key == rs.signing_key assert py.private_key == rs.private_key assert py.profile == rs.profile assert py.user_manifest_id == rs.user_manifest_id assert py.user_manifest_key == rs.user_manifest_key assert py.local_symkey == rs.local_symkey assert py.is_admin == rs.is_admin assert py.is_outsider == rs.is_outsider assert py.slug == rs.slug assert py.slughash == rs.slughash assert py.root_verify_key == rs.root_verify_key assert py.organization_id == rs.organization_id assert py.device_name == rs.device_name assert py.user_id == rs.user_id assert py.verify_key == rs.verify_key assert py.public_key == rs.public_key assert py.user_display == rs.user_display assert py.short_user_display == rs.short_user_display assert py.device_display == rs.device_display signing_key = SigningKey.generate() kwargs = { "organization_addr": BackendOrganizationAddr.build( BackendAddr.from_url("parsec://foo"), organization_id=OrganizationID("org"), root_verify_key=signing_key.verify_key, ), "device_id": DeviceID.new(), "device_label": None, "human_handle": None, "signing_key": signing_key, "private_key": PrivateKey.generate(), "profile": UserProfile.ADMIN, "user_manifest_id": EntryID.new(), "user_manifest_key": SecretKey.generate(), "local_symkey": SecretKey.generate(), } py_ba = _PyLocalDevice(**kwargs) rs_ba = LocalDevice(**kwargs) _assert_local_device_eq(py_ba, rs_ba)
async def test_claimer_exchange_bad_access(alice, backend, backend_invited_sock_factory, action): invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) async with backend_invited_sock_factory( backend, organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, freeze_on_transport_error=False, ) as invited_sock: if action == "1_wait_peer": claimer_privkey = PrivateKey.generate() call = partial( invite_1_claimer_wait_peer, invited_sock, claimer_public_key=claimer_privkey.public_key, ) elif action == "2a_send_hashed_nonce": call = partial( invite_2a_claimer_send_hashed_nonce, invited_sock, claimer_hashed_nonce=b"<claimer_hashed_nonce>", ) elif action == "2b_send_nonce": call = partial(invite_2b_claimer_send_nonce, invited_sock, claimer_nonce=b"<claimer_nonce>") elif action == "3a_signify_trust": call = partial(invite_3a_claimer_signify_trust, invited_sock) elif action == "3b_wait_peer_trust": call = partial(invite_3b_claimer_wait_peer_trust, invited_sock) elif action == "4_communicate": call = partial(invite_4_claimer_communicate, invited_sock, payload=b"<payload>") # Disable the callback responsible for closing the claimer's connection # on invitation deletion. This way we can test connection behavior # when the automatic closing takes time to be processed. backend.event_bus.mute(BackendEvent.INVITE_STATUS_CHANGED) await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation.token, on=datetime(2000, 1, 2), reason=InvitationDeletedReason.ROTTEN, ) with pytest.raises(TransportError): # Transport is always closed when invitation is closed after this handshake await call()
def generate_new_device( device_id: DeviceID, organization_addr: BackendOrganizationAddr) -> LocalDevice: return LocalDevice( organization_addr=organization_addr, device_id=device_id, signing_key=SigningKey.generate(), private_key=PrivateKey.generate(), user_manifest_access=ManifestAccess(), local_symkey=generate_secret_key(), )
async def test_greeter_exchange_bad_access(alice, backend, alice_backend_sock, type): if type == "deleted_invitation": invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id) await backend.invite.delete( organization_id=alice.organization_id, greeter=alice.user_id, token=invitation.token, on=datetime(2000, 1, 2), reason=InvitationDeletedReason.ROTTEN, ) token = invitation.token status = "already_deleted" else: token = uuid4() status = "not_found" greeter_privkey = PrivateKey.generate() with trio.fail_after(1): rep = await invite_1_greeter_wait_peer( alice_backend_sock, token=token, greeter_public_key=greeter_privkey.public_key) assert rep == {"status": status} with trio.fail_after(1): rep = await invite_2a_greeter_get_hashed_nonce(alice_backend_sock, token=token) assert rep == {"status": status} with trio.fail_after(1): rep = await invite_2b_greeter_send_nonce( alice_backend_sock, token=token, greeter_nonce=b"<greeter_nonce>") assert rep == {"status": status} with trio.fail_after(1): rep = await invite_3a_greeter_wait_peer_trust(alice_backend_sock, token=token) assert rep == {"status": status} with trio.fail_after(1): rep = await invite_3b_greeter_signify_trust(alice_backend_sock, token=token) assert rep == {"status": status} with trio.fail_after(1): rep = await invite_4_greeter_communicate(alice_backend_sock, token=token, payload=b"<payload>") assert rep == {"status": status}
def generate_new_device(device_id: DeviceID, organization_addr: BackendOrganizationAddr, is_admin: bool = False) -> LocalDevice: return LocalDevice( organization_addr=organization_addr, device_id=device_id, signing_key=SigningKey.generate(), private_key=PrivateKey.generate(), is_admin=is_admin, user_manifest_id=EntryID(uuid4().hex), user_manifest_key=SecretKey.generate(), local_symkey=SecretKey.generate(), )
def decrypt_and_load_for(self, encrypted: bytes, recipient_privkey: PrivateKey, **kwargs) -> "BaseData": """ Raises: DataError """ try: raw = recipient_privkey.decrypt_from_self(encrypted) except CryptoError as exc: raise DataError(str(exc)) from exc return self.load(raw, **kwargs)
def decrypt_and_load_for( cls: Type[BaseDataTypeVar], encrypted: bytes, recipient_privkey: PrivateKey, **kwargs: object, ) -> BaseDataTypeVar: """ Raises: DataError """ try: raw = recipient_privkey.decrypt_from_self(encrypted) except CryptoError as exc: raise DataError(str(exc)) from exc return cls.load(raw, **kwargs)
async def _do_wait_peer(self) -> Tuple[SASCode, SASCode, SecretKey]: greeter_private_key = PrivateKey.generate() rep = await self._cmds.invite_1_greeter_wait_peer( token=self.token, greeter_public_key=greeter_private_key.public_key) if rep["status"] in ("not_found", "already_deleted"): raise InviteNotAvailableError() elif rep["status"] != "ok": raise InviteError(f"Backend error during step 1: {rep}") shared_secret_key = generate_shared_secret_key( our_private_key=greeter_private_key, peer_public_key=rep["claimer_public_key"]) greeter_nonce = generate_nonce() rep = await self._cmds.invite_2a_greeter_get_hashed_nonce( token=self.token) if rep["status"] in ("not_found", "already_deleted"): raise InviteNotAvailableError() elif rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError(f"Backend error during step 2a: {rep}") claimer_hashed_nonce = rep["claimer_hashed_nonce"] rep = await self._cmds.invite_2b_greeter_send_nonce( token=self.token, greeter_nonce=greeter_nonce) if rep["status"] in ("not_found", "already_deleted"): raise InviteNotAvailableError() elif rep["status"] == "invalid_state": raise InvitePeerResetError() elif rep["status"] != "ok": raise InviteError(f"Backend error during step 2b: {rep}") if HashDigest.from_data(rep["claimer_nonce"]) != claimer_hashed_nonce: raise InviteError("Invitee nonce and hashed nonce doesn't match") claimer_sas, greeter_sas = generate_sas_codes( claimer_nonce=rep["claimer_nonce"], greeter_nonce=greeter_nonce, shared_secret_key=shared_secret_key, ) return claimer_sas, greeter_sas, shared_secret_key
def generate_new_device( organization_addr: BackendOrganizationAddr, device_id: Optional[DeviceID] = None, profile: UserProfile = UserProfile.STANDARD, human_handle: Optional[HumanHandle] = None, device_label: Optional[DeviceLabel] = None, signing_key: Optional[SigningKey] = None, private_key: Optional[PrivateKey] = None, ) -> LocalDevice: return LocalDevice( organization_addr=organization_addr, device_id=device_id or DeviceID.new(), device_label=device_label, human_handle=human_handle, signing_key=signing_key or SigningKey.generate(), private_key=private_key or PrivateKey.generate(), profile=profile, user_manifest_id=EntryID.new(), user_manifest_key=SecretKey.generate(), local_symkey=SecretKey.generate(), )
async def new( cls, addr: BackendPkiEnrollmentAddr ) -> "PkiEnrollmentSubmitterSubmittedStatusCtx": """ Raises: PkiEnrollmentCertificateError PkiEnrollmentCertificateCryptoError PkiEnrollmentCertificateNotFoundError """ enrollment_id = uuid4() signing_key = SigningKey.generate() private_key = PrivateKey.generate() x509_certificate = await pki_enrollment_select_certificate() return cls( addr=addr, enrollment_id=enrollment_id, signing_key=signing_key, private_key=private_key, x509_certificate=x509_certificate, )
def test_user_certificate(): from parsec.api.data.certif import ( _RsUserCertificateContent, UserCertificateContent, _PyUserCertificateContent, ) assert UserCertificateContent is _RsUserCertificateContent def _assert_user_certificate_eq(py, rs): assert py.is_admin == rs.is_admin assert py.author == rs.author assert py.timestamp == rs.timestamp assert py.user_id == rs.user_id assert py.human_handle == rs.human_handle assert py.public_key == rs.public_key assert py.profile == rs.profile kwargs = { "author": DeviceID.new(), "timestamp": pendulum.now(), "user_id": UserID("bob"), "human_handle": HumanHandle("*****@*****.**", "Boby McBobFace"), "public_key": PrivateKey.generate().public_key, "profile": UserProfile.ADMIN, } py_uc = _PyUserCertificateContent(**kwargs) rs_uc = UserCertificateContent(**kwargs) _assert_user_certificate_eq(py_uc, rs_uc) kwargs = { "author": DeviceID.new(), "timestamp": pendulum.now(), "user_id": UserID("alice"), "human_handle": None, "public_key": PrivateKey.generate().public_key, "profile": UserProfile.STANDARD, } py_uc = py_uc.evolve(**kwargs) rs_uc = rs_uc.evolve(**kwargs) _assert_user_certificate_eq(py_uc, rs_uc) sign_key = SigningKey.generate() py_data = py_uc.dump_and_sign(sign_key) rs_data = rs_uc.dump_and_sign(sign_key) py_uc = _PyUserCertificateContent.verify_and_load( rs_data, sign_key.verify_key, expected_author=py_uc.author, expected_user=py_uc.user_id, expected_human_handle=py_uc.human_handle, ) rs_uc = UserCertificateContent.verify_and_load( py_data, sign_key.verify_key, expected_author=rs_uc.author, expected_user=rs_uc.user_id, expected_human_handle=rs_uc.human_handle, ) _assert_user_certificate_eq(py_uc, rs_uc) py_uc = _PyUserCertificateContent.unsecure_load(rs_data) rs_uc = UserCertificateContent.unsecure_load(py_data) _assert_user_certificate_eq(py_uc, rs_uc)
async def test_claimer_step_1_retry( backend, alice, backend_invited_sock_factory, alice_backend_sock, invitation ): greeter_privkey = PrivateKey.generate() claimer_privkey = PrivateKey.generate() async with backend_invited_sock_factory( backend, organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) as invited_sock: with backend.event_bus.listen() as spy: with trio.CancelScope() as cancel_scope: async with invite_1_claimer_wait_peer.async_call( invited_sock, claimer_public_key=claimer_privkey.public_key ): await spy.wait_with_timeout( BackendEvent.INVITE_CONDUIT_UPDATED, kwargs={ "organization_id": alice.organization_id, "token": invitation.token, }, ) # Here greeter is waiting for claimer, that the time we choose to close greeter connection cancel_scope.cancel() # Now retry the first step with a new connection async with backend_invited_sock_factory( backend, organization_id=alice.organization_id, invitation_type=InvitationType.DEVICE, token=invitation.token, ) as invited_sock: with trio.fail_after(1): with backend.event_bus.listen() as spy: async with invite_1_claimer_wait_peer.async_call( invited_sock, claimer_public_key=claimer_privkey.public_key ) as claimer_async_rep: # Must wait for the reset command to update the conduit # before starting the greeter command otherwise it will # also be reseted await spy.wait_with_timeout( BackendEvent.INVITE_CONDUIT_UPDATED, kwargs={ "organization_id": alice.organization_id, "token": invitation.token, }, ) greeter_rep = await invite_1_greeter_wait_peer( alice_backend_sock, token=invitation.token, greeter_public_key=greeter_privkey.public_key, ) assert greeter_rep == { "status": "ok", "claimer_public_key": claimer_privkey.public_key, } assert claimer_async_rep.rep == { "status": "ok", "greeter_public_key": greeter_privkey.public_key, }
async def test_claimer_step_2_retry( backend, alice, backend_sock_factory, alice_backend_sock, invitation, invited_sock ): greeter_privkey = PrivateKey.generate() claimer_privkey = PrivateKey.generate() greeter_retry_privkey = PrivateKey.generate() claimer_retry_privkey = PrivateKey.generate() # Step 1 with trio.fail_after(1): async with invite_1_greeter_wait_peer.async_call( alice_backend_sock, token=invitation.token, greeter_public_key=greeter_privkey.public_key, ) as greeter_async_rep: claimer_rep = await invite_1_claimer_wait_peer( invited_sock, claimer_public_key=claimer_privkey.public_key ) assert claimer_rep == {"status": "ok", "greeter_public_key": greeter_privkey.public_key} assert greeter_async_rep.rep == { "status": "ok", "claimer_public_key": claimer_privkey.public_key, } # Greeter initiates step 2a... with trio.fail_after(1): with backend.event_bus.listen() as spy: async with invite_2a_greeter_get_hashed_nonce.async_call( alice_backend_sock, token=invitation.token ) as greeter_2a_async_rep: await spy.wait_with_timeout( BackendEvent.INVITE_CONDUIT_UPDATED, kwargs={"organization_id": alice.organization_id, "token": invitation.token}, ) # ...but changes his mind and reset from another connection ! async with backend_sock_factory(backend, alice) as alice_backend_sock2: async with invite_1_greeter_wait_peer.async_call( alice_backend_sock2, token=invitation.token, greeter_public_key=greeter_retry_privkey.public_key, ) as greeter_retry_1_async_rep: # First connection should be notified of the reset await greeter_2a_async_rep.do_recv() assert greeter_2a_async_rep.rep == {"status": "invalid_state"} # Claimer now arrives and try to do step 2a rep = await invite_2a_claimer_send_hashed_nonce( invited_sock, claimer_hashed_nonce=b"hashed nonce" ) assert rep == {"status": "invalid_state"} # So claimer returns to step 1 rep = await invite_1_claimer_wait_peer( invited_sock, claimer_public_key=claimer_retry_privkey.public_key ) assert rep == { "status": "ok", "greeter_public_key": greeter_retry_privkey.public_key, } assert greeter_retry_1_async_rep.rep == { "status": "ok", "claimer_public_key": claimer_retry_privkey.public_key, } # Finally retry and achieve step 2 async def _claimer_step_2(): rep = await invite_2a_greeter_get_hashed_nonce( alice_backend_sock, token=invitation.token ) assert rep == {"status": "ok", "claimer_hashed_nonce": b"retry hashed nonce"} rep = await invite_2b_greeter_send_nonce( alice_backend_sock, token=invitation.token, greeter_nonce=b"greeter nonce" ) assert rep == {"status": "ok", "claimer_nonce": b"claimer nonce"} async def _greeter_step_2(): rep = await invite_2a_claimer_send_hashed_nonce( invited_sock, claimer_hashed_nonce=b"retry hashed nonce" ) assert rep == {"status": "ok", "greeter_nonce": b"greeter nonce"} rep = await invite_2b_claimer_send_nonce( invited_sock, claimer_nonce=b"claimer nonce" ) assert rep == {"status": "ok"} with trio.fail_after(1): async with trio.open_nursery() as nursery: nursery.start_soon(_claimer_step_2) nursery.start_soon(_greeter_step_2)
async def exchange_testbed(alice_backend_sock, invitation, invited_sock): greeter_privkey = PrivateKey.generate() claimer_privkey = PrivateKey.generate() async def _run_greeter(peer_controller): while True: order, order_arg = await peer_controller.peer_next_order() if order == "1_wait_peer": await peer_controller.peer_do( invite_1_greeter_wait_peer, alice_backend_sock, token=invitation.token, greeter_public_key=greeter_privkey.public_key, ) elif order == "2a_get_hashed_nonce": await peer_controller.peer_do( invite_2a_greeter_get_hashed_nonce, alice_backend_sock, token=invitation.token ) elif order == "2b_send_nonce": await peer_controller.peer_do( invite_2b_greeter_send_nonce, alice_backend_sock, token=invitation.token, greeter_nonce=b"<greeter_nonce>", ) elif order == "3a_wait_peer_trust": await peer_controller.peer_do( invite_3a_greeter_wait_peer_trust, alice_backend_sock, token=invitation.token ) elif order == "3b_signify_trust": await peer_controller.peer_do( invite_3b_greeter_signify_trust, alice_backend_sock, token=invitation.token ) elif order == "4_communicate": await peer_controller.peer_do( invite_4_greeter_communicate, alice_backend_sock, token=invitation.token, payload=order_arg, ) else: assert False async def _run_claimer(peer_controller): while True: order, order_arg = await peer_controller.peer_next_order() if order == "1_wait_peer": await peer_controller.peer_do( invite_1_claimer_wait_peer, invited_sock, claimer_public_key=claimer_privkey.public_key, ) elif order == "2a_send_hashed_nonce": await peer_controller.peer_do( invite_2a_claimer_send_hashed_nonce, invited_sock, claimer_hashed_nonce=b"<claimer_hashed_nonce>", ) elif order == "2b_send_nonce": await peer_controller.peer_do( invite_2b_claimer_send_nonce, invited_sock, claimer_nonce=b"<claimer_nonce>" ) elif order == "3a_signify_trust": await peer_controller.peer_do(invite_3a_claimer_signify_trust, invited_sock) elif order == "3b_wait_peer_trust": await peer_controller.peer_do(invite_3b_claimer_wait_peer_trust, invited_sock) elif order == "4_communicate": assert order_arg is not None await peer_controller.peer_do( invite_4_claimer_communicate, invited_sock, payload=order_arg ) else: assert False greeter_ctlr = PeerControler() claimer_ctlr = PeerControler() async with trio.open_nursery() as nursery: nursery.start_soon(_run_greeter, greeter_ctlr) nursery.start_soon(_run_claimer, claimer_ctlr) yield greeter_privkey, claimer_privkey, greeter_ctlr, claimer_ctlr nursery.cancel_scope.cancel()
def test_invite_device_confirmation(): from parsec.api.data.invite import ( _RsInviteDeviceConfirmation, InviteDeviceConfirmation, _PyInviteDeviceConfirmation, ) assert InviteDeviceConfirmation is _RsInviteDeviceConfirmation di = DeviceID("a@b") dl = DeviceLabel("label") hh = HumanHandle("*****@*****.**", "Hubert Farnsworth") profile = UserProfile.STANDARD pk = PrivateKey.generate() umi = EntryID.new() umk = SecretKey.generate() sk = SigningKey.generate() vk = sk.verify_key sek = SecretKey.generate() py_idc = _PyInviteDeviceConfirmation( device_id=di, device_label=dl, human_handle=hh, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) rs_idc = InviteDeviceConfirmation( device_id=di, device_label=dl, human_handle=hh, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) assert rs_idc.device_label.str == py_idc.device_label.str assert str(rs_idc.human_handle) == str(py_idc.human_handle) assert rs_idc.device_id.str == py_idc.device_id.str assert rs_idc.profile == py_idc.profile assert rs_idc.user_manifest_id.hex == py_idc.user_manifest_id.hex rs_encrypted = rs_idc.dump_and_encrypt(key=sek) py_encrypted = py_idc.dump_and_encrypt(key=sek) # Decrypt Rust-encrypted with Rust rs_idc2 = InviteDeviceConfirmation.decrypt_and_load(rs_encrypted, sek) assert rs_idc.device_label.str == rs_idc2.device_label.str assert str(rs_idc.human_handle) == str(rs_idc2.human_handle) assert rs_idc.device_id.str == rs_idc2.device_id.str assert rs_idc.profile == rs_idc2.profile assert rs_idc.user_manifest_id.hex == rs_idc2.user_manifest_id.hex # Decrypt Python-encrypted with Python rs_idc3 = InviteDeviceConfirmation.decrypt_and_load(py_encrypted, sek) assert rs_idc.device_label.str == rs_idc3.device_label.str assert str(rs_idc.human_handle) == str(rs_idc3.human_handle) assert rs_idc.device_id.str == rs_idc3.device_id.str assert rs_idc.profile == rs_idc3.profile assert rs_idc.user_manifest_id.hex == rs_idc3.user_manifest_id.hex # Decrypt Rust-encrypted with Python py_idc2 = _PyInviteDeviceConfirmation.decrypt_and_load(rs_encrypted, sek) assert rs_idc.device_label.str == py_idc2.device_label.str assert str(rs_idc.human_handle) == str(py_idc2.human_handle) assert rs_idc.device_id.str == py_idc2.device_id.str assert rs_idc.profile == py_idc2.profile assert rs_idc.user_manifest_id.hex == rs_idc2.user_manifest_id.hex # With human_handle and device_label as None py_idc = _PyInviteDeviceConfirmation( device_id=di, device_label=None, human_handle=None, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) rs_idc = InviteDeviceConfirmation( device_id=di, device_label=None, human_handle=None, profile=profile, private_key=pk, user_manifest_id=umi, user_manifest_key=umk, root_verify_key=vk, ) assert py_idc.device_label is None assert rs_idc.device_label is None assert py_idc.human_handle is None assert rs_idc.human_handle is None
async def exchange_testbed(backend, alice, alice_backend_sock, backend_invited_sock_factory): async def _run_greeter(tb): peer_controller = tb.greeter_ctlr while True: order, step_4_payload = await peer_controller.peer_next_order() if order == "1_wait_peer": await peer_controller.peer_do( invite_1_greeter_wait_peer, tb.greeter_sock, token=tb.invitation.token, greeter_public_key=tb.greeter_privkey.public_key, ) elif order == "2a_get_hashed_nonce": await peer_controller.peer_do( invite_2a_greeter_get_hashed_nonce, tb.greeter_sock, token=tb.invitation.token) elif order == "2b_send_nonce": await peer_controller.peer_do( invite_2b_greeter_send_nonce, tb.greeter_sock, token=tb.invitation.token, greeter_nonce=b"<greeter_nonce>", ) elif order == "3a_wait_peer_trust": await peer_controller.peer_do( invite_3a_greeter_wait_peer_trust, tb.greeter_sock, token=tb.invitation.token) elif order == "3b_signify_trust": await peer_controller.peer_do(invite_3b_greeter_signify_trust, tb.greeter_sock, token=tb.invitation.token) elif order == "4_communicate": assert step_4_payload is not None await peer_controller.peer_do( invite_4_greeter_communicate, tb.greeter_sock, token=tb.invitation.token, payload=step_4_payload, ) else: assert False async def _run_claimer(tb): peer_controller = tb.claimer_ctlr while True: order, step_4_payload = await peer_controller.peer_next_order() if order == "invite_info": await peer_controller.peer_do(invite_info, tb.claimer_sock) elif order == "1_wait_peer": await peer_controller.peer_do( invite_1_claimer_wait_peer, tb.claimer_sock, claimer_public_key=tb.claimer_privkey.public_key, ) elif order == "2a_send_hashed_nonce": await peer_controller.peer_do( invite_2a_claimer_send_hashed_nonce, tb.claimer_sock, claimer_hashed_nonce=HashDigest.from_data( b"<claimer_nonce>"), ) elif order == "2b_send_nonce": await peer_controller.peer_do(invite_2b_claimer_send_nonce, tb.claimer_sock, claimer_nonce=b"<claimer_nonce>") elif order == "3a_signify_trust": await peer_controller.peer_do(invite_3a_claimer_signify_trust, tb.claimer_sock) elif order == "3b_wait_peer_trust": await peer_controller.peer_do( invite_3b_claimer_wait_peer_trust, tb.claimer_sock) elif order == "4_communicate": assert step_4_payload is not None await peer_controller.peer_do(invite_4_claimer_communicate, tb.claimer_sock, payload=step_4_payload) else: assert False greeter_ctlr = PeerControler() claimer_ctlr = PeerControler() greeter_privkey = PrivateKey.generate() claimer_privkey = PrivateKey.generate() invitation = await backend.invite.new_for_device( organization_id=alice.organization_id, greeter_user_id=alice.user_id, created_on=datetime(2000, 1, 2), ) async with backend_invited_sock_factory( backend, organization_id=alice.organization_id, invitation_type=invitation.TYPE, token=invitation.token, # claimer gets it connection closed if invitation is deleted freeze_on_transport_error=False, ) as claimer_sock: async with trio.open_nursery() as nursery: tb = ExchangeTestBed( organization_id=alice.organization_id, greeter=alice, invitation=invitation, greeter_privkey=greeter_privkey, claimer_privkey=claimer_privkey, greeter_ctlr=greeter_ctlr, claimer_ctlr=claimer_ctlr, greeter_sock=alice_backend_sock, claimer_sock=claimer_sock, ) nursery.start_soon(_run_greeter, tb) nursery.start_soon(_run_claimer, tb) yield tb nursery.cancel_scope.cancel()
async def claim_device( organization_addr: BackendOrganizationAddr, new_device_id: DeviceID, token: str, keepalive: Optional[int] = None, ) -> LocalDevice: """ Raises: InviteClaimError InviteClaimBackendOfflineError InviteClaimValidationError InviteClaimPackingError InviteClaimCryptoError """ device_signing_key = SigningKey.generate() answer_private_key = PrivateKey.generate() try: async with backend_anonymous_cmds_factory(organization_addr, keepalive=keepalive) as cmds: # 1) Retrieve invitation creator try: invitation_creator_user, invitation_creator_device = await get_device_invitation_creator( cmds, organization_addr.root_verify_key, new_device_id) except RemoteDevicesManagerBackendOfflineError as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise InviteClaimError( f"Cannot retrieve invitation creator: {exc}") from exc # 2) Generate claim info for invitation creator try: encrypted_claim = DeviceClaimContent( token=token, device_id=new_device_id, verify_key=device_signing_key.verify_key, answer_public_key=answer_private_key.public_key, ).dump_and_encrypt_for( recipient_pubkey=invitation_creator_user.public_key) except DataError as exc: raise InviteClaimError( f"Cannot generate device claim message: {exc}") from exc # 3) Send claim rep = await cmds.device_claim(new_device_id, encrypted_claim) if rep["status"] != "ok": raise InviteClaimError(f"Claim request error: {rep}") # 4) Verify device certificate try: DeviceCertificateContent.verify_and_load( rep["device_certificate"], author_verify_key=invitation_creator_device.verify_key, expected_author=invitation_creator_device.device_id, expected_device=new_device_id, ) except DataError as exc: raise InviteClaimCryptoError(str(exc)) from exc try: answer = DeviceClaimAnswerContent.decrypt_and_load_for( rep["encrypted_answer"], recipient_privkey=answer_private_key) except DataError as exc: raise InviteClaimCryptoError( f"Cannot decrypt device claim answer: {exc}") from exc except BackendNotAvailable as exc: raise InviteClaimBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise InviteClaimError(f"Cannot claim device: {exc}") from exc return LocalDevice( organization_addr=organization_addr, device_id=new_device_id, signing_key=device_signing_key, private_key=answer.private_key, is_admin=invitation_creator_user.is_admin, user_manifest_id=answer.user_manifest_id, user_manifest_key=answer.user_manifest_key, local_symkey=SecretKey.generate(), )
def test_invite_user_data(): from parsec.api.data.invite import _RsInviteUserData, InviteUserData, _PyInviteUserData assert InviteUserData is _RsInviteUserData dl = DeviceLabel("label") hh = HumanHandle("*****@*****.**", "Hubert Farnsworth") pk = PrivateKey.generate() sik = SigningKey.generate() sek = SecretKey.generate() py_iud = _PyInviteUserData( requested_device_label=dl, requested_human_handle=hh, public_key=pk.public_key, verify_key=sik.verify_key, ) rs_iud = InviteUserData( requested_device_label=dl, requested_human_handle=hh, public_key=pk.public_key, verify_key=sik.verify_key, ) assert rs_iud.requested_device_label.str == py_iud.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( py_iud.requested_human_handle) rs_encrypted = rs_iud.dump_and_encrypt(key=sek) py_encrypted = py_iud.dump_and_encrypt(key=sek) # Decrypt Rust-encrypted with Rust rs_iud2 = InviteUserData.decrypt_and_load(rs_encrypted, sek) assert rs_iud.requested_device_label.str == rs_iud2.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( rs_iud2.requested_human_handle) # Decrypt Python-encrypted with Python rs_iud3 = InviteUserData.decrypt_and_load(py_encrypted, sek) assert rs_iud.requested_device_label.str == rs_iud3.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( rs_iud3.requested_human_handle) # Decrypt Rust-encrypted with Python py_iud2 = _PyInviteUserData.decrypt_and_load(rs_encrypted, sek) assert rs_iud.requested_device_label.str == py_iud2.requested_device_label.str assert str(rs_iud.requested_human_handle) == str( py_iud2.requested_human_handle) # With requested_human_handle and requested_device_label as None py_iud = _PyInviteUserData( requested_device_label=None, requested_human_handle=None, public_key=pk.public_key, verify_key=sik.verify_key, ) rs_iud = InviteUserData( requested_device_label=None, requested_human_handle=None, public_key=pk.public_key, verify_key=sik.verify_key, ) assert py_iud.requested_device_label is None assert rs_iud.requested_device_label is None assert py_iud.requested_human_handle is None assert rs_iud.requested_human_handle is None