Example #1
0
    async def api_device_create(self, client_ctx, msg):
        msg = device_create_serializer.req_load(msg)

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

            redacted_data = None
            if msg["redacted_device_certificate"]:
                redacted_data = DeviceCertificateContent.verify_and_load(
                    msg["redacted_device_certificate"],
                    author_verify_key=client_ctx.verify_key,
                    expected_author=client_ctx.device_id,
                )

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

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

        if data.device_id.user_id != client_ctx.user_id:
            return {"status": "bad_user_id", "reason": "Device must be handled by it own user."}

        if redacted_data:
            if redacted_data.evolve(device_label=data.device_label) != data:
                return {
                    "status": "invalid_data",
                    "reason": "Redacted Device certificate differs from Device certificate.",
                }
            if redacted_data.device_label:
                return {
                    "status": "invalid_data",
                    "reason": "Redacted Device certificate must not contain a device_label field.",
                }

        try:
            device = Device(
                device_id=data.device_id,
                device_label=data.device_label,
                device_certificate=msg["device_certificate"],
                redacted_device_certificate=msg["redacted_device_certificate"]
                or msg["device_certificate"],
                device_certifier=data.author,
                created_on=data.timestamp,
            )
            await self.create_device(client_ctx.organization_id, device)
        except UserAlreadyExistsError as exc:
            return {"status": "already_exists", "reason": str(exc)}

        return device_create_serializer.rep_dump({"status": "ok"})
Example #2
0
def validate_new_device_certificate(
    expected_author: DeviceID,
    author_verify_key: VerifyKey,
    device_certificate: bytes,
    redacted_device_certificate: bytes,
) -> Device:
    try:
        data = DeviceCertificateContent.verify_and_load(
            device_certificate,
            author_verify_key=author_verify_key,
            expected_author=expected_author)

        redacted_data = DeviceCertificateContent.verify_and_load(
            redacted_device_certificate,
            author_verify_key=author_verify_key,
            expected_author=expected_author,
        )

    except DataError as exc:
        raise CertificateValidationError(
            "invalid_certification", f"Invalid certification data ({exc}).")

    if not timestamps_in_the_ballpark(data.timestamp, pendulum.now()):
        raise CertificateValidationError(
            "invalid_certification", f"Invalid timestamp in certification.")

    if data.device_id.user_id != expected_author.user_id:
        raise CertificateValidationError(
            "bad_user_id", "Device must be handled by it own user.")

    if redacted_data.evolve(device_label=data.device_label) != data:
        raise CertificateValidationError(
            "invalid_data",
            "Redacted Device certificate differs from Device certificate.")
    if redacted_data.device_label:
        raise CertificateValidationError(
            "invalid_data",
            "Redacted Device certificate must not contain a device_label field."
        )

    return Device(
        device_id=data.device_id,
        device_label=data.device_label,
        device_certificate=device_certificate,
        redacted_device_certificate=redacted_device_certificate,
        device_certifier=data.author,
        created_on=data.timestamp,
    )
Example #3
0
def test_build_device_certificate(alice, bob, mallory):
    now = pendulum_now()
    certif = DeviceCertificateContent(
        author=alice.device_id,
        timestamp=now,
        device_id=bob.device_id,
        device_label=bob.device_label,
        verify_key=bob.verify_key,
    ).dump_and_sign(alice.signing_key)
    assert isinstance(certif, bytes)

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

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

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

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

    with pytest.raises(DataError) as exc:
        DeviceCertificateContent.verify_and_load(
            certif,
            author_verify_key=alice.verify_key,
            expected_author=alice.device_id,
            expected_device=mallory.device_id,
        )
    assert str(
        exc.value
    ) == "Invalid device ID: expected `mallory@dev1`, got `bob@dev1`"
Example #4
0
    async def apiv1_device_create(self, client_ctx, msg):
        msg = apiv1_device_create_serializer.req_load(msg)

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

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

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

        if data.device_id.user_id != client_ctx.user_id:
            return {
                "status": "bad_user_id",
                "reason": "Device must be handled by it own user."
            }

        try:
            device = Device(
                device_id=data.device_id,
                device_certificate=msg["device_certificate"],
                redacted_device_certificate=msg["device_certificate"],
                device_certifier=data.author,
                created_on=data.timestamp,
            )
            await self.create_device(client_ctx.organization_id,
                                     device,
                                     encrypted_answer=msg["encrypted_answer"])
        except UserAlreadyExistsError as exc:
            return {"status": "already_exists", "reason": str(exc)}

        return apiv1_device_create_serializer.rep_dump({"status": "ok"})
Example #5
0
    async def _api_user_create(self, client_ctx, msg):
        try:
            d_data = DeviceCertificateContent.verify_and_load(
                msg["device_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )
            u_data = UserCertificateContent.verify_and_load(
                msg["user_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )
            ru_data = UserCertificateContent.verify_and_load(
                msg["redacted_user_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )
            rd_data = DeviceCertificateContent.verify_and_load(
                msg["redacted_device_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )

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

        if u_data.timestamp != d_data.timestamp:
            return {
                "status": "invalid_data",
                "reason": "Device and User certificates must have the same timestamp.",
            }

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

        if u_data.user_id != d_data.device_id.user_id:
            return {
                "status": "invalid_data",
                "reason": "Device and User must have the same user ID.",
            }

        if ru_data.evolve(human_handle=u_data.human_handle) != u_data:
            return {
                "status": "invalid_data",
                "reason": "Redacted User certificate differs from User certificate.",
            }
        if ru_data.human_handle:
            return {
                "status": "invalid_data",
                "reason": "Redacted User certificate must not contain a human_handle field.",
            }

        if rd_data.evolve(device_label=d_data.device_label) != d_data:
            return {
                "status": "invalid_data",
                "reason": "Redacted Device certificate differs from Device certificate.",
            }
        if rd_data.device_label:
            return {
                "status": "invalid_data",
                "reason": "Redacted Device certificate must not contain a device_label field.",
            }

        try:
            user = User(
                user_id=u_data.user_id,
                human_handle=u_data.human_handle,
                profile=u_data.profile,
                user_certificate=msg["user_certificate"],
                redacted_user_certificate=msg["redacted_user_certificate"]
                or msg["user_certificate"],
                user_certifier=u_data.author,
                created_on=u_data.timestamp,
            )
            first_device = Device(
                device_id=d_data.device_id,
                device_label=d_data.device_label,
                device_certificate=msg["device_certificate"],
                redacted_device_certificate=msg["redacted_device_certificate"]
                or msg["device_certificate"],
                device_certifier=d_data.author,
                created_on=d_data.timestamp,
            )
            await self.create_user(client_ctx.organization_id, user, first_device)

        except UserAlreadyExistsError as exc:
            return {"status": "already_exists", "reason": str(exc)}

        return {"status": "ok"}
Example #6
0
async def claim_user(
    organization_addr: BackendOrganizationAddr,
    new_device_id: DeviceID,
    token: str,
    keepalive: Optional[int] = None,
) -> LocalDevice:
    """
    Raises:
        InviteClaimError
        InviteClaimBackendOfflineError
        InviteClaimValidationError
        InviteClaimPackingError
        InviteClaimCryptoError
    """
    new_device = generate_new_device(new_device_id, organization_addr)

    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_user_invitation_creator(
                    cmds, new_device.root_verify_key, new_device.user_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 = UserClaimContent(
                    device_id=new_device_id,
                    token=token,
                    public_key=new_device.public_key,
                    verify_key=new_device.verify_key,
                ).dump_and_encrypt_for(
                    recipient_pubkey=invitation_creator_user.public_key)

            except DataError as exc:
                raise InviteClaimError(
                    f"Cannot generate user claim message: {exc}") from exc

            # 3) Send claim
            rep = await cmds.user_claim(new_device_id.user_id, encrypted_claim)
            if rep["status"] != "ok":
                raise InviteClaimError(f"Cannot claim user: {rep}")

            # 4) Verify user&device certificates and check admin status
            try:
                user = UserCertificateContent.verify_and_load(
                    rep["user_certificate"],
                    author_verify_key=invitation_creator_device.verify_key,
                    expected_author=invitation_creator_device.device_id,
                    expected_user=new_device_id.user_id,
                )

                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,
                )

                new_device = new_device.evolve(is_admin=user.is_admin)

            except DataError as exc:
                raise InviteClaimCryptoError(str(exc)) from exc

    except BackendNotAvailable as exc:
        raise InviteClaimBackendOfflineError(str(exc)) from exc

    except BackendConnectionError as exc:
        raise InviteClaimError(f"Cannot claim user: {exc}") from exc

    return new_device
Example #7
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(),
    )
Example #8
0
    async def api_organization_bootstrap(self, client_ctx, msg):
        msg = apiv1_organization_bootstrap_serializer.req_load(msg)
        bootstrap_token = msg["bootstrap_token"]
        root_verify_key = msg["root_verify_key"]

        try:
            u_data = UserCertificateContent.verify_and_load(
                msg["user_certificate"],
                author_verify_key=root_verify_key,
                expected_author=None)
            d_data = DeviceCertificateContent.verify_and_load(
                msg["device_certificate"],
                author_verify_key=root_verify_key,
                expected_author=None)

            ru_data = rd_data = None
            if "redacted_user_certificate" in msg:
                ru_data = UserCertificateContent.verify_and_load(
                    msg["redacted_user_certificate"],
                    author_verify_key=root_verify_key,
                    expected_author=None,
                )
            if "redacted_device_certificate" in msg:
                rd_data = DeviceCertificateContent.verify_and_load(
                    msg["redacted_device_certificate"],
                    author_verify_key=root_verify_key,
                    expected_author=None,
                )

        except DataError as exc:
            return {
                "status": "invalid_certification",
                "reason": f"Invalid certification data ({exc}).",
            }
        if u_data.profile != UserProfile.ADMIN:
            return {
                "status": "invalid_data",
                "reason": "Bootstrapping user must have admin profile.",
            }

        if u_data.timestamp != d_data.timestamp:
            return {
                "status":
                "invalid_data",
                "reason":
                "Device and user certificates must have the same timestamp.",
            }

        if u_data.user_id != d_data.device_id.user_id:
            return {
                "status": "invalid_data",
                "reason": "Device and user must have the same user ID.",
            }

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

        if ru_data:
            if ru_data.evolve(human_handle=u_data.human_handle) != u_data:
                return {
                    "status":
                    "invalid_data",
                    "reason":
                    "Redacted User certificate differs from User certificate.",
                }
            if ru_data.human_handle:
                return {
                    "status":
                    "invalid_data",
                    "reason":
                    "Redacted User certificate must not contain a human_handle field.",
                }

        if rd_data:
            if rd_data.evolve(device_label=d_data.device_label) != d_data:
                return {
                    "status":
                    "invalid_data",
                    "reason":
                    "Redacted Device certificate differs from Device certificate.",
                }
            if rd_data.device_label:
                return {
                    "status":
                    "invalid_data",
                    "reason":
                    "Redacted Device certificate must not contain a device_label field.",
                }

        if (rd_data and not ru_data) or (ru_data and not rd_data):
            return {
                "status":
                "invalid_data",
                "reason":
                "Redacted user&device certificate muste be provided together",
            }

        user = User(
            user_id=u_data.user_id,
            human_handle=u_data.human_handle,
            profile=u_data.profile,
            user_certificate=msg["user_certificate"],
            redacted_user_certificate=msg.get("redacted_user_certificate",
                                              msg["user_certificate"]),
            user_certifier=u_data.author,
            created_on=u_data.timestamp,
        )
        first_device = Device(
            device_id=d_data.device_id,
            device_label=d_data.device_label,
            device_certificate=msg["device_certificate"],
            redacted_device_certificate=msg.get("redacted_device_certificate",
                                                msg["device_certificate"]),
            device_certifier=d_data.author,
            created_on=d_data.timestamp,
        )
        try:
            await self.bootstrap(client_ctx.organization_id, user,
                                 first_device, bootstrap_token,
                                 root_verify_key)

        except OrganizationAlreadyBootstrappedError:
            return {"status": "already_bootstrapped"}

        except (OrganizationNotFoundError,
                OrganizationInvalidBootstrapTokenError):
            return {"status": "not_found"}

        # Note: we let OrganizationFirstUserCreationError bobbles up given
        # it should not occurs under normal circumstances

        # Finally notify webhook
        await self.webhooks.on_organization_bootstrap(
            organization_id=client_ctx.organization_id,
            device_id=first_device.device_id,
            device_label=first_device.device_label,
            human_email=user.human_handle.email if user.human_handle else None,
            human_label=user.human_handle.label if user.human_handle else None,
        )

        return apiv1_organization_bootstrap_serializer.rep_dump(
            {"status": "ok"})
Example #9
0
    async def api_organization_bootstrap(self, client_ctx, msg):
        msg = organization_bootstrap_serializer.req_load(msg)
        root_verify_key = msg["root_verify_key"]

        try:
            u_data = UserCertificateContent.verify_and_load(
                msg["user_certificate"],
                author_verify_key=root_verify_key,
                expected_author=None)
            d_data = DeviceCertificateContent.verify_and_load(
                msg["device_certificate"],
                author_verify_key=root_verify_key,
                expected_author=None)

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

        if u_data.timestamp != d_data.timestamp:
            return {
                "status":
                "invalid_data",
                "reason":
                "Device and user certificates must have the same timestamp.",
            }

        if u_data.user_id != d_data.device_id.user_id:
            return {
                "status": "invalid_data",
                "reason": "Device and user must have the same user ID.",
            }

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

        user, first_device = new_user_factory(
            device_id=d_data.device_id,
            human_handle=u_data.human_handle,
            is_admin=True,
            certifier=None,
            user_certificate=msg["user_certificate"],
            device_certificate=msg["device_certificate"],
        )
        try:
            await self.bootstrap(
                client_ctx.organization_id,
                user,
                first_device,
                msg["bootstrap_token"],
                root_verify_key,
            )

        except OrganizationAlreadyBootstrappedError:
            return {"status": "already_bootstrapped"}

        except (OrganizationNotFoundError,
                OrganizationInvalidBootstrapTokenError):
            return {"status": "not_found"}

        # Note: we let OrganizationFirstUserCreationError bobbles up given
        # it should not occurs under normal circumstances

        return organization_bootstrap_serializer.rep_dump({"status": "ok"})
Example #10
0
def validate_new_user_certificates(
    expected_author: DeviceID,
    author_verify_key: VerifyKey,
    user_certificate: bytes,
    device_certificate: bytes,
    redacted_user_certificate: bytes,
    redacted_device_certificate: bytes,
) -> Tuple[User, Device]:
    """
    Raises:
        CertificateValidationError
        UserInvalidCertificationError
    """
    try:
        d_data = DeviceCertificateContent.verify_and_load(
            device_certificate,
            author_verify_key=author_verify_key,
            expected_author=expected_author)
        u_data = UserCertificateContent.verify_and_load(
            user_certificate,
            author_verify_key=author_verify_key,
            expected_author=expected_author)
        ru_data = UserCertificateContent.verify_and_load(
            redacted_user_certificate,
            author_verify_key=author_verify_key,
            expected_author=expected_author,
        )
        rd_data = DeviceCertificateContent.verify_and_load(
            redacted_device_certificate,
            author_verify_key=author_verify_key,
            expected_author=expected_author,
        )

    except DataError as exc:
        raise CertificateValidationError(
            "invalid_certification", f"Invalid certification data ({exc}).")

    if u_data.timestamp != d_data.timestamp:
        raise CertificateValidationError(
            "invalid_data",
            "Device and User certificates must have the same timestamp.")

    now = pendulum.now()
    if not timestamps_in_the_ballpark(u_data.timestamp, now):
        raise CertificateValidationError("invalid_certification",
                                         "Invalid timestamp in certificate.")

    if u_data.user_id != d_data.device_id.user_id:
        raise CertificateValidationError(
            "invalid_data", "Device and User must have the same user ID.")

    if ru_data.evolve(human_handle=u_data.human_handle) != u_data:
        raise CertificateValidationError(
            "invalid_data",
            "Redacted User certificate differs from User certificate.")

    if ru_data.human_handle:
        raise CertificateValidationError(
            "invalid_data",
            "Redacted User certificate must not contain a human_handle field.")

    if rd_data.evolve(device_label=d_data.device_label) != d_data:
        raise CertificateValidationError(
            "invalid_data",
            "Redacted Device certificate differs from Device certificate.")

    if rd_data.device_label:
        raise CertificateValidationError(
            "invalid_data",
            "Redacted Device certificate must not contain a device_label field."
        )

    user = User(
        user_id=u_data.user_id,
        human_handle=u_data.human_handle,
        profile=u_data.profile,
        user_certificate=user_certificate,
        redacted_user_certificate=redacted_user_certificate,
        user_certifier=u_data.author,
        created_on=u_data.timestamp,
    )
    first_device = Device(
        device_id=d_data.device_id,
        device_label=d_data.device_label,
        device_certificate=device_certificate,
        redacted_device_certificate=redacted_device_certificate,
        device_certifier=d_data.author,
        created_on=d_data.timestamp,
    )

    return user, first_device
Example #11
0
    async def api_user_create(self, client_ctx, msg):
        if not client_ctx.is_admin:
            return {
                "status": "not_allowed",
                "reason": f"User `{client_ctx.device_id.user_id}` is not admin",
            }

        msg = user_create_serializer.req_load(msg)

        try:
            d_data = DeviceCertificateContent.verify_and_load(
                msg["device_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )
            u_data = UserCertificateContent.verify_and_load(
                msg["user_certificate"],
                author_verify_key=client_ctx.verify_key,
                expected_author=client_ctx.device_id,
            )

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

        if u_data.timestamp != d_data.timestamp:
            return {
                "status": "invalid_data",
                "reason": "Device and User certifications must have the same timestamp.",
            }

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

        if u_data.user_id != d_data.device_id.user_id:
            return {
                "status": "invalid_data",
                "reason": "Device and User must have the same user ID.",
            }

        try:
            user = User(
                user_id=u_data.user_id,
                human_handle=u_data.human_handle,
                is_admin=u_data.is_admin,
                user_certificate=msg["user_certificate"],
                user_certifier=u_data.author,
                created_on=u_data.timestamp,
            )
            first_devices = Device(
                device_id=d_data.device_id,
                device_certificate=msg["device_certificate"],
                device_certifier=d_data.author,
                created_on=d_data.timestamp,
            )
            await self.create_user(client_ctx.organization_id, user, first_devices)

        except UserAlreadyExistsError as exc:
            return {"status": "already_exists", "reason": str(exc)}

        return user_create_serializer.rep_dump({"status": "ok"})