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`"
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": "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"})
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": "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"})
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": "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"}