async def test_device_claim_invalid_returned_certificate( running_backend, backend, alice, bob, backend_claim_response_hook): hooks = backend_claim_response_hook device_count = 0 async def _do_test(): nonlocal device_count device_count += 1 new_device_id = DeviceID(f"{alice.user_id}@newdev{device_count}") token = generate_invitation_token() exception_occured = False async def _from_alice(): await invite_and_create_device(alice, new_device_id.device_name, token=token) async def _from_new_device(): nonlocal exception_occured with pytest.raises(InviteClaimCryptoError): await claim_device(alice.organization_addr, new_device_id, token=token) exception_occured = True await _invite_and_claim(running_backend, _from_alice, _from_new_device, event_name="device.claimed") assert exception_occured # Invalid data hooks["device_certificate"] = lambda x: b"dummy" await _do_test() # Certificate author differs from invitation creator def bob_sign(certif): return certif.evolve(author=bob.device_id).dump_and_sign( author_signkey=bob.signing_key) hooks["device_certificate"] = lambda raw: bob_sign( DeviceCertificateContent.unsecure_load(raw)) await _do_test() # Certificate info doesn't correspond to created user hooks["device_certificate"] = ( lambda raw: DeviceCertificateContent.unsecure_load(raw).evolve( device_id=bob.device_id).dump_and_sign(author_signkey=alice. signing_key)) await _do_test()
async def _alice_nd_claim(): async with apiv1_backend_anonymous_cmds_factory(alice.organization_addr) as cmds: ret = await cmds.device_get_invitation_creator(nd_id) assert ret["status"] == "ok" assert ret["trustchain"] == {"devices": [], "revoked_users": [], "users": []} creator = UserCertificateContent.unsecure_load(ret["user_certificate"]) creator_device = DeviceCertificateContent.unsecure_load(ret["device_certificate"]) assert creator_device.device_id.user_id == creator.user_id answer_private_key = PrivateKey.generate() encrypted_claim = APIV1_DeviceClaimContent( token=token, device_id=nd_id, verify_key=nd_signing_key.verify_key, answer_public_key=answer_private_key.public_key, ).dump_and_encrypt_for(recipient_pubkey=creator.public_key) with trio.fail_after(1): ret = await cmds.device_claim(nd_id, encrypted_claim) assert ret["status"] == "ok" assert ret["device_certificate"] == device_certificate answer = APIV1_DeviceClaimAnswerContent.decrypt_and_load_for( ret["encrypted_answer"], recipient_privkey=answer_private_key ) assert answer == APIV1_DeviceClaimAnswerContent( private_key=alice.private_key, user_manifest_id=alice.user_manifest_id, user_manifest_key=alice.user_manifest_key, )
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 _mallory_claim(): async with backend_anonymous_cmds_factory(mallory.organization_addr) as cmds: rep = await cmds.user_get_invitation_creator(mallory.user_id) assert rep["trustchain"] == {"devices": [], "revoked_users": [], "users": []} creator = UserCertificateContent.unsecure_load(rep["user_certificate"]) creator_device = DeviceCertificateContent.unsecure_load(rep["device_certificate"]) assert creator_device.device_id.user_id == creator.user_id encrypted_claim = UserClaimContent( device_id=mallory.device_id, token=token, public_key=mallory.public_key, verify_key=mallory.verify_key, ).dump_and_encrypt_for(recipient_pubkey=creator.public_key) with trio.fail_after(1): rep = await cmds.user_claim(mallory.user_id, encrypted_claim) assert rep["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()], )
def verify_key(self) -> VerifyKey: return DeviceCertificateContent.unsecure_load(self.device_certificate).verify_key
def test_unsecure_read_device_certificate_bad_data(): with pytest.raises(DataError): DeviceCertificateContent.unsecure_load(b"dummy")