def test_vladimir_uses_his_own_signing_key(blockchain_alice, blockchain_ursulas): """ Similar to the attack above, but this time Vladimir makes his own interface signature using his own signing key, which he claims is Ursula's. """ his_target = list(blockchain_ursulas)[4] fraduluent_keys = CryptoPower(power_ups=Ursula._default_crypto_powerups) vladimir = Vladimir.from_target_ursula(target_ursula=his_target) message = vladimir._signable_interface_info_message() signature = vladimir._crypto_power.power_ups(SigningPower).sign( vladimir.timestamp_bytes() + message) vladimir._interface_signature_object = signature vladimir.substantiate_stamp( passphrase=TEST_URSULA_INSECURE_DEVELOPMENT_PASSWORD) # With this slightly more sophisticated attack, his metadata does appear valid. vladimir.validate_metadata() # However, the actual handshake proves him wrong. with pytest.raises(vladimir.InvalidNode): vladimir.verify_node(blockchain_alice.network_middleware)
def from_target_ursula(cls, target_ursula: Ursula, claim_signing_key: bool = False, attach_transacting_key: bool = True) -> 'Vladimir': """ Sometimes Vladimir seeks to attack or imitate a *specific* target Ursula. TODO: This is probably a more instructive method if it takes a bytes representation instead of the entire Ursula. """ try: from tests.utils.middleware import EvilMiddleWare except ImportError: raise DevelopmentInstallationRequired( importable_name='tests.utils.middleware.EvilMiddleWare') cls.network_middleware = EvilMiddleWare() crypto_power = CryptoPower( power_ups=target_ursula._default_crypto_powerups) if claim_signing_key: crypto_power.consume_power_up( SigningPower( public_key=target_ursula.stamp.as_umbral_pubkey())) if attach_transacting_key: cls.attach_transacting_key( blockchain=target_ursula.policy_agent.blockchain) vladimir = cls( is_me=True, crypto_power=crypto_power, db_filepath=cls.db_filepath, domains=[TEMPORARY_DOMAIN], block_until_ready=False, start_working_now=False, rest_host=target_ursula.rest_interface.host, rest_port=target_ursula.rest_interface.port, certificate=target_ursula.rest_server_certificate(), network_middleware=cls.network_middleware, checksum_address=cls.fraud_address, worker_address=cls.fraud_address, ######### Asshole. timestamp=target_ursula._timestamp, interface_signature=target_ursula._interface_signature, ######### ) return vladimir
def from_public_keys(cls, powers_and_material: Dict = None, verifying_key: Union[bytes, UmbralPublicKey] = None, encrypting_key: Union[bytes, UmbralPublicKey] = None, federated_only: bool = True, *args, **kwargs) -> 'Character': """ Sometimes we discover a Character and, at the same moment, learn the public parts of more of their powers. Here, we take a Dict (powers_and_material) in the format {CryptoPowerUp class: material}, where material can be bytes or UmbralPublicKey. Each item in the collection will have the CryptoPowerUp instantiated with the given material, and the resulting CryptoPowerUp instance consumed by the Character. Alternatively, you can pass directly a verifying public key (for SigningPower) and/or an encrypting public key (for DecryptionPower). # TODO: Need to be federated only until we figure out the best way to get the checksum_address in here. """ crypto_power = CryptoPower() if powers_and_material is None: powers_and_material = dict() if verifying_key: powers_and_material[SigningPower] = verifying_key if encrypting_key: powers_and_material[DecryptingPower] = encrypting_key for power_up, public_key in powers_and_material.items(): try: umbral_key = UmbralPublicKey.from_bytes(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(public_key=umbral_key)) return cls(is_me=False, federated_only=federated_only, crypto_power=crypto_power, *args, **kwargs)
def from_public_keys(cls, powers_and_keys: Dict, *args, **kwargs): """ Sometimes we discover a Character and, at the same moment, learn one or more of their public keys. Here, we take a Dict (powers_and_key_bytes) in the following format: {CryptoPowerUp class: public_key_bytes} Each item in the collection will have the CryptoPowerUp instantiated with the public_key_bytes, and the resulting CryptoPowerUp instance consumed by the Character. """ crypto_power = CryptoPower() for power_up, public_key in powers_and_keys.items(): try: umbral_key = UmbralPublicKey(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(pubkey=umbral_key)) return cls(is_me=False, crypto_power=crypto_power, *args, **kwargs)
def from_target_ursula(cls, target_ursula: Ursula, claim_signing_key: bool = False, attach_transacting_key: bool = True) -> 'Vladimir': """ Sometimes Vladimir seeks to attack or imitate a *specific* target Ursula. TODO: This is probably a more instructive method if it takes a bytes representation instead of the entire Ursula. """ crypto_power = CryptoPower( power_ups=target_ursula._default_crypto_powerups) if claim_signing_key: crypto_power.consume_power_up( SigningPower(pubkey=target_ursula.stamp.as_umbral_pubkey())) if attach_transacting_key: cls.attach_transacting_key(blockchain=target_ursula.blockchain) vladimir = cls( is_me=True, crypto_power=crypto_power, db_name=cls.db_name, db_filepath=cls.db_name, rest_host=target_ursula.rest_information()[0].host, rest_port=target_ursula.rest_information()[0].port, certificate=target_ursula.rest_server_certificate(), network_middleware=cls.network_middleware, checksum_address=cls.fraud_address, ######### Asshole. timestamp=target_ursula._timestamp, interface_signature=target_ursula._interface_signature_object, ######### ) cls.attach_transacting_key(blockchain=target_ursula.blockchain) return vladimir
def test_actor_without_signing_power_cannot_sign(): """ We can create a Character with no real CryptoPower to speak of. This Character can't even sign a message. """ cannot_sign = CryptoPower(power_ups=[]) non_signer = Character(crypto_power=cannot_sign) # The non-signer's stamp doesn't work for signing... with pytest.raises(NoSigningPower) as e_info: non_signer.stamp("something") # ...or as a way to cast the (non-existent) public key to bytes. with pytest.raises(NoSigningPower) as e_info: bytes(non_signer.stamp)
def from_target_ursula(cls, target_ursula, claim_signing_key=False): """ Sometimes Vladimir seeks to attack or imitate a *specific* target Ursula. TODO: This is probably a more instructive method if it takes a bytes representation instead of the entire Ursula. """ crypto_power = CryptoPower(power_ups=Ursula._default_crypto_powerups) if claim_signing_key: crypto_power.consume_power_up( SigningPower(pubkey=target_ursula.stamp.as_umbral_pubkey())) vladimir = cls(crypto_power=crypto_power, rest_host=target_ursula.rest_information()[0].host, rest_port=target_ursula.rest_information()[0].port, checksum_address=cls.fraud_address, certificate=target_ursula.rest_server_certificate(), is_me=False) vladimir._interface_signature_object = target_ursula._interface_signature_object # Asshole. cls.attach_transacting_key(blockchain=target_ursula.blockchain) return vladimir
def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid( blockchain_alice, idle_blockchain_policy, mining_ursulas): target = list(mining_ursulas)[2] # First, let's imagine that Alice has sampled a Vladimir while making this policy. vladimir = Ursula( crypto_power=CryptoPower(power_ups=Ursula._default_crypto_powerups), rest_host=target.rest_interface.host, rest_port=target.rest_interface.port, checksum_address= '0xE57bFE9F44b819898F47BF37E5AF72a0783e1141', # Fradulent address is_me=False) message = vladimir._signable_interface_info_message() signature = vladimir._crypto_power.power_ups(SigningPower).sign(message) vladimir.substantiate_stamp() vladimir._interface_signature_object = signature class FakeArrangement: federated = False with pytest.raises(vladimir.InvalidNode): idle_blockchain_policy.consider_arrangement( network_middleware=blockchain_alice.network_middleware, arrangement=FakeArrangement(), ursula=vladimir)
def __init__(self, domains: Set = (GLOBAL_DOMAIN, ), is_me: bool = True, federated_only: bool = False, blockchain: Blockchain = None, checksum_public_address: bytes = NO_BLOCKCHAIN_CONNECTION. bool_value(False), network_middleware: RestMiddleware = None, keyring_dir: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ self.federated_only = federated_only # type: bool # # Powers # if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) self._checksum_address = checksum_public_address # # Self-Character # if is_me is True: if not self.federated_only: self.blockchain = blockchain or Blockchain.connect() self.keyring_dir = keyring_dir # type: str self.treasure_maps = {} # type: dict self.network_middleware = network_middleware or RestMiddleware() # # Signing Power # try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Learner # Learner.__init__(self, domains=domains, network_middleware=network_middleware, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stanger-Character." ) self._stamp = StrangerStamp(self.public_keys(SigningPower)) self.keyring_dir = STRANGER self.network_middleware = STRANGER # # Decentralized # if not federated_only: if not checksum_public_address: raise ValueError( "No checksum_public_address provided while running in a non-federated mode." ) else: self._checksum_address = checksum_public_address # TODO: Check that this matches BlockchainPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_public_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_public_address == self.checksum_public_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_public_address)) # # Nicknames # try: self.nickname, self.nickname_metadata = nickname_from_seed( self.checksum_public_address) except SigningPower.not_found_error: if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL
class Character(Learner): """ A base-class for any character in our cryptography protocol narrative. """ _default_crypto_powerups = None _stamp = None _crashed = False from nucypher.network.protocols import SuspiciousActivity # Ship this exception with every Character. from nucypher.crypto.signing import InvalidSignature def __init__(self, domains: Set = (GLOBAL_DOMAIN, ), is_me: bool = True, federated_only: bool = False, blockchain: Blockchain = None, checksum_public_address: bytes = NO_BLOCKCHAIN_CONNECTION. bool_value(False), network_middleware: RestMiddleware = None, keyring_dir: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ self.federated_only = federated_only # type: bool # # Powers # if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) self._checksum_address = checksum_public_address # # Self-Character # if is_me is True: if not self.federated_only: self.blockchain = blockchain or Blockchain.connect() self.keyring_dir = keyring_dir # type: str self.treasure_maps = {} # type: dict self.network_middleware = network_middleware or RestMiddleware() # # Signing Power # try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Learner # Learner.__init__(self, domains=domains, network_middleware=network_middleware, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stanger-Character." ) self._stamp = StrangerStamp(self.public_keys(SigningPower)) self.keyring_dir = STRANGER self.network_middleware = STRANGER # # Decentralized # if not federated_only: if not checksum_public_address: raise ValueError( "No checksum_public_address provided while running in a non-federated mode." ) else: self._checksum_address = checksum_public_address # TODO: Check that this matches BlockchainPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_public_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_public_address == self.checksum_public_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_public_address)) # # Nicknames # try: self.nickname, self.nickname_metadata = nickname_from_seed( self.checksum_public_address) except SigningPower.not_found_error: if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL def __eq__(self, other) -> bool: try: other_stamp = other.stamp except (AttributeError, NoSigningPower): return False return bytes(self.stamp) == bytes(other_stamp) def __hash__(self): return int.from_bytes(bytes(self.stamp), byteorder="big") def __repr__(self): r = "({})⇀{}↽ ({})" try: r = r.format(self.__class__.__name__, self.nickname, self.checksum_public_address) except NoSigningPower: # TODO: ....yeah? r = r.format(self.__class__.__name__, self.nickname) return r @property def name(self): return self.__class__.__name__ @property def rest_interface(self): return self.rest_server.rest_url() @property def stamp(self): if self._stamp is NO_SIGNING_POWER: raise NoSigningPower elif not self._stamp: raise AttributeError("SignatureStamp has not been set up yet.") else: return self._stamp @property def canonical_public_address(self): return to_canonical_address(self.checksum_public_address) @canonical_public_address.setter def canonical_public_address(self, address_bytes): self._checksum_address = to_checksum_address(address_bytes) @property def checksum_public_address(self): if self._checksum_address is NO_BLOCKCHAIN_CONNECTION: self._set_checksum_address() return self._checksum_address @classmethod def from_config(cls, config, **overrides) -> 'Character': return config.produce(**overrides) @classmethod def from_public_keys(cls, powers_and_material: Dict, federated_only=True, *args, **kwargs) -> 'Character': """ Sometimes we discover a Character and, at the same moment, learn the public parts of more of their powers. Here, we take a Dict (powers_and_key_bytes) in the following format: {CryptoPowerUp class: public_material_bytes} Each item in the collection will have the CryptoPowerUp instantiated with the public_material_bytes, and the resulting CryptoPowerUp instance consumed by the Character. # TODO: Need to be federated only until we figure out the best way to get the checksum_public_address in here. """ crypto_power = CryptoPower() for power_up, public_key in powers_and_material.items(): try: umbral_key = UmbralPublicKey(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(pubkey=umbral_key)) return cls(is_me=False, federated_only=federated_only, crypto_power=crypto_power, *args, **kwargs) def store_metadata(self, filepath: str) -> str: """ Save this node to the disk. :param filepath: Output filepath to save node metadata. :return: Output filepath """ return self.node_storage.store_node_metadata(node=self, filepath=filepath) def encrypt_for( self, recipient: 'Character', plaintext: bytes, sign: bool = True, sign_plaintext=True, ) -> tuple: """ Encrypts plaintext for recipient actor. Optionally signs the message as well. :param recipient: The character whose public key will be used to encrypt cleartext. :param plaintext: The secret to be encrypted. :param sign: Whether or not to sign the message. :param sign_plaintext: When signing, the cleartext is signed if this is True, Otherwise, the resulting ciphertext is signed. :return: A tuple, (ciphertext, signature). If sign==False, then signature will be NOT_SIGNED. """ signer = self.stamp if sign else DO_NOT_SIGN message_kit, signature = encrypt_and_sign( recipient_pubkey_enc=recipient.public_keys(DecryptingPower), plaintext=plaintext, signer=signer, sign_plaintext=sign_plaintext) return message_kit, signature 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. :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: # 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 def decrypt(self, message_kit: UmbralMessageKit, label: Optional[bytes] = None) -> bytes: if label and DelegatingPower in self._default_crypto_powerups: delegating_power = self._crypto_power.power_ups(DelegatingPower) decrypting_power = delegating_power.get_decrypting_power_from_label( label) else: decrypting_power = self._crypto_power.power_ups(DecryptingPower) return decrypting_power.decrypt(message_kit) def sign(self, message): return self._crypto_power.power_ups(SigningPower).sign(message) def public_keys(self, power_up_class: ClassVar): """ Pass a power_up_class, get the public material for this Character which corresponds to that class - whatever type of object that may be. If the Character doesn't have the power corresponding to that class, raises the appropriate PowerUpError (ie, NoSigningPower or NoDecryptingPower). """ power_up = self._crypto_power.power_ups(power_up_class) return power_up.public_key() def _set_checksum_address(self): if self.federated_only: verifying_key = self.public_keys(SigningPower) uncompressed_bytes = verifying_key.to_bytes(is_compressed=False) without_prefix = uncompressed_bytes[1:] verifying_key_as_eth_key = EthKeyAPI.PublicKey(without_prefix) public_address = verifying_key_as_eth_key.to_checksum_address() else: try: public_address = to_checksum_address( self.canonical_public_address) except TypeError: raise TypeError( "You can't use a decentralized character without a _checksum_address." ) except NotImplementedError: raise TypeError( "You can't use a plain Character in federated mode - you need to implement ether_address." ) self._checksum_address = public_address
def __init__(self, domains: Set = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value( False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Operating Mode if hasattr(self, '_interface_class' ): # TODO: have argument about meaning of 'lawful' # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) if is_me: if not known_node_class: # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. from nucypher.characters.lawful import Ursula known_node_class = Ursula # If we're federated only, we assume that all other nodes in our domain are as well. known_node_class.set_federated_mode(federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError( f"Cannot init federated-only character with {registry or provider_uri}." ) self.federated_only = bool(federated_only) # type: bool # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) self._checksum_address = checksum_address # Fleet and Blockchain Connection (Everyone) if not domains: domains = {CharacterConfiguration.DEFAULT_DOMAIN} # # Self-Character # if is_me: self.treasure_maps = {} # type: dict # # Signing Power # self.signer = signer try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication( network=list(domains)[0]) #TODO: #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware( registry=self.registry) # # Learner # Learner.__init__(self, domains=domains, network_middleware=self.network_middleware, node_class=known_node_class, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stranger-Character." ) if registry is not None: raise TypeError( "Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER # # Decentralized # if not federated_only: self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_address == self.checksum_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_address)) # # Nicknames # if self._checksum_address is NO_BLOCKCHAIN_CONNECTION and not self.federated_only and not is_me: # Sometimes we don't care about the nickname. For example, if Alice is granting to Bob, she usually # doesn't know or care about his wallet. Maybe this needs to change? # Currently, if this is a stranger and there's no blockchain connection, we assign NO_NICKNAME: self.nickname = self.nickname_metadata = NO_NICKNAME else: try: self.nickname, self.nickname_metadata = nickname_from_seed( self.checksum_address) except SigningPower.not_found_error: # TODO: Handle NO_BLOCKCHAIN_CONNECTION more coherently - #1547 if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL
class Character(Learner): """ A base-class for any character in our cryptography protocol narrative. """ _display_name_template = "({})⇀{}↽ ({})" # Used in __repr__ and in cls.from_bytes _default_crypto_powerups = None _stamp = None _crashed = False from nucypher.network.protocols import SuspiciousActivity # Ship this exception with every Character. def __init__(self, domains: Set = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value( False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Operating Mode if hasattr(self, '_interface_class' ): # TODO: have argument about meaning of 'lawful' # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) if is_me: if not known_node_class: # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. from nucypher.characters.lawful import Ursula known_node_class = Ursula # If we're federated only, we assume that all other nodes in our domain are as well. known_node_class.set_federated_mode(federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError( f"Cannot init federated-only character with {registry or provider_uri}." ) self.federated_only = bool(federated_only) # type: bool # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) self._checksum_address = checksum_address # Fleet and Blockchain Connection (Everyone) if not domains: domains = {CharacterConfiguration.DEFAULT_DOMAIN} # # Self-Character # if is_me: self.treasure_maps = {} # type: dict # # Signing Power # self.signer = signer try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication( network=list(domains)[0]) #TODO: #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware( registry=self.registry) # # Learner # Learner.__init__(self, domains=domains, network_middleware=self.network_middleware, node_class=known_node_class, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stranger-Character." ) if registry is not None: raise TypeError( "Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER # # Decentralized # if not federated_only: self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_address == self.checksum_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_address)) # # Nicknames # if self._checksum_address is NO_BLOCKCHAIN_CONNECTION and not self.federated_only and not is_me: # Sometimes we don't care about the nickname. For example, if Alice is granting to Bob, she usually # doesn't know or care about his wallet. Maybe this needs to change? # Currently, if this is a stranger and there's no blockchain connection, we assign NO_NICKNAME: self.nickname = self.nickname_metadata = NO_NICKNAME else: try: self.nickname, self.nickname_metadata = nickname_from_seed( self.checksum_address) except SigningPower.not_found_error: # TODO: Handle NO_BLOCKCHAIN_CONNECTION more coherently - #1547 if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL def __eq__(self, other) -> bool: try: other_stamp = other.stamp except (AttributeError, NoSigningPower): return False return bytes(self.stamp) == bytes(other_stamp) def __hash__(self): return int.from_bytes(bytes(self.stamp), byteorder="big") def __repr__(self): r = self._display_name_template try: r = r.format(self.__class__.__name__, self.nickname, self.checksum_address) except ( NoSigningPower, TypeError ): # TODO: ....yeah? We can probably do better for a repr here. r = f"({self.__class__.__name__})⇀{self.nickname}↽" return r @property def name(self): return self.__class__.__name__ @property def stamp(self): if self._stamp is NO_SIGNING_POWER: raise NoSigningPower elif not self._stamp: raise AttributeError("SignatureStamp has not been set up yet.") else: return self._stamp @property def canonical_public_address(self): return to_canonical_address(self._checksum_address) @canonical_public_address.setter def canonical_public_address(self, address_bytes): self._checksum_address = to_checksum_address(address_bytes) @property def checksum_address(self): if self._checksum_address is NO_BLOCKCHAIN_CONNECTION: self._set_checksum_address() return self._checksum_address @classmethod def from_config(cls, config, **overrides) -> 'Character': return config.produce(**overrides) @classmethod def from_public_keys(cls, powers_and_material: Dict = None, verifying_key: Union[bytes, UmbralPublicKey] = None, encrypting_key: Union[bytes, UmbralPublicKey] = None, *args, **kwargs) -> 'Character': """ Sometimes we discover a Character and, at the same moment, learn the public parts of more of their powers. Here, we take a Dict (powers_and_material) in the format {CryptoPowerUp class: material}, where material can be bytes or UmbralPublicKey. Each item in the collection will have the CryptoPowerUp instantiated with the given material, and the resulting CryptoPowerUp instance consumed by the Character. Alternatively, you can pass directly a verifying public key (for SigningPower) and/or an encrypting public key (for DecryptionPower). """ crypto_power = CryptoPower() if powers_and_material is None: powers_and_material = dict() if verifying_key: powers_and_material[SigningPower] = verifying_key if encrypting_key: powers_and_material[DecryptingPower] = encrypting_key for power_up, public_key in powers_and_material.items(): try: umbral_key = UmbralPublicKey.from_bytes(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(public_key=umbral_key)) return cls(is_me=False, crypto_power=crypto_power, *args, **kwargs) def store_metadata(self, filepath: str) -> str: """ Save this node to the disk. :param filepath: Output filepath to save node metadata. :return: Output filepath """ return self.node_storage.store_node_metadata(node=self, filepath=filepath) def encrypt_for( self, recipient: 'Character', plaintext: bytes, sign: bool = True, sign_plaintext=True, ) -> tuple: """ Encrypts plaintext for recipient actor. Optionally signs the message as well. :param recipient: The character whose public key will be used to encrypt cleartext. :param plaintext: The secret to be encrypted. :param sign: Whether or not to sign the message. :param sign_plaintext: When signing, the cleartext is signed if this is True, Otherwise, the resulting ciphertext is signed. :return: A tuple, (ciphertext, signature). If sign==False, then signature will be NOT_SIGNED. """ signer = self.stamp if sign else DO_NOT_SIGN message_kit, signature = encrypt_and_sign( recipient_pubkey_enc=recipient.public_keys(DecryptingPower), plaintext=plaintext, signer=signer, sign_plaintext=sign_plaintext) return message_kit, signature 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 decrypt(self, message_kit: UmbralMessageKit, label: Optional[bytes] = None) -> bytes: if label and DelegatingPower in self._default_crypto_powerups: delegating_power = self._crypto_power.power_ups(DelegatingPower) decrypting_power = delegating_power.get_decrypting_power_from_label( label) else: decrypting_power = self._crypto_power.power_ups(DecryptingPower) return decrypting_power.decrypt(message_kit) def sign(self, message): return self._crypto_power.power_ups(SigningPower).sign(message) def public_keys(self, power_up_class: ClassVar): """ Pass a power_up_class, get the public material for this Character which corresponds to that class - whatever type of object that may be. If the Character doesn't have the power corresponding to that class, raises the appropriate PowerUpError (ie, NoSigningPower or NoDecryptingPower). """ power_up = self._crypto_power.power_ups(power_up_class) return power_up.public_key() def _set_checksum_address(self): if self.federated_only: verifying_key = self.public_keys(SigningPower) uncompressed_bytes = verifying_key.to_bytes(is_compressed=False) without_prefix = uncompressed_bytes[1:] verifying_key_as_eth_key = EthKeyAPI.PublicKey(without_prefix) public_address = verifying_key_as_eth_key.to_checksum_address() else: try: public_address = to_checksum_address( self.canonical_public_address) except TypeError: raise TypeError( "You can't use a decentralized character without a _checksum_address." ) except NotImplementedError: raise TypeError( "You can't use a plain Character in federated mode - you need to implement ether_address." ) self._checksum_address = public_address def make_rpc_controller(self, crash_on_error: bool = False): app_name = bytes(self.stamp).hex()[:6] controller = JSONRPCController(app_name=app_name, crash_on_error=crash_on_error, interface=self.interface) self.controller = controller return controller def make_cli_controller(self, crash_on_error: bool = False): app_name = bytes(self.stamp).hex()[:6] controller = CLIController(app_name=app_name, crash_on_error=crash_on_error, interface=self.interface) self.controller = controller return controller
def __init__(self, domain: str = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = None, network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, include_self_in_the_state: bool = False, *args, **kwargs) -> None: """ A participant in the cryptological drama (a screenplay, if you like) of NuCypher. Characters can represent users, nodes, wallets, offline devices, or other objects of varying levels of abstraction. The Named Characters use this class as a Base, and achieve their individuality from additional methods and PowerUps. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Prologue of the federation # # FIXME: excuse me... can I speak to the manager? if is_me: # If this is a federated-is_me-character, assume everyone else is too. self._set_known_node_class(known_node_class, federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError( f"Cannot init federated-only character with {registry or provider_uri}." ) self.federated_only: bool = federated_only ########################################## # # Keys & Powers # if keyring: keyring_root, keyring_checksum_address = keyring.keyring_root, keyring.checksum_address if checksum_address and (keyring_checksum_address != checksum_address): raise ValueError( f"Provided checksum address {checksum_address} " f"does not match character's keyring checksum address {keyring_checksum_address}" ) checksum_address = keyring_checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) # # Self # if is_me: # Signing Power self.signer = signer try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # Blockchainy if not self.federated_only: self.provider_uri = provider_uri # TODO: Implicit / lazy blockchain connection here? # if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri): # BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri) self.registry = registry or InMemoryContractRegistry.from_latest_publication( network=domain) # See #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware( registry=self.registry) # Learner Learner.__init__( self, domain=domain, network_middleware=self.network_middleware, node_class=known_node_class, include_self_in_the_state=include_self_in_the_state, *args, **kwargs) if self.federated_only: try: derived_federated_address = self.derive_federated_address() except NoSigningPower: derived_federated_address = NO_SIGNING_POWER.bool_value( False) if checksum_address and (checksum_address != derived_federated_address): raise ValueError( f"Provided checksum address {checksum_address} " f"does not match federated character's verifying key {derived_federated_address}" ) checksum_address = derived_federated_address self.checksum_address = checksum_address # # Stranger # else: if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stranger-Character." ) if registry is not None: raise TypeError( "Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER self.checksum_address = checksum_address self.__setup_nickname(is_me=is_me) # Character Control # TODO: have argument about meaning of 'lawful' and whether maybe only Lawful characters have an interface if hasattr(self, '_interface_class'): # Controller Interface self.interface = self._interface_class(character=self) self.controller = NO_CONTROL_PROTOCOL
def __init__(self, domains: Set = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value(False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, registry: BaseContractRegistry = None, *args, **kwargs ) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Operating Mode # if federated_only: if registry or provider_uri: raise ValueError(f"Cannot init federated-only character with {registry or provider_uri}.") self.federated_only = federated_only # type: bool # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.") crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) self._checksum_address = checksum_address # Fleet and Blockchain Connection (Everyone) if not domains: domains = (CharacterConfiguration.DEFAULT_DOMAIN,) # # Self-Character # if is_me: if not bool(federated_only) ^ bool(registry): raise ValueError(f"Pass either federated only or registry for is_me Characters. \ Got '{federated_only}' and '{registry}'.") self.treasure_maps = {} # type: dict self.network_middleware = network_middleware or RestMiddleware() # # Signing Power # try: signing_power = self._crypto_power.power_ups(SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication() else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # # Learner # Learner.__init__(self, domains=domains, network_middleware=self.network_middleware, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError("Network middleware cannot be attached to a Stranger-Character.") if registry is not None: raise TypeError("Registry cannot be attached to stranger-Characters.") self._stamp = StrangerStamp(self.public_keys(SigningPower)) self.network_middleware = STRANGER # # Decentralized # if not federated_only: self._checksum_address = checksum_address # TODO: Check that this matches TransactingPower # # Federated # elif federated_only: try: self._set_checksum_address() # type: str except NoSigningPower: self._checksum_address = NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key if not checksum_address == self.checksum_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity(error.format(checksum_address)) # # Nicknames # try: self.nickname, self.nickname_metadata = nickname_from_seed(self.checksum_address) except SigningPower.not_found_error: if self.federated_only: self.nickname = self.nickname_metadata = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL
def from_target_ursula( cls, target_ursula: Ursula, substitute_verifying_key: bool = False, sign_metadata: bool = False, ) -> 'Vladimir': """ Sometimes Vladimir seeks to attack or imitate a *specific* target Ursula. TODO: This is probably a more instructive method if it takes a bytes representation instead of the entire Ursula. """ try: from tests.utils.middleware import EvilMiddleWare except ImportError: raise DevelopmentInstallationRequired( importable_name='tests.utils.middleware.EvilMiddleWare') cls.network_middleware = EvilMiddleWare() crypto_power = CryptoPower( power_ups=target_ursula._default_crypto_powerups) blockchain = target_ursula.policy_agent.blockchain cls.attach_transacting_key(blockchain=blockchain) db_filepath = tempfile.mkdtemp(prefix='Vladimir') vladimir = cls( is_me=True, crypto_power=crypto_power, db_filepath=db_filepath, domain=TEMPORARY_DOMAIN, rest_host=target_ursula.rest_interface.host, rest_port=target_ursula.rest_interface.port, certificate=target_ursula.certificate, network_middleware=cls.network_middleware, checksum_address=cls.fraud_address, worker_address=cls.fraud_address, signer=Web3Signer(blockchain.client), provider_uri=blockchain.provider_uri, ) # Let's use the target's public info, and try to make some changes. metadata = target_ursula.metadata() metadata_bytes = bytes(metadata) # Since it is an object from a Rust extension, we cannot directly modify it, # so we have to replace stuff in the byte representation and then deserialize. # We are replacinig objects with constant size, # so it should work regardless of the binary format. # Our basic replacement. We want to impersonate the target Ursula. metadata_bytes = metadata_bytes.replace( metadata.payload.canonical_address, vladimir.canonical_address) # Use our own verifying key if substitute_verifying_key: metadata_bytes = metadata_bytes.replace( bytes(metadata.payload.verifying_key), bytes(vladimir.stamp.as_umbral_pubkey())) fake_metadata = NodeMetadata.from_bytes(metadata_bytes) # Re-generate metadata signature using our signing key if sign_metadata: fake_metadata = NodeMetadata(vladimir.stamp.as_umbral_signer(), fake_metadata.payload) # Put metadata back vladimir._metadata = fake_metadata return vladimir
class Character(Learner): """ A base-class for any character in our cryptography protocol narrative. """ _default_crypto_powerups = None _stamp = None _crashed = False from nucypher.network.protocols import SuspiciousActivity # Ship this exception with every Character. class InvalidSignature(Exception): """ Raised when a signature doesn't pass validation/verification. """ def __init__(self, is_me: bool = True, network_middleware: RestMiddleware = None, known_certificates_dir: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, federated_only: bool = False, checksum_address: bytes = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ super().__init__(*args, **kwargs) self.federated_only = federated_only # type: bool self.known_certificates_dir = known_certificates_dir # # Power-ups and Powers # if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or [] # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) # # Identity and Network # if is_me is True: self.treasure_maps = {} # type: dict self.network_middleware = network_middleware or RestMiddleware() try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = constants.NO_SIGNING_POWER else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Can't attach network middleware to a Character who isn't me. What are you even trying to do?" ) self._stamp = StrangerStamp(self.public_keys(SigningPower)) # Decentralized if not federated_only: if not checksum_address: raise ValueError( "No checksum_address provided while running in a non-federated mode." ) else: self._checksum_address = checksum_address # type: str # Federated elif federated_only: self._checksum_address = constants.NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key self._set_checksum_address() # type: str if not checksum_address == self.checksum_public_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_address)) def __eq__(self, other) -> bool: return bytes(self.stamp) == bytes(other.stamp) def __hash__(self): return int.from_bytes(bytes(self.stamp), byteorder="big") def __repr__(self): class_name = self.__class__.__name__ r = "{} {}" r = r.format(class_name, self.canonical_public_address) return r @property def name(self): return self.__class__.__name__ @property def rest_interface(self): return self._crypto_power.power_ups( TLSHostingPower).rest_server.rest_url() @property def stamp(self): if self._stamp is constants.NO_SIGNING_POWER: raise NoSigningPower elif not self._stamp: raise AttributeError("SignatureStamp has not been set up yet.") else: return self._stamp @property def canonical_public_address(self): return to_canonical_address(self.checksum_public_address) @canonical_public_address.setter def canonical_public_address(self, address_bytes): self._checksum_address = to_checksum_address(address_bytes) @property def ether_address(self): raise NotImplementedError @property def checksum_public_address(self): if self._checksum_address is constants.NO_BLOCKCHAIN_CONNECTION: self._set_checksum_address() return self._checksum_address @classmethod def from_config(cls, config, **overrides) -> 'Character': return config.produce(**overrides) @classmethod def from_public_keys(cls, powers_and_material: Dict, federated_only=True, *args, **kwargs) -> 'Character': # TODO: Need to be federated only until we figure out the best way to get the checksum_address in here. """ Sometimes we discover a Character and, at the same moment, learn the public parts of more of their powers. Here, we take a Dict (powers_and_key_bytes) in the following format: {CryptoPowerUp class: public_material_bytes} Each item in the collection will have the CryptoPowerUp instantiated with the public_material_bytes, and the resulting CryptoPowerUp instance consumed by the Character. """ crypto_power = CryptoPower() for power_up, public_key in powers_and_material.items(): try: umbral_key = UmbralPublicKey(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(pubkey=umbral_key)) return cls(is_me=False, federated_only=federated_only, crypto_power=crypto_power, *args, **kwargs) 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.warning("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 = os.path.join( self.known_certificates_dir, current_teacher.certificate_filename) 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) new_nodes = [] for node in node_list: if node.checksum_public_address in self.known_nodes or node.checksum_public_address == self.checksum_public_address: continue # TODO: 168 Check version and update if required. try: if eager: node.verify_node(self.network_middleware, accept_federated_only=self.federated_only) else: node.validate_metadata( accept_federated_only=self.federated_only) 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.warning(message) self.log.info("Previously unknown node: {}".format( node.checksum_public_address)) self.log.info("Previously unknown node: {}".format( node.checksum_public_address)) self.remember_node(node) 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.checksum_public_address, 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) return new_nodes def encrypt_for( self, recipient: 'Character', plaintext: bytes, sign: bool = True, sign_plaintext=True, ) -> tuple: """ Encrypts plaintext for recipient actor. Optionally signs the message as well. :param recipient: The character whose public key will be used to encrypt cleartext. :param plaintext: The secret to be encrypted. :param sign: Whether or not to sign the message. :param sign_plaintext: When signing, the cleartext is signed if this is True, Otherwise, the resulting ciphertext is signed. :return: A tuple, (ciphertext, signature). If sign==False, then signature will be NOT_SIGNED. """ signer = self.stamp if sign else constants.DO_NOT_SIGN message_kit, signature = encrypt_and_sign( recipient_pubkey_enc=recipient.public_keys(EncryptingPower), plaintext=plaintext, signer=signer, sign_plaintext=sign_plaintext) return message_kit, signature def verify_from( self, stranger: '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 = 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: # 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) 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.") # # Next we have decrypt() and sign() - these use the private # keys of their respective powers; any character who has these powers can use these functions. # # If they don't have the correct Power, the appropriate PowerUpError is raised. # return cleartext def decrypt(self, message_kit, verifying_key: UmbralPublicKey = None): return self._crypto_power.power_ups(EncryptingPower).decrypt( message_kit, verifying_key) def sign(self, message): return self._crypto_power.power_ups(SigningPower).sign(message) def public_keys(self, power_up_class: ClassVar) -> Union[Tuple, UmbralPublicKey]: """ Pass a power_up_class, get the public material for this Character which corresponds to that class - whatever type of object that may be. If the Character doesn't have the power corresponding to that class, raises the appropriate PowerUpError (ie, NoSigningPower or NoEncryptingPower). """ power_up = self._crypto_power.power_ups(power_up_class) return power_up.public_key() def _set_checksum_address(self): if self.federated_only: verifying_key = self.public_keys(SigningPower) uncompressed_bytes = verifying_key.to_bytes(is_compressed=False) without_prefix = uncompressed_bytes[1:] verifying_key_as_eth_key = EthKeyAPI.PublicKey(without_prefix) public_address = verifying_key_as_eth_key.to_checksum_address() else: try: public_address = to_checksum_address( self.canonical_public_address) except TypeError: raise TypeError( "You can't use a decentralized character without a _checksum_address." ) except NotImplementedError: raise TypeError( "You can't use a plain Character in federated mode - you need to implement ether_address." ) self._checksum_address = public_address
def __init__(self, attach_server=True, crypto_power: CryptoPower = None, crypto_power_ups=None, is_me=True, network_middleware=None, config: "NucypherConfig" = None, *args, **kwargs): """ :param attach_server: Whether to attach a Server when this Character is born. :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new CryptoPower will be made and will consume all of the CryptoPowerUps in this list. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ self.config = config if config is not None else NucypherConfig.get( ) # default self.known_nodes = {} self.log = getLogger("characters") if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) if not crypto_power_ups: crypto_power_ups = [] if crypto_power: self._crypto_power = crypto_power elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower(self._default_crypto_powerups) if is_me: self.network_middleware = network_middleware or NetworkyStuff() try: signing_power = self._crypto_power.power_ups(SigningPower) self._stamp = signing_power.get_signature_stamp() except NoSigningPower: self._stamp = constants.NO_SIGNING_POWER if attach_server: self.attach_server() else: if network_middleware is not None: raise TypeError( "Can't attach network middleware to a Character who isn't me. What are you even trying to do?" ) self._stamp = StrangerStamp(self.public_key(SigningPower))
class Character(object): """ A base-class for any character in our cryptography protocol narrative. """ _server = None _server_class = Server _default_crypto_powerups = None _stamp = None address = "This is a fake address." # TODO: #192 def __init__(self, attach_server=True, crypto_power: CryptoPower = None, crypto_power_ups=None, is_me=True, network_middleware=None, config: "NucypherConfig" = None, *args, **kwargs): """ :param attach_server: Whether to attach a Server when this Character is born. :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new CryptoPower will be made and will consume all of the CryptoPowerUps in this list. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ self.config = config if config is not None else NucypherConfig.get( ) # default self.known_nodes = {} self.log = getLogger("characters") if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) if not crypto_power_ups: crypto_power_ups = [] if crypto_power: self._crypto_power = crypto_power elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower(self._default_crypto_powerups) if is_me: self.network_middleware = network_middleware or NetworkyStuff() try: signing_power = self._crypto_power.power_ups(SigningPower) self._stamp = signing_power.get_signature_stamp() except NoSigningPower: self._stamp = constants.NO_SIGNING_POWER if attach_server: self.attach_server() else: if network_middleware is not None: raise TypeError( "Can't attach network middleware to a Character who isn't me. What are you even trying to do?" ) self._stamp = StrangerStamp(self.public_key(SigningPower)) def __eq__(self, other): return bytes(self.stamp) == bytes(other.stamp) def __hash__(self): return int.from_bytes(self.stamp, byteorder="big") class NotEnoughUrsulas(RuntimeError): """ All Characters depend on knowing about enough Ursulas to perform their role. This exception is raised when a piece of logic can't proceed without more Ursulas. """ class SuspiciousActivity(RuntimeError): """raised when an action appears to amount to malicious conduct.""" @classmethod def from_public_keys(cls, powers_and_keys: Dict, *args, **kwargs): """ Sometimes we discover a Character and, at the same moment, learn one or more of their public keys. Here, we take a Dict (powers_and_key_bytes) in the following format: {CryptoPowerUp class: public_key_bytes} Each item in the collection will have the CryptoPowerUp instantiated with the public_key_bytes, and the resulting CryptoPowerUp instance consumed by the Character. """ crypto_power = CryptoPower() for power_up, public_key in powers_and_keys.items(): try: umbral_key = UmbralPublicKey(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(pubkey=umbral_key)) return cls(is_me=False, crypto_power=crypto_power, *args, **kwargs) def attach_server(self, ksize=20, alpha=3, id=None, storage=None, *args, **kwargs) -> None: if self._server: raise RuntimeError( "Attaching the server twice is almost certainly a bad idea.") self._server = self._server_class(ksize, alpha, id, storage, *args, **kwargs) @property def stamp(self): if self._stamp is constants.NO_SIGNING_POWER: raise NoSigningPower elif not self._stamp: raise AttributeError("SignatureStamp has not been set up yet.") else: return self._stamp @property def server(self) -> Server: if self._server: return self._server else: raise RuntimeError("Server hasn't been attached.") @property def name(self): return self.__class__.__name__ def encrypt_for( self, recipient: "Character", plaintext: bytes, sign: bool = True, sign_plaintext=True, ) -> tuple: """ Encrypts plaintext for recipient actor. Optionally signs the message as well. :param recipient: The character whose public key will be used to encrypt cleartext. :param plaintext: The secret to be encrypted. :param sign: Whether or not to sign the message. :param sign_plaintext: When signing, the cleartext is signed if this is True, Otherwise, the resulting ciphertext is signed. :return: A tuple, (ciphertext, signature). If sign==False, then signature will be NOT_SIGNED. """ signer = self.stamp if sign else constants.DO_NOT_SIGN message_kit, signature = encrypt_and_sign( recipient_pubkey_enc=recipient.public_key(EncryptingPower), plaintext=plaintext, signer=signer, sign_plaintext=sign_plaintext) return message_kit, signature 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 """ Next we have decrypt(), sign(), and generate_self_signed_certificate() - these use the private keys of their respective powers; any character who has these powers can use these functions. If they don't have the correct Power, the appropriate PowerUpError is raised. """ def decrypt(self, message_kit, verifying_key: UmbralPublicKey = None): return self._crypto_power.power_ups(EncryptingPower).decrypt( message_kit, verifying_key) def sign(self, message): return self._crypto_power.power_ups(SigningPower).sign(message) def generate_self_signed_certificate(self): signing_power = self._crypto_power.power_ups(SigningPower) return signing_power.generate_self_signed_cert( self.stamp.fingerprint().decode()) """ And finally, some miscellaneous but generally-applicable abilities: """ def public_key(self, power_up_class: ClassVar): """ Pass a power_up_class, get the public key for this Character which corresponds to that class. If the Character doesn't have the power corresponding to that class, raises the appropriate PowerUpError (ie, NoSigningPower or NoEncryptingPower). """ power_up = self._crypto_power.power_ups(power_up_class) return power_up.public_key() def learn_about_nodes(self, address, port): """ Sends a request to node_url to find out about known nodes. """ response = self.network_middleware.get_nodes_via_rest(address, port) signature, nodes = signature_splitter(response.content, return_remainder=True) # TODO: Although not treasure map-related, this has a whiff of #172. ursula_interface_splitter = dht_value_splitter + BytestringSplitter( (bytes, 17)) split_nodes = ursula_interface_splitter.repeat(nodes) new_nodes = {} for node_meta in split_nodes: header, sig, pubkey, interface_info = node_meta if not pubkey in self.known_nodes: if sig.verify(keccak_digest(interface_info), pubkey): address, dht_port, rest_port = msgpack.loads( interface_info) new_nodes[pubkey] = \ Ursula.as_discovered_on_network( rest_port=rest_port, dht_port=dht_port, ip_address=address.decode("utf-8"), powers_and_keys=({SigningPower: pubkey}) ) else: message = "Suspicious Activity: Discovered node with bad signature: {}. Propagated by: {}:{}".format( node_meta, address, port) self.log.warn(message) return new_nodes def network_bootstrap(self, node_list): for node_addr, port in node_list: new_nodes = self.learn_about_nodes(node_addr, port) self.known_nodes.update(new_nodes)
class Character(Learner): """A base-class for any character in our cryptography protocol narrative.""" _display_name_template = "({})⇀{}↽ ({})" # Used in __repr__ and in cls.from_bytes _default_crypto_powerups = None _stamp = None def __init__(self, domain: str = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = None, network_middleware: RestMiddleware = None, keystore: Keystore = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, eth_provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, include_self_in_the_state: bool = False, *args, **kwargs) -> None: """ A participant in the cryptological drama (a screenplay, if you like) of NuCypher. Characters can represent users, nodes, wallets, offline devices, or other objects of varying levels of abstraction. The Named Characters use this class as a Base, and achieve their individuality from additional methods and PowerUps. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ # # Prologue of the federation # # FIXME: excuse me... can I speak to the manager? if is_me: # If this is a federated-is_me-character, assume everyone else is too. self._set_known_node_class(known_node_class, federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or eth_provider_uri: raise ValueError( f"Cannot init federated-only character with {registry or eth_provider_uri}." ) self.federated_only: bool = federated_only ########################################## # # Keys & Powers # if keystore: crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keystore.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keystore = keystore if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) # # Self # if is_me: # Signing Power self.signer = signer try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # Blockchainy if not self.federated_only: self.eth_provider_uri = eth_provider_uri self.registry = registry or InMemoryContractRegistry.from_latest_publication( network=domain) # See #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware( registry=self.registry, eth_provider_uri=eth_provider_uri) # Learner Learner.__init__( self, domain=domain, network_middleware=self.network_middleware, node_class=known_node_class, include_self_in_the_state=include_self_in_the_state, *args, **kwargs) if self.federated_only: try: derived_federated_address = self.derive_federated_address() except NoSigningPower: # TODO: Why allow such a character (without signing power) to be created at all? derived_federated_address = NO_SIGNING_POWER.bool_value( False) if checksum_address and (checksum_address != derived_federated_address): raise ValueError( f"Provided checksum address {checksum_address} " f"does not match federated character's verifying key {derived_federated_address}" ) checksum_address = derived_federated_address self.checksum_address = checksum_address # # Stranger # else: if network_middleware is not None: raise TypeError( "Network middleware cannot be attached to a Stranger-Character." ) if registry is not None: raise TypeError( "Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keystore_dir = STRANGER self.network_middleware = STRANGER self.checksum_address = checksum_address self.__setup_nickname(is_me=is_me) # Character Control # TODO: have argument about meaning of 'lawful' and whether maybe only Lawful characters have an interface if hasattr(self, '_interface_class'): # Controller Interface self.interface = self._interface_class(character=self) self.controller = NO_CONTROL_PROTOCOL def __eq__(self, other) -> bool: try: other_stamp = other.stamp except (AttributeError, NoSigningPower): return False return bytes(self.stamp) == bytes(other_stamp) def __hash__(self): return int.from_bytes(bytes(self.stamp), byteorder="big") def __repr__(self): r = self._display_name_template try: r = r.format(self.__class__.__name__, self.nickname, self.checksum_address) except ( NoSigningPower, TypeError ): # TODO: ....yeah? We can probably do better for a repr here. r = f"({self.__class__.__name__})⇀{self.nickname}↽" return r def __setup_nickname(self, is_me: bool): if not self.checksum_address and not self.federated_only and not is_me: # Sometimes we don't care about the nickname. For example, if Alice is granting to Bob, she usually # doesn't know or care about his wallet. Maybe this needs to change? # Currently, if this is a stranger and there's no blockchain connection, we assign NO_NICKNAME: self.nickname = NO_NICKNAME else: try: if not self.checksum_address: self.nickname = NO_NICKNAME else: # This can call _set_checksum_address. self.nickname = Nickname.from_seed(self.checksum_address) except SigningPower.not_found_error: if self.federated_only: self.nickname = NO_NICKNAME else: raise @property def name(self): return self.__class__.__name__ @property def stamp(self): if self._stamp is NO_SIGNING_POWER: raise NoSigningPower elif not self._stamp: raise AttributeError("SignatureStamp has not been set up yet.") else: return self._stamp @property def canonical_address(self): # TODO: This is wasteful. #1995 return to_canonical_address(self.checksum_address) @classmethod def from_config(cls, config, **overrides) -> 'Character': return config.produce(**overrides) @classmethod def from_public_keys(cls, powers_and_material: Dict = None, verifying_key: Optional[PublicKey] = None, encrypting_key: Optional[PublicKey] = None, *args, **kwargs) -> 'Character': """ Sometimes we discover a Character and, at the same moment, learn the public parts of more of their powers. Here, we take a Dict (powers_and_material) in the format {CryptoPowerUp class: material}, where material can be bytes or umbral.PublicKey. Each item in the collection will have the CryptoPowerUp instantiated with the given material, and the resulting CryptoPowerUp instance consumed by the Character. Alternatively, you can pass directly a verifying public key (for SigningPower) and/or an encrypting public key (for DecryptionPower). """ crypto_power = CryptoPower() if powers_and_material is None: powers_and_material = dict() if verifying_key: powers_and_material[SigningPower] = verifying_key if encrypting_key: powers_and_material[DecryptingPower] = encrypting_key for power_up, public_key in powers_and_material.items(): try: umbral_key = PublicKey.from_bytes(public_key) except TypeError: umbral_key = public_key crypto_power.consume_power_up(power_up(public_key=umbral_key)) return cls(is_me=False, crypto_power=crypto_power, *args, **kwargs) def _set_known_node_class(self, known_node_class, federated_only): if not known_node_class: # Once in a while, in tests or demos, we init a plain Character who doesn't already know about its node class. from nucypher.characters.lawful import Ursula known_node_class = Ursula self.known_node_class = known_node_class # If we're federated only, we assume that all other nodes in our domain are as well. known_node_class.set_federated_mode(federated_only) # TODO: Unused def store_metadata(self, filepath: Path) -> Path: """ Save this node to the disk. :param filepath: Output filepath to save node metadata. :return: Output filepath """ return self.node_storage.store_node_metadata(node=self, filepath=filepath) def encrypt_for( self, recipient: 'Character', plaintext: bytes, ) -> MessageKit: """ Encrypts plaintext for recipient actor. Optionally signs the message as well. :param recipient: The character whose public key will be used to encrypt cleartext. :param plaintext: The secret to be encrypted. :param sign_plaintext: the cleartext is signed if this is True, Otherwise, the resulting ciphertext is signed. :return: the message kit. """ # TODO: who even uses this method except for tests? message_kit = MessageKit( policy_encrypting_key=recipient.public_keys(DecryptingPower), plaintext=plaintext) return message_kit def public_keys(self, power_up_class: ClassVar): """ Pass a power_up_class, get the public material for this Character which corresponds to that class - whatever type of object that may be. If the Character doesn't have the power corresponding to that class, raises the appropriate PowerUpError (ie, NoSigningPower or NoDecryptingPower). """ power_up = self._crypto_power.power_ups(power_up_class) return power_up.public_key() def derive_federated_address(self): if self.federated_only: verifying_key = self.public_keys(SigningPower) verifying_key_as_eth_key = EthKeyAPI.PublicKey.from_compressed_bytes( bytes(verifying_key)) federated_address = verifying_key_as_eth_key.to_checksum_address() else: raise RuntimeError( 'Federated address can only be derived for federated characters.' ) return federated_address def make_rpc_controller(self, crash_on_error: bool = False): app_name = bytes(self.stamp).hex()[:6] controller = JSONRPCController(app_name=app_name, crash_on_error=crash_on_error, interface=self.interface) self.controller = controller return controller def make_cli_controller(self, crash_on_error: bool = False): app_name = bytes(self.stamp).hex()[:6] controller = CharacterCLIController(app_name=app_name, crash_on_error=crash_on_error, interface=self.interface) self.controller = controller return controller def disenchant(self): self.log.debug(f"Disenchanting {self}") Learner.stop_learning_loop(self)
def __init__(self, is_me: bool = True, network_middleware: RestMiddleware = None, known_certificates_dir: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, federated_only: bool = False, checksum_address: bytes = None, *args, **kwargs) -> None: """ Base class for Nucypher protocol actors. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ super().__init__(*args, **kwargs) self.federated_only = federated_only # type: bool self.known_certificates_dir = known_certificates_dir # # Power-ups and Powers # if crypto_power and crypto_power_ups: raise ValueError( "Pass crypto_power or crypto_power_ups (or neither), but not both." ) crypto_power_ups = crypto_power_ups or [] # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower( power_ups=self._default_crypto_powerups) # # Identity and Network # if is_me is True: self.treasure_maps = {} # type: dict self.network_middleware = network_middleware or RestMiddleware() try: signing_power = self._crypto_power.power_ups( SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp( ) # type: SignatureStamp except NoSigningPower: self._stamp = constants.NO_SIGNING_POWER else: # Feel like a stranger if network_middleware is not None: raise TypeError( "Can't attach network middleware to a Character who isn't me. What are you even trying to do?" ) self._stamp = StrangerStamp(self.public_keys(SigningPower)) # Decentralized if not federated_only: if not checksum_address: raise ValueError( "No checksum_address provided while running in a non-federated mode." ) else: self._checksum_address = checksum_address # type: str # Federated elif federated_only: self._checksum_address = constants.NO_BLOCKCHAIN_CONNECTION if checksum_address: # We'll take a checksum address, as long as it matches their singing key self._set_checksum_address() # type: str if not checksum_address == self.checksum_public_address: error = "Federated-only Characters derive their address from their Signing key; got {} instead." raise self.SuspiciousActivity( error.format(checksum_address))
def __init__(self, domain: str = None, known_node_class: object = None, is_me: bool = True, federated_only: bool = False, checksum_address: str = NO_BLOCKCHAIN_CONNECTION.bool_value(False), network_middleware: RestMiddleware = None, keyring: NucypherKeyring = None, keyring_root: str = None, crypto_power: CryptoPower = None, crypto_power_ups: List[CryptoPowerUp] = None, provider_uri: str = None, signer: Signer = None, registry: BaseContractRegistry = None, *args, **kwargs ) -> None: """ A participant in the cryptological drama (a screenplay, if you like) of NuCypher. Characters can represent users, nodes, wallets, offline devices, or other objects of varying levels of abstraction. The Named Characters use this class as a Base, and achieve their individuality from additional methods and PowerUps. PowerUps ======== :param crypto_power: A CryptoPower object; if provided, this will be the character's CryptoPower. :param crypto_power_ups: If crypto_power is not provided, a new one will be made to consume all CryptoPowerUps. If neither crypto_power nor crypto_power_ups are provided, we give this Character all CryptoPowerUps listed in their _default_crypto_powerups attribute. :param is_me: Set this to True when you want this Character to represent the owner of the configuration under which the program is being run. A Character who is_me can do things that other Characters can't, like run servers, sign messages, and decrypt messages which are encrypted for them. Typically this will be True for exactly one Character, but there are scenarios in which its imaginable to be represented by zero Characters or by more than one Character. """ if provider_uri: if not BlockchainInterfaceFactory.is_interface_initialized(provider_uri=provider_uri): BlockchainInterfaceFactory.initialize_interface(provider_uri=provider_uri) # # Operating Mode # if hasattr(self, '_interface_class'): # TODO: have argument about meaning of 'lawful' # and whether maybe only Lawful characters have an interface self.interface = self._interface_class(character=self) if is_me: self._set_known_node_class(known_node_class, federated_only) else: # What an awful hack. The last convulsions of #466. # TODO: Anything else. with suppress(AttributeError): federated_only = known_node_class._federated_only_instances if federated_only: if registry or provider_uri: raise ValueError(f"Cannot init federated-only character with {registry or provider_uri}.") self.federated_only: bool = federated_only # # Powers # # Derive powers from keyring if keyring_root and keyring: if keyring_root != keyring.keyring_root: raise ValueError("Inconsistent keyring root directory path") if keyring: keyring_root, checksum_address = keyring.keyring_root, keyring.checksum_address crypto_power_ups = list() for power_up in self._default_crypto_powerups: power = keyring.derive_crypto_power(power_class=power_up) crypto_power_ups.append(power) self.keyring_root = keyring_root self.keyring = keyring if crypto_power and crypto_power_ups: raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.") crypto_power_ups = crypto_power_ups or list() # type: list if crypto_power: self._crypto_power = crypto_power # type: CryptoPower elif crypto_power_ups: self._crypto_power = CryptoPower(power_ups=crypto_power_ups) else: self._crypto_power = CryptoPower(power_ups=self._default_crypto_powerups) # # Self-Character # if is_me: # # Signing Power # self.signer = signer try: signing_power = self._crypto_power.power_ups(SigningPower) # type: SigningPower self._stamp = signing_power.get_signature_stamp() # type: SignatureStamp except NoSigningPower: self._stamp = NO_SIGNING_POWER # # Blockchain # self.provider_uri = provider_uri if not self.federated_only: self.registry = registry or InMemoryContractRegistry.from_latest_publication(network=domain) # See #1580 else: self.registry = NO_BLOCKCHAIN_CONNECTION.bool_value(False) # REST self.network_middleware = network_middleware or RestMiddleware(registry=self.registry) # # Learner # Learner.__init__(self, domain=domain, network_middleware=self.network_middleware, node_class=known_node_class, *args, **kwargs) # # Stranger-Character # else: # Feel like a stranger if network_middleware is not None: raise TypeError("Network middleware cannot be attached to a Stranger-Character.") if registry is not None: raise TypeError("Registry cannot be attached to stranger-Characters.") verifying_key = self.public_keys(SigningPower) self._stamp = StrangerStamp(verifying_key) self.keyring_root = STRANGER self.network_middleware = STRANGER # TODO: Figure out when to do this. try: _transacting_power = self._crypto_power.power_ups(TransactingPower) except NoTransactingPower: self._checksum_address = checksum_address else: self._set_checksum_address(checksum_address) # # Nicknames # if self._checksum_address is NO_BLOCKCHAIN_CONNECTION and not self.federated_only and not is_me: # Sometimes we don't care about the nickname. For example, if Alice is granting to Bob, she usually # doesn't know or care about his wallet. Maybe this needs to change? # Currently, if this is a stranger and there's no blockchain connection, we assign NO_NICKNAME: self.nickname = NO_NICKNAME else: try: # TODO: It's possible that this is NO_BLOCKCHAIN_CONNECTION. if self.checksum_address is NO_BLOCKCHAIN_CONNECTION: self.nickname = NO_NICKNAME else: # This can call _set_checksum_address. self.nickname = Nickname.from_seed(self.checksum_address) except SigningPower.not_found_error: # TODO: Handle NO_BLOCKCHAIN_CONNECTION more coherently - #1547 if self.federated_only: self.nickname = NO_NICKNAME else: raise # # Fleet state # if is_me is True: self.known_nodes.record_fleet_state() # # Character Control # self.controller = NO_CONTROL_PROTOCOL