def verify_from(self, stranger: 'Character', message_kit: Union[UmbralMessageKit, bytes], signature: Signature = None, decrypt=False, label=None, ) -> bytes: """ Inverse of encrypt_for. :param stranger: A Character instance representing the actor whom the sender claims to be. We check the public key owned by this Character instance to verify. :param message_kit: the message to be (perhaps decrypted and) verified. :param signature: The signature to check. :param decrypt: Whether or not to decrypt the messages. :param label: A label used for decrypting messages encrypted under its associated policy encrypting key :return: Whether or not the signature is valid, the decrypted plaintext or NO_DECRYPTION_PERFORMED """ # # Optional Sanity Check # # In the spirit of duck-typing, we want to accept a message kit object, or bytes # If the higher-order object MessageKit is passed, we can perform an additional # eager sanity check before performing decryption. with contextlib.suppress(AttributeError): sender_verifying_key = stranger.stamp.as_umbral_pubkey() if message_kit.sender_verifying_key: if not message_kit.sender_verifying_key == sender_verifying_key: raise ValueError("This MessageKit doesn't appear to have come from {}".format(stranger)) # # Decrypt # signature_from_kit = None if decrypt: # We are decrypting the message; let's do that first and see what the sig header says. cleartext_with_sig_header = self.decrypt(message_kit=message_kit, label=label) sig_header, cleartext = default_constant_splitter(cleartext_with_sig_header, return_remainder=True) if sig_header == SIGNATURE_IS_ON_CIPHERTEXT: # The ciphertext is what is signed - note that for later. message = message_kit.ciphertext if not signature: raise ValueError("Can't check a signature on the ciphertext if don't provide one.") elif sig_header == SIGNATURE_TO_FOLLOW: # The signature follows in this cleartext - split it off. signature_from_kit, cleartext = signature_splitter(cleartext, return_remainder=True) message = cleartext else: # Not decrypting - the message is the object passed in as a message kit. Cast it. message = bytes(message_kit) cleartext = NO_DECRYPTION_PERFORMED # # Verify Signature # if signature and signature_from_kit: if signature != signature_from_kit: raise ValueError( "The MessageKit has a Signature, but it's not the same one you provided. Something's up.") signature_to_use = signature or signature_from_kit if signature_to_use: is_valid = signature_to_use.verify(message, sender_verifying_key) # FIXME: Message is undefined here if not is_valid: raise InvalidSignature("Signature for message isn't valid: {}".format(signature_to_use)) else: raise InvalidSignature("No signature provided -- signature presumed invalid.") return cleartext
def learn_from_teacher_node(self, eager=True): """ Sends a request to node_url to find out about known nodes. """ self._learning_round += 1 try: current_teacher = self.current_teacher_node() except self.NotEnoughTeachers as e: self.log.warn("Can't learn right now: {}".format(e.args[0])) return if Teacher in self.__class__.__bases__: announce_nodes = [self] else: announce_nodes = None unresponsive_nodes = set() try: # TODO: Streamline path generation certificate_filepath = self.node_storage.generate_certificate_filepath( checksum_address=current_teacher.checksum_public_address) response = self.network_middleware.get_nodes_via_rest( node=current_teacher, nodes_i_need=self._node_ids_to_learn_about_immediately, announce_nodes=announce_nodes, fleet_checksum=self.known_nodes.checksum) except NodeSeemsToBeDown as e: unresponsive_nodes.add(current_teacher) self.log.info("Bad Response from teacher: {}:{}.".format( current_teacher, e)) return finally: self.cycle_teacher_node() # # Before we parse the response, let's handle some edge cases. if response.status_code == 204: # In this case, this node knows about no other nodes. Hopefully we've taught it something. if response.content == b"": return NO_KNOWN_NODES # In the other case - where the status code is 204 but the repsonse isn't blank - we'll keep parsing. # It's possible that our fleet states match, and we'll check for that later. elif response.status_code != 200: self.log.info("Bad response from teacher {}: {} - {}".format( current_teacher, response, response.content)) return try: signature, node_payload = signature_splitter(response.content, return_remainder=True) except BytestringSplittingError as e: self.log.warn(e.args[0]) return try: self.verify_from(current_teacher, node_payload, signature=signature) except current_teacher.InvalidSignature: # TODO: What to do if the teacher improperly signed the node payload? raise # End edge case handling. # fleet_state_checksum_bytes, fleet_state_updated_bytes, node_payload = FleetStateTracker.snapshot_splitter( node_payload, return_remainder=True) current_teacher.last_seen = maya.now() # TODO: This is weird - let's get a stranger FleetState going. checksum = fleet_state_checksum_bytes.hex() # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node. from nucypher.characters.lawful import Ursula if constant_or_bytes(node_payload) is FLEET_STATES_MATCH: current_teacher.update_snapshot( checksum=checksum, updated=maya.MayaDT( int.from_bytes(fleet_state_updated_bytes, byteorder="big")), number_of_known_nodes=len(self.known_nodes)) return FLEET_STATES_MATCH node_list = Ursula.batch_from_bytes( node_payload, federated_only=self.federated_only) # TODO: 466 current_teacher.update_snapshot(checksum=checksum, updated=maya.MayaDT( int.from_bytes( fleet_state_updated_bytes, byteorder="big")), number_of_known_nodes=len(node_list)) new_nodes = [] for node in node_list: if GLOBAL_DOMAIN not in self.learning_domains: if not self.learning_domains.intersection( node.serving_domains): continue # This node is not serving any of our domains. # First, determine if this is an outdated representation of an already known node. with suppress(KeyError): already_known_node = self.known_nodes[ node.checksum_public_address] if not node.timestamp > already_known_node.timestamp: self.log.debug("Skipping already known node {}".format( already_known_node)) # This node is already known. We can safely continue to the next. continue certificate_filepath = self.node_storage.store_node_certificate( certificate=node.certificate) try: if eager: node.verify_node( self.network_middleware, accept_federated_only=self.federated_only, # TODO: 466 certificate_filepath=certificate_filepath) self.log.debug("Verified node: {}".format( node.checksum_public_address)) else: node.validate_metadata( accept_federated_only=self.federated_only) # TODO: 466 # This block is a mess of eagerness. This can all be done better lazily. except NodeSeemsToBeDown as e: self.log.info( f"Can't connect to {node} to verify it right now.") except node.InvalidNode: # TODO: Account for possibility that stamp, rather than interface, was bad. self.log.warn(node.invalid_metadata_message.format(node)) except node.SuspiciousActivity: message = "Suspicious Activity: Discovered node with bad signature: {}. " \ "Propagated by: {}".format(current_teacher.checksum_public_address, teacher_uri) self.log.warn(message) else: new = self.remember_node(node, record_fleet_state=False) if new: new_nodes.append(node) self._adjust_learning(new_nodes) learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new." self.log.info( learning_round_log_message.format(self._learning_round, current_teacher, len(node_list), len(new_nodes)), ) if new_nodes: self.known_nodes.record_fleet_state() for node in new_nodes: self.node_storage.store_node_certificate( certificate=node.certificate) return new_nodes
def learn_from_teacher_node(self, eager=True): """ Sends a request to node_url to find out about known nodes. """ self._learning_round += 1 try: current_teacher = self.current_teacher_node() except self.NotEnoughTeachers as e: self.log.warn("Can't learn right now: {}".format(e.args[0])) return rest_url = current_teacher.rest_interface # TODO: Name this..? # TODO: Do we really want to try to learn about all these nodes instantly? # Hearing this traffic might give insight to an attacker. if VerifiableNode in self.__class__.__bases__: announce_nodes = [self] else: announce_nodes = None unresponsive_nodes = set() try: # TODO: Streamline path generation certificate_filepath = current_teacher.get_certificate_filepath( certificates_dir=self.known_certificates_dir) response = self.network_middleware.get_nodes_via_rest(url=rest_url, nodes_i_need=self._node_ids_to_learn_about_immediately, announce_nodes=announce_nodes, certificate_filepath=certificate_filepath) except requests.exceptions.ConnectionError as e: unresponsive_nodes.add(current_teacher) teacher_rest_info = current_teacher.rest_information()[0] # TODO: This error isn't necessarily "no repsonse" - let's maybe pass on the text of the exception here. self.log.info("No Response from teacher: {}:{}.".format(teacher_rest_info.host, teacher_rest_info.port)) self.cycle_teacher_node() return if response.status_code != 200: raise RuntimeError("Bad response from teacher: {} - {}".format(response, response.content)) signature, nodes = signature_splitter(response.content, return_remainder=True) # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node. from nucypher.characters.lawful import Ursula node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) # TODO: 466 new_nodes = [] for node in node_list: try: if eager: certificate_filepath = current_teacher.get_certificate_filepath( certificates_dir=self.known_certificates_dir) node.verify_node(self.network_middleware, accept_federated_only=self.federated_only, # TODO: 466 certificate_filepath=certificate_filepath) self.log.debug("Verified node: {}".format(node.checksum_public_address)) else: node.validate_metadata(accept_federated_only=self.federated_only) # TODO: 466 except node.SuspiciousActivity: # TODO: Account for possibility that stamp, rather than interface, was bad. message = "Suspicious Activity: Discovered node with bad signature: {}. " \ "Propagated by: {}".format(current_teacher.checksum_public_address, rest_url) self.log.warn(message) new = self.remember_node(node) if new: new_nodes.append(node) self._adjust_learning(new_nodes) learning_round_log_message = "Learning round {}. Teacher: {} knew about {} nodes, {} were new." current_teacher.last_seen = maya.now() self.cycle_teacher_node() self.log.info(learning_round_log_message.format(self._learning_round, current_teacher, len(node_list), len(new_nodes)), ) if new_nodes and self.known_certificates_dir: for node in new_nodes: node.save_certificate_to_disk(self.known_certificates_dir, force=True) return new_nodes
def verify_from( self, actor_whom_sender_claims_to_be: "Character", message_kit: Union[UmbralMessageKit, bytes], signature: Signature = None, decrypt=False, delegator_signing_key: UmbralPublicKey = None, ) -> tuple: """ Inverse of encrypt_for. :param actor_that_sender_claims_to_be: A Character instance representing the actor whom the sender claims to be. We check the public key owned by this Character instance to verify. :param message_kit: the message to be (perhaps decrypted and) verified. :param signature: The signature to check. :param decrypt: Whether or not to decrypt the messages. :param delegator_signing_key: A signing key from the original delegator. This is used only when decrypting a MessageKit with an activated Capsule to check that the KFrag used to create each attached CFrag is the authentic KFrag initially created by the delegator. :return: Whether or not the signature is valid, the decrypted plaintext or NO_DECRYPTION_PERFORMED """ sender_pubkey_sig = actor_whom_sender_claims_to_be.stamp.as_umbral_pubkey( ) with suppress(AttributeError): if message_kit.sender_pubkey_sig: if not message_kit.sender_pubkey_sig == sender_pubkey_sig: raise ValueError( "This MessageKit doesn't appear to have come from {}". format(actor_whom_sender_claims_to_be)) signature_from_kit = None if decrypt: # We are decrypting the message; let's do that first and see what the sig header says. cleartext_with_sig_header = self.decrypt( message_kit, verifying_key=delegator_signing_key) sig_header, cleartext = default_constant_splitter( cleartext_with_sig_header, return_remainder=True) if sig_header == constants.SIGNATURE_IS_ON_CIPHERTEXT: # THe ciphertext is what is signed - note that for later. message = message_kit.ciphertext if not signature: raise ValueError( "Can't check a signature on the ciphertext if don't provide one." ) elif sig_header == constants.SIGNATURE_TO_FOLLOW: # The signature follows in this cleartext - split it off. signature_from_kit, cleartext = signature_splitter( cleartext, return_remainder=True) message = cleartext else: # Not decrypting - the message is the object passed in as a message kit. Cast it. message = bytes(message_kit) cleartext = constants.NO_DECRYPTION_PERFORMED if signature and signature_from_kit: if signature != signature_from_kit: raise ValueError( "The MessageKit has a Signature, but it's not the same one you provided. Something's up." ) signature_to_use = signature or signature_from_kit if signature_to_use: is_valid = signature_to_use.verify(message, sender_pubkey_sig) else: # Meh, we didn't even get a signature. Not much we can do. is_valid = False return is_valid, cleartext
def verify_from( self, stranger: 'Character', message_kit: Union[UmbralMessageKit, bytes], signature: Signature = None, decrypt=False, ) -> bytes: """ Inverse of encrypt_for. :param stranger: A Character instance representing the actor whom the sender claims to be. We check the public key owned by this Character instance to verify. :param message_kit: the message to be (perhaps decrypted and) verified. :param signature: The signature to check. :param decrypt: Whether or not to decrypt the messages. :return: Whether or not the signature is valid, the decrypted plaintext or NO_DECRYPTION_PERFORMED """ sender_pubkey_sig = stranger.stamp.as_umbral_pubkey() with suppress(AttributeError): if message_kit.sender_pubkey_sig: if not message_kit.sender_pubkey_sig == sender_pubkey_sig: raise ValueError( "This MessageKit doesn't appear to have come from {}". format(stranger)) signature_from_kit = None if decrypt: # Let's try to get the label from the Stranger. try: label = stranger.label except AttributeError: # The Stranger has no idea what we're talking about. Nothing to do here. label = None # We are decrypting the message; let's do that first and see what the sig header says. cleartext_with_sig_header = self.decrypt(message_kit=message_kit, label=label) sig_header, cleartext = default_constant_splitter( cleartext_with_sig_header, return_remainder=True) if sig_header == SIGNATURE_IS_ON_CIPHERTEXT: # THe ciphertext is what is signed - note that for later. message = message_kit.ciphertext if not signature: raise ValueError( "Can't check a signature on the ciphertext if don't provide one." ) elif sig_header == SIGNATURE_TO_FOLLOW: # The signature follows in this cleartext - split it off. signature_from_kit, cleartext = signature_splitter( cleartext, return_remainder=True) message = cleartext else: # Not decrypting - the message is the object passed in as a message kit. Cast it. message = bytes(message_kit) cleartext = NO_DECRYPTION_PERFORMED if signature and signature_from_kit: if signature != signature_from_kit: raise ValueError( "The MessageKit has a Signature, but it's not the same one you provided. Something's up." ) signature_to_use = signature or signature_from_kit if signature_to_use: is_valid = signature_to_use.verify(message, sender_pubkey_sig) if not is_valid: raise stranger.InvalidSignature( "Signature for message isn't valid: {}".format( signature_to_use)) else: raise self.InvalidSignature( "No signature provided -- signature presumed invalid.") return cleartext