コード例 #1
0
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(),
    )
コード例 #2
0
ファイル: claimer.py プロジェクト: stjordanis/parsec-cloud
    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
コード例 #3
0
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"
            )),
    )
コード例 #4
0
    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,
            )
コード例 #5
0
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"
            )),
    )
コード例 #6
0
    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
コード例 #7
0
ファイル: base.py プロジェクト: admariner/parsec-cloud
    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,
        )
コード例 #8
0
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}
コード例 #9
0
    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
コード例 #10
0
    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
コード例 #11
0
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)
コード例 #12
0
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()
コード例 #13
0
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(),
    )
コード例 #14
0
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}
コード例 #15
0
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(),
    )
コード例 #16
0
ファイル: base.py プロジェクト: anthrax3/parsec-cloud
    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)
コード例 #17
0
ファイル: base.py プロジェクト: admariner/parsec-cloud
    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)
コード例 #18
0
    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
コード例 #19
0
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(),
    )
コード例 #20
0
ファイル: submitter.py プロジェクト: Scille/parsec-cloud
    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,
        )
コード例 #21
0
ファイル: test_certif.py プロジェクト: Scille/parsec-cloud
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)
コード例 #22
0
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,
            }
コード例 #23
0
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)
コード例 #24
0
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()
コード例 #25
0
ファイル: test_invite.py プロジェクト: Scille/parsec-cloud
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
コード例 #26
0
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()
コード例 #27
0
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(),
    )
コード例 #28
0
ファイル: test_invite.py プロジェクト: Scille/parsec-cloud
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