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"})
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, )
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 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"})
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"}
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
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(), )
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"})
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"})
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
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"})