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