Esempio n. 1
0
def test_build_user_certificate(alice, bob, mallory):
    now = pendulum_now()
    certif = UserCertificateContent(
        author=alice.device_id,
        timestamp=now,
        user_id=bob.user_id,
        human_handle=bob.human_handle,
        public_key=bob.public_key,
        profile=UserProfile.ADMIN,
    ).dump_and_sign(alice.signing_key)
    assert isinstance(certif, bytes)

    unsecure = UserCertificateContent.unsecure_load(certif)
    assert isinstance(unsecure, UserCertificateContent)
    assert unsecure.user_id == bob.user_id
    assert unsecure.public_key == bob.public_key
    assert unsecure.timestamp == now
    assert unsecure.author == alice.device_id
    assert unsecure.profile == UserProfile.ADMIN

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

    with pytest.raises(DataError) as exc:
        UserCertificateContent.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:
        UserCertificateContent.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:
        UserCertificateContent.verify_and_load(
            certif,
            author_verify_key=alice.verify_key,
            expected_author=alice.device_id,
            expected_user=mallory.user_id,
        )
    assert str(exc.value) == "Invalid user ID: expected `mallory`, got `bob`"
Esempio n. 2
0
def test_user_certificate_supports_legacy_is_admin_field(alice, bob):
    now = pendulum_now()
    certif = UserCertificateContent(
        author=bob.device_id,
        timestamp=now,
        user_id=alice.user_id,
        human_handle=None,
        public_key=alice.public_key,
        profile=alice.profile,
    )

    # Manually craft a certificate in legacy format
    raw_legacy_certif = {
        "type": "user_certificate",
        "author": bob.device_id,
        "timestamp": now,
        "user_id": alice.user_id,
        "public_key": alice.public_key.encode(),
        "is_admin": True,
    }
    dumped_legacy_certif = bob.signing_key.sign(
        zlib.compress(packb(raw_legacy_certif)))

    # Make sure the legacy format can be loaded
    legacy_certif = UserCertificateContent.verify_and_load(
        dumped_legacy_certif,
        author_verify_key=bob.verify_key,
        expected_author=bob.device_id,
        expected_user=alice.user_id,
        expected_human_handle=None,
    )
    assert legacy_certif == certif

    # Manually decode new format to check it is compatible with legacy
    dumped_certif = certif.dump_and_sign(bob.signing_key)
    raw_certif = unpackb(zlib.decompress(bob.verify_key.verify(dumped_certif)))
    assert raw_certif == {
        **raw_legacy_certif, "profile": alice.profile.value,
        "human_handle": None
    }
Esempio n. 3
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"}
Esempio n. 4
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
Esempio n. 5
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"})
Esempio n. 6
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"})
Esempio n. 7
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
Esempio n. 8
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"})