def new_user_and_device(self, is_admin, certifier_id, certifier_key): device_id = self.next_device_id() local_device = local_device_factory(device_id, org=coolorg) self.local_devices[device_id] = local_device user = UserCertificateContent( author=certifier_id, timestamp=pendulum_now(), user_id=local_device.user_id, human_handle=local_device.human_handle, public_key=local_device.public_key, profile=UserProfile.ADMIN if is_admin else UserProfile.STANDARD, ) self.users_content[device_id.user_id] = user self.users_certifs[device_id.user_id] = user.dump_and_sign(certifier_key) device = DeviceCertificateContent( author=certifier_id, timestamp=pendulum_now(), device_id=local_device.device_id, device_label=local_device.device_label, verify_key=local_device.verify_key, ) self.devices_content[local_device.device_id] = device self.devices_certifs[local_device.device_id] = device.dump_and_sign(certifier_key) return device_id
async def bootstrap_organization( cmds: APIV1_BackendAnonymousCmds, human_handle: Optional[HumanHandle], device_label: Optional[str], ) -> LocalDevice: root_signing_key = SigningKey.generate() root_verify_key = root_signing_key.verify_key organization_addr = BackendOrganizationAddr.build( backend_addr=cmds.addr, organization_id=cmds.addr.organization_id, root_verify_key=root_verify_key, ) device = generate_new_device( organization_addr=organization_addr, profile=UserProfile.ADMIN, human_handle=human_handle, device_label=device_label, ) now = pendulum_now() user_certificate = UserCertificateContent( author=None, timestamp=now, user_id=device.user_id, human_handle=device.human_handle, public_key=device.public_key, profile=device.profile, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=None, timestamp=now, device_id=device.device_id, device_label=device.device_label, verify_key=device.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(root_signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( root_signing_key) device_certificate = device_certificate.dump_and_sign(root_signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( root_signing_key) rep = await cmds.organization_bootstrap( organization_id=cmds.addr.organization_id, bootstrap_token=cmds.addr.token, root_verify_key=root_verify_key, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) _check_rep(rep, step_name="organization bootstrap") return device
async def test_redacted_certificates_cannot_contain_sensitive_data( alice_backend_sock, alice, mallory): now = pendulum.now() user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(alice.signing_key) device_certificate = device_certificate.dump_and_sign(alice.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( alice.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( alice.signing_key) with freeze_time(now): rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=user_certificate, redacted_device_certificate=redacted_device_certificate, ) assert rep == { "status": "invalid_data", "reason": "Redacted User certificate must not contain a human_handle field.", } rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=device_certificate, ) assert rep == { "status": "invalid_data", "reason": "Redacted Device certificate must not contain a device_label field.", }
def local_device_to_backend_user( device: LocalDevice, certifier: Union[LocalDevice, OrganizationFullData] ) -> Tuple[BackendUser, BackendDevice]: if isinstance(certifier, OrganizationFullData): certifier_id = None certifier_signing_key = certifier.root_signing_key else: certifier_id = certifier.device_id certifier_signing_key = certifier.signing_key now = pendulum.now() user_certificate = UserCertificateContent( author=certifier_id, timestamp=now, user_id=device.user_id, public_key=device.public_key, profile=device.profile, human_handle=device.human_handle, ) device_certificate = DeviceCertificateContent( author=certifier_id, timestamp=now, device_id=device.device_id, device_label=device.device_label, verify_key=device.verify_key, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) redacted_device_certificate = device_certificate.evolve(device_label=None) user = BackendUser( user_id=device.user_id, human_handle=device.human_handle, profile=device.profile, user_certificate=user_certificate.dump_and_sign(certifier_signing_key), redacted_user_certificate=redacted_user_certificate.dump_and_sign( certifier_signing_key), user_certifier=certifier_id, created_on=now, ) first_device = BackendDevice( device_id=device.device_id, device_label=device.device_label, device_certificate=device_certificate.dump_and_sign( certifier_signing_key), redacted_device_certificate=redacted_device_certificate.dump_and_sign( certifier_signing_key), device_certifier=certifier_id, created_on=now, ) return user, first_device
async def _register_new_user( cmds: BackendAuthenticatedCmds, author: LocalDevice, device_label: Optional[str], human_handle: Optional[HumanHandle], profile: UserProfile, ) -> LocalDevice: new_device = generate_new_device( organization_addr=cmds.addr, device_label=device_label, human_handle=human_handle, profile=profile, ) now = pendulum_now() user_certificate = UserCertificateContent( author=author.device_id, timestamp=now, user_id=new_device.device_id.user_id, human_handle=new_device.human_handle, public_key=new_device.public_key, profile=new_device.profile, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=author.device_id, timestamp=now, device_id=new_device.device_id, device_label=new_device.device_label, verify_key=new_device.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(author.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( author.signing_key) device_certificate = device_certificate.dump_and_sign(author.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( author.signing_key) rep = await cmds.user_create( user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) if rep["status"] != "ok": raise RuntimeError(f"Cannot create user: {rep}") return new_device
async def test_user_create_not_matching_user_device(alice_backend_sock, alice, bob, mallory): now = pendulum.now() user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(alice.signing_key) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=bob.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=user_certificate, redacted_device_certificate=device_certificate, ) assert rep == { "status": "invalid_data", "reason": "Device and User must have the same user ID.", }
async def test_user_create_certificate_too_old(alice_backend_sock, alice, mallory): too_old = pendulum.datetime(2000, 1, 1) now = too_old.add(seconds=INVITATION_VALIDITY + 1) user_certificate = UserCertificateContent( author=alice.device_id, timestamp=too_old, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(alice.signing_key) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=too_old, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) with freeze_time(now): rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=user_certificate, redacted_device_certificate=device_certificate, ) assert rep == { "status": "invalid_certification", "reason": "Invalid timestamp in certificate.", }
async def test_user_create_already_exists(alice_backend_sock, alice, bob): now = pendulum.now() user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=bob.user_id, human_handle=None, public_key=bob.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(alice.signing_key) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=bob.device_id, device_label=None, verify_key=bob.verify_key, ).dump_and_sign(alice.signing_key) rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=user_certificate, redacted_device_certificate=device_certificate, ) assert rep == { "status": "already_exists", "reason": f"User `{bob.user_id}` already exists" }
async def test_user_create_not_matching_certified_on(alice_backend_sock, alice, mallory): date1 = pendulum.datetime(2000, 1, 1) date2 = date1.add(seconds=1) user_certificate = UserCertificateContent( author=alice.device_id, timestamp=date1, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(alice.signing_key) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=date2, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) with freeze_time(date1): rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=user_certificate, redacted_device_certificate=device_certificate, ) assert rep == { "status": "invalid_data", "reason": "Device and User certificates must have the same timestamp.", }
async def test_user_create_human_handle_with_revoked_previous_one( alice_backend_sock, alice, bob, backend_data_binder): # First revoke bob await backend_data_binder.bind_revocation(user_id=bob.user_id, certifier=alice) # Now recreate another user with bob's human handle now = pendulum.now() bob2_device_id = DeviceID("bob2@dev1") user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=bob2_device_id.user_id, human_handle=bob.human_handle, public_key=bob.public_key, profile=UserProfile.STANDARD, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=bob2_device_id, device_label=bob. device_label, # Device label doesn't have to be unique verify_key=bob.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(alice.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( alice.signing_key) device_certificate = device_certificate.dump_and_sign(alice.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( alice.signing_key) rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) assert rep == {"status": "ok"}
async def test_user_create_human_handle_already_exists(alice_backend_sock, alice, bob): now = pendulum.now() bob2_device_id = DeviceID("bob2@dev1") user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=bob2_device_id.user_id, human_handle=bob.human_handle, public_key=bob.public_key, profile=UserProfile.STANDARD, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=bob2_device_id, device_label="dev2", verify_key=bob.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) user_certificate = user_certificate.dump_and_sign(alice.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( alice.signing_key) device_certificate = device_certificate.dump_and_sign(alice.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( alice.signing_key) rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) assert rep == { "status": "already_exists", "reason": f"Human handle `{bob.human_handle}` already corresponds to a non-revoked user", }
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 test_user_create_invalid_certificate(alice_backend_sock, alice, bob, mallory): now = pendulum.now() good_user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(alice.signing_key) good_device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) bad_user_certificate = UserCertificateContent( author=bob.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ).dump_and_sign(bob.signing_key) bad_device_certificate = DeviceCertificateContent( author=bob.device_id, timestamp=now, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ).dump_and_sign(bob.signing_key) for cu, cd in [ (good_user_certificate, bad_device_certificate), (bad_user_certificate, good_device_certificate), (bad_user_certificate, bad_device_certificate), ]: rep = await user_create( alice_backend_sock, user_certificate=cu, device_certificate=cd, redacted_user_certificate=good_user_certificate, redacted_device_certificate=good_device_certificate, ) assert rep == { "status": "invalid_certification", "reason": "Invalid certification data (Signature was forged or corrupt).", } # Same thing for the redacted part for cu, cd in [ (good_user_certificate, bad_device_certificate), (bad_user_certificate, good_device_certificate), (bad_user_certificate, bad_device_certificate), ]: rep = await user_create( alice_backend_sock, user_certificate=good_user_certificate, device_certificate=good_device_certificate, redacted_user_certificate=cu, redacted_device_certificate=cd, ) assert rep == { "status": "invalid_certification", "reason": "Invalid certification data (Signature was forged or corrupt).", }
async def do_create_new_user( self, author: LocalDevice, device_label: Optional[str], human_handle: Optional[HumanHandle], profile: UserProfile, ) -> None: device_id = DeviceID.new() try: now = pendulum_now() user_certificate = UserCertificateContent( author=author.device_id, timestamp=now, user_id=device_id.user_id, human_handle=human_handle, public_key=self._public_key, profile=profile, ) redacted_user_certificate = user_certificate.evolve( human_handle=None) device_certificate = DeviceCertificateContent( author=author.device_id, timestamp=now, device_id=device_id, device_label=device_label, verify_key=self._verify_key, ) redacted_device_certificate = device_certificate.evolve( device_label=None) user_certificate = user_certificate.dump_and_sign( author.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( author.signing_key) device_certificate = device_certificate.dump_and_sign( author.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( author.signing_key) except DataError as exc: raise InviteError( f"Cannot generate device certificate: {exc}") from exc rep = await self._cmds.user_create( user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) if rep["status"] != "ok": raise InviteError(f"Cannot create device: {rep}") try: payload = InviteUserConfirmation( device_id=device_id, device_label=device_label, human_handle=human_handle, profile=profile, root_verify_key=author.root_verify_key, ).dump_and_encrypt(key=self._shared_secret_key) except DataError as exc: raise InviteError( "Cannot generate InviteUserConfirmation payload") from exc rep = await self._cmds.invite_4_greeter_communicate(token=self.token, payload=payload) _check_rep(rep, step_name="step 4 (confirmation exchange)") await self._cmds.invite_delete(token=self.token, reason=InvitationDeletedReason.FINISHED)
async def test_user_create_bad_redacted_user_certificate( alice_backend_sock, alice, mallory): now = pendulum.now() device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, device_label=None, # Can be used as regular and redacted certificate verify_key=mallory.verify_key, ).dump_and_sign(alice.signing_key) user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=UserProfile.STANDARD, ) good_redacted_user_certificate = user_certificate.evolve(human_handle=None) user_certificate = user_certificate.dump_and_sign(alice.signing_key) for bad_redacted_user_certificate in ( good_redacted_user_certificate.evolve(timestamp=now.add( seconds=1)), good_redacted_user_certificate.evolve(user_id=alice.user_id), good_redacted_user_certificate.evolve(public_key=alice.public_key), good_redacted_user_certificate.evolve( profile=UserProfile.OUTSIDER), ): rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=bad_redacted_user_certificate. dump_and_sign(alice.signing_key), redacted_device_certificate=device_certificate, ) assert rep == { "status": "invalid_data", "reason": "Redacted User certificate differs from User certificate.", } # Missing redacted certificate is not allowed as well rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=None, redacted_device_certificate=device_certificate, ) assert rep == { "status": "bad_message", "reason": "Invalid message.", "errors": { "redacted_user_certificate": ["Missing data for required field."] }, } # Finally just make sure good was really good rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=good_redacted_user_certificate.dump_and_sign( alice.signing_key), redacted_device_certificate=device_certificate, ) assert rep == {"status": "ok"}
async def test_user_create_ok(backend, backend_sock_factory, alice_backend_sock, alice, mallory, profile, with_labels): now = pendulum.now() user_certificate = UserCertificateContent( author=alice.device_id, timestamp=now, user_id=mallory.user_id, human_handle=mallory.human_handle, public_key=mallory.public_key, profile=profile, ) redacted_user_certificate = user_certificate.evolve(human_handle=None) device_certificate = DeviceCertificateContent( author=alice.device_id, timestamp=now, device_id=mallory.device_id, device_label=mallory.device_label, verify_key=mallory.verify_key, ) redacted_device_certificate = device_certificate.evolve(device_label=None) if not with_labels: user_certificate = redacted_user_certificate device_certificate = redacted_device_certificate user_certificate = user_certificate.dump_and_sign(alice.signing_key) device_certificate = device_certificate.dump_and_sign(alice.signing_key) redacted_user_certificate = redacted_user_certificate.dump_and_sign( alice.signing_key) redacted_device_certificate = redacted_device_certificate.dump_and_sign( alice.signing_key) rep = await user_create( alice_backend_sock, user_certificate=user_certificate, device_certificate=device_certificate, redacted_user_certificate=redacted_user_certificate, redacted_device_certificate=redacted_device_certificate, ) assert rep == {"status": "ok"} # Make sure mallory can connect now async with backend_sock_factory(backend, mallory) as sock: rep = await user_get(sock, user_id=mallory.user_id) assert rep["status"] == "ok" # Check the resulting data in the backend backend_user, backend_device = await backend.user.get_user_with_device( mallory.organization_id, mallory.device_id) assert backend_user == User( user_id=mallory.user_id, human_handle=mallory.human_handle if with_labels else None, profile=profile, user_certificate=user_certificate, redacted_user_certificate=redacted_user_certificate, user_certifier=alice.device_id, created_on=now, ) assert backend_device == Device( device_id=mallory.device_id, device_label=mallory.device_label if with_labels else None, device_certificate=device_certificate, redacted_device_certificate=redacted_device_certificate, device_certifier=alice.device_id, created_on=now, )
def test_unsecure_read_user_certificate_bad_data(): with pytest.raises(DataError): UserCertificateContent.unsecure_load(b"dummy")
def _trustchain_data_factory(todo_devices, todo_users): data = TrustchainData(coolorg.organization_id, coolorg.root_verify_key) def _get_certifier_id_and_key(certifier): if not certifier: # Certified by root certifier_id = None certifier_key = coolorg.root_signing_key else: certifier_ld = data.get_local_device(certifier) if not certifier_ld: raise RuntimeError(f"Missing `{certifier}` to sign creation of `{todo_device}`") certifier_id = certifier_ld.device_id certifier_key = certifier_ld.signing_key return certifier_id, certifier_key # First create all the devices for todo_device in todo_devices: local_device = local_device_factory(todo_device["id"], org=coolorg) data.add_local_device(local_device) # Generate device certificate certifier_id, certifier_key = _get_certifier_id_and_key(todo_device.get("certifier")) created_on = todo_device.get("created_on", now) device_certificate = DeviceCertificateContent( author=certifier_id, timestamp=created_on, device_id=local_device.device_id, device_label=local_device.device_label, verify_key=local_device.verify_key, ) data.add_device_certif( device_certificate, device_certificate.dump_and_sign(certifier_key) ) # Now deal with the users for todo_user in todo_users: local_user = next( (u for u in data.local_devices.values() if str(u.user_id) == todo_user["id"]), None ) if not local_user: raise RuntimeError(f"Missing device for user `{todo_user['id']}`") # Generate user certificate certifier_id, certifier_key = _get_certifier_id_and_key(todo_user.get("certifier")) created_on = todo_user.get("created_on", now) user_certif = UserCertificateContent( author=certifier_id, timestamp=created_on, user_id=local_user.user_id, human_handle=local_device.human_handle, public_key=local_user.public_key, profile=todo_user.get("profile", UserProfile.STANDARD), ) data.add_user_certif(user_certif, user_certif.dump_and_sign(certifier_key)) # Generate user revocation certificate if needed revoker = todo_user.get("revoker", None) if revoker: revoked_on = todo_user.get("revoked_on", now) revoker_ld = data.get_local_device(revoker) if not revoker_ld: raise RuntimeError( f"Missing `{revoker}` to sign revocation of `{todo_user['id']}`" ) revoked_user_certificate = RevokedUserCertificateContent( author=revoker_ld.device_id, timestamp=revoked_on, user_id=local_user.user_id ) data.add_revoked_user_certif( revoked_user_certificate, revoked_user_certificate.dump_and_sign(revoker_ld.signing_key), ) return data
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"})
def public_key(self) -> PublicKey: return UserCertificateContent.unsecure_load( self.user_certificate).public_key
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"}
def load_trustchain( self, users: Sequence[bytes] = (), revoked_users: Sequence[bytes] = (), devices: Sequence[bytes] = (), now: DateTime = None, ) -> Tuple[List[UserCertificateContent], List[RevokedUserCertificateContent], List[DeviceCertificateContent], ]: now = now or pendulum_now() users_states = {} devices_states = {} revoked_users_states = {} # Deserialize the certificates and filter the ones we already have in cache try: for certif in devices: unverified_device = DeviceCertificateContent.unsecure_load( certif) verified_device = self.get_device(unverified_device.device_id, now) if verified_device: devices_states[verified_device.device_id] = CertifState( certif, verified_device, True) else: devices_states[unverified_device.device_id] = CertifState( certif, unverified_device, False) for certif in users: unverified_user = UserCertificateContent.unsecure_load(certif) verified_user = self.get_user(unverified_user.user_id, now) if verified_user: users_states[verified_user.user_id] = CertifState( certif, verified_user, True) else: users_states[unverified_user.user_id] = CertifState( certif, unverified_user, False) for certif in revoked_users: unverified_revoked_user = RevokedUserCertificateContent.unsecure_load( certif) verified_revoked_user = self.get_revoked_user( unverified_revoked_user.user_id, now) if verified_revoked_user: revoked_users_states[ verified_revoked_user.user_id] = CertifState( certif, verified_revoked_user, True) else: revoked_users_states[ unverified_revoked_user.user_id] = CertifState( certif, unverified_revoked_user, False) except DataError as exc: raise TrustchainError(f"Invalid certificate: {exc}") from exc def _get_eventually_verified_user(user_id): try: return users_states[user_id].content except KeyError: return None def _get_eventually_verified_revoked_user(user_id): try: return revoked_users_states[user_id].content except KeyError: return None def _verify_created_by_root(certif, certif_cls, sign_chain): try: return certif_cls.verify_and_load( certif, author_verify_key=self.root_verify_key, expected_author=None) except DataError as exc: path = _build_signature_path(*sign_chain, "<Root Key>") raise TrustchainError( f"{path}: Invalid certificate: {exc}") from exc def _verify_created_by_device(certif, certif_cls, author_id, sign_chain): author_device = _recursive_verify_device(author_id, sign_chain) try: verified = certif_cls.verify_and_load( certif, author_verify_key=author_device.verify_key, expected_author=author_device.device_id, ) except DataError as exc: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Invalid certificate: {exc}") from exc # Author is either admin or signing one of it own devices verified_user_id = (verified.device_id.user_id if isinstance( verified, DeviceCertificateContent) else verified.user_id) if author_device.device_id.user_id != verified_user_id: author_user = _get_eventually_verified_user(author_id.user_id) if not author_user: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Missing user certificate for {author_id.user_id}" ) elif author_user.profile != UserProfile.ADMIN: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Invalid signature given {author_user.user_id} is not admin" ) # Also make sure author wasn't revoked at creation time author_revoked_user = _get_eventually_verified_revoked_user( author_id.user_id) if author_revoked_user and verified.timestamp > author_revoked_user.timestamp: path = _build_signature_path(*sign_chain, author_id) raise TrustchainError( f"{path}: Signature ({verified.timestamp}) is posterior " f"to user revocation ({author_revoked_user.timestamp})") return verified def _recursive_verify_device(device_id, signed_children=()): if device_id in signed_children: path = _build_signature_path(*signed_children, device_id) raise TrustchainError( f"{path}: Invalid signature loop detected") try: state = devices_states[device_id] except KeyError: path = _build_signature_path(*signed_children, device_id) raise TrustchainError( f"{path}: Missing device certificate for {device_id}") author = state.content.author if author is None: verified_device = _verify_created_by_root( state.certif, DeviceCertificateContent, sign_chain=(*signed_children, device_id)) else: verified_device = _verify_created_by_device( state.certif, DeviceCertificateContent, author, sign_chain=(*signed_children, device_id), ) return verified_device def _verify_user(unverified_content, certif): author = unverified_content.author user_id = unverified_content.user_id if author is None: verified_user = _verify_created_by_root( certif, UserCertificateContent, sign_chain=(f"{user_id}'s creation", )) elif author.user_id == user_id: raise TrustchainError( f"{user_id}: Invalid self-signed user certificate") else: verified_user = _verify_created_by_device( certif, UserCertificateContent, author, sign_chain=(f"{user_id}'s creation", )) return verified_user def _verify_revoked_user(unverified_content, certif): author = unverified_content.author user_id = unverified_content.user_id if author is None: verified_revoked_user = _verify_created_by_root( certif, RevokedUserCertificateContent, sign_chain=(f"{user_id}'s revocation", )) elif author.user_id == user_id: raise TrustchainError( f"{user_id}: Invalid self-signed user revocation certificate" ) else: verified_revoked_user = _verify_created_by_device( certif, RevokedUserCertificateContent, author, sign_chain=(f"{user_id}'s revocation", ), ) return verified_revoked_user # Verified what need to be and populate the cache with them for certif_state in devices_states.values(): if not certif_state.verified: certif_state.content = _recursive_verify_device( certif_state.content.device_id) for certif_state in users_states.values(): if not certif_state.verified: certif_state.content = _verify_user(certif_state.content, certif_state.certif) for certif_state in revoked_users_states.values(): if not certif_state.verified: certif_state.content = _verify_revoked_user( certif_state.content, certif_state.certif) # Finally populate the cache for certif_state in devices_states.values(): if not certif_state.verified: self._devices_cache[certif_state.content.device_id] = ( now, certif_state.content) for certif_state in users_states.values(): if not certif_state.verified: self._users_cache[certif_state.content.user_id] = ( now, certif_state.content) for certif_state in revoked_users_states.values(): if not certif_state.verified: self._revoked_users_cache[certif_state.content.user_id] = ( now, certif_state.content, ) return ( [state.content for state in users_states.values()], [state.content for state in revoked_users_states.values()], [state.content for state in devices_states.values()], )
async def test_organization_bootstrap_bad_data( backend_data_binder, apiv1_backend_sock_factory, organization_factory, local_device_factory, backend, coolorg, ): neworg = organization_factory("NewOrg") newalice = local_device_factory("alice@dev1", neworg) await backend_data_binder.bind_organization(neworg) bad_organization_id = coolorg.organization_id good_organization_id = neworg.organization_id root_signing_key = neworg.root_signing_key bad_root_signing_key = coolorg.root_signing_key good_bootstrap_token = neworg.bootstrap_token bad_bootstrap_token = coolorg.bootstrap_token good_rvk = neworg.root_verify_key bad_rvk = coolorg.root_verify_key good_device_id = newalice.device_id good_user_id = newalice.user_id bad_user_id = UserID("dummy") public_key = newalice.public_key verify_key = newalice.verify_key now = pendulum.now() bad_now = now.subtract(seconds=1) good_cu = UserCertificateContent( author=None, timestamp=now, user_id=good_user_id, public_key=public_key, profile=UserProfile.ADMIN, human_handle=newalice.human_handle, ) good_redacted_cu = good_cu.evolve(human_handle=None) good_cd = DeviceCertificateContent( author=None, timestamp=now, device_id=good_device_id, device_label=newalice.device_label, verify_key=verify_key, ) good_redacted_cd = good_cd.evolve(device_label=None) bad_now_cu = good_cu.evolve(timestamp=bad_now) bad_now_cd = good_cd.evolve(timestamp=bad_now) bad_now_redacted_cu = good_redacted_cu.evolve(timestamp=bad_now) bad_now_redacted_cd = good_redacted_cd.evolve(timestamp=bad_now) bad_id_cu = good_cu.evolve(user_id=bad_user_id) bad_not_admin_cu = good_cu.evolve(profile=UserProfile.STANDARD) bad_key_cu = good_cu.dump_and_sign(bad_root_signing_key) bad_key_cd = good_cd.dump_and_sign(bad_root_signing_key) good_cu = good_cu.dump_and_sign(root_signing_key) good_redacted_cu = good_redacted_cu.dump_and_sign(root_signing_key) good_cd = good_cd.dump_and_sign(root_signing_key) good_redacted_cd = good_redacted_cd.dump_and_sign(root_signing_key) bad_now_cu = bad_now_cu.dump_and_sign(root_signing_key) bad_now_cd = bad_now_cd.dump_and_sign(root_signing_key) bad_now_redacted_cu = bad_now_redacted_cu.dump_and_sign(root_signing_key) bad_now_redacted_cd = bad_now_redacted_cd.dump_and_sign(root_signing_key) bad_id_cu = bad_id_cu.dump_and_sign(root_signing_key) bad_not_admin_cu = bad_not_admin_cu.dump_and_sign(root_signing_key) for i, (status, organization_id, *params) in enumerate([ ("not_found", good_organization_id, bad_bootstrap_token, good_cu, good_cd, good_rvk), ( "already_bootstrapped", bad_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, good_cu, good_cd, bad_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_now_cu, good_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_id_cu, good_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, bad_key_cu, good_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, bad_now_cd, good_rvk, ), ( "invalid_certification", good_organization_id, good_bootstrap_token, good_cu, bad_key_cd, good_rvk, ), ( "invalid_data", good_organization_id, good_bootstrap_token, bad_not_admin_cu, good_cd, good_rvk, ), # Tests with redacted certificates ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_cu, # Not redacted ! good_redacted_cd, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, good_cd, # Not redacted ! ), ( "bad_message", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, None, # None not allowed good_redacted_cd, ), ( "bad_message", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, None, # None not allowed ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, bad_now_redacted_cu, good_redacted_cd, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, bad_now_redacted_cd, ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, _missing, # Must proved redacted_device if redacted user is present ), ( "invalid_data", good_organization_id, good_bootstrap_token, good_cu, good_cd, good_rvk, _missing, # Must proved redacted_device if redacted user is present good_redacted_cd, ), ]): print(f"sub test {i}") async with apiv1_backend_sock_factory(backend, organization_id) as sock: rep = await organization_bootstrap(sock, *params) assert rep["status"] == status # Finally cheap test to make sure our "good" data were really good async with apiv1_backend_sock_factory(backend, good_organization_id) as sock: rep = await organization_bootstrap( sock, good_bootstrap_token, good_cu, good_cd, good_rvk, good_redacted_cu, good_redacted_cd, ) assert rep["status"] == "ok"