示例#1
0
    def __init__(self,
                 attach_server=True,
                 crypto_power: CryptoPower = None,
                 crypto_power_ups=None,
                 is_me=True,
                 config: "KMSConfig" = None) -> None:
        """
        :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 KMSConfig.get_config()
        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,
                                             generate_keys_if_needed=is_me)
        else:
            self._crypto_power = CryptoPower(self._default_crypto_powerups,
                                             generate_keys_if_needed=is_me)
        if is_me:
            try:
                self._stamp = SignatureStamp(
                    self._crypto_power.power_ups(SigningPower).keypair)
            except NoSigningPower:
                self._stamp = constants.NO_SIGNING_POWER

            if attach_server:
                self.attach_server()
        else:
            self._stamp = StrangerStamp(
                self._crypto_power.power_ups(SigningPower).keypair)
示例#2
0
    def __init__(self,
                 attach_server=True,
                 crypto_power: CryptoPower = None,
                 crypto_power_ups=[]):
        """
        :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.
        """
        self._actor_mapping = {}
        if crypto_power and crypto_power_ups:
            raise ValueError(
                "Pass crypto_power or crypto_power_ups (or neither), but not both."
            )

        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)

        class Seal(object):
            """
            Can be called to sign something or used to express the signing public key as bytes.
            """
            __call__ = self._crypto_power.sign

            def _as_tuple(seal):
                return self._crypto_power.pubkey_sig_tuple()

            def __iter__(seal):
                yield from seal._as_tuple()

            def __bytes__(seal):
                return self._crypto_power.pubkey_sig_bytes()

            def __eq__(seal, other):
                return other == seal._as_tuple() or other == bytes(seal)

        self._seal = Seal()

        if attach_server:
            self.attach_server()
示例#3
0
    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)
示例#4
0
    def __init__(self, attach_server=True, crypto_power: CryptoPower = None,
                 crypto_power_ups=[], is_me=True) -> None:
        """
        :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.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 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._actor_mapping = {}

            self._seal = Seal(self)

            if attach_server:
                self.attach_server()
        else:
            self._seal = StrangerSeal(self)
示例#5
0
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)
示例#6
0
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

    def __init__(self, attach_server=True, crypto_power: CryptoPower = None,
                 crypto_power_ups=[], is_me=True) -> None:
        """
        :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.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 is_me:
            self._stamp = SignatureStamp(self)

            if attach_server:
                self.attach_server()
        else:
            self._stamp = StrangerStamp(self)

        if crypto_power:
            self._crypto_power = crypto_power
        elif crypto_power_ups:
            self._crypto_power = CryptoPower(power_ups=crypto_power_ups,
                                             generate_keys_if_needed=is_me)
        else:
            self._crypto_power = CryptoPower(self._default_crypto_powerups,
                                             generate_keys_if_needed=is_me)

    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 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.
        """
        recipient_pubkey_enc = recipient.public_key(EncryptingPower)
        if sign:
            if sign_plaintext:
                # Sign first, encrypt second.
                signature = self.stamp(plaintext)
                ciphertext, capsule = pre.encrypt(recipient_pubkey_enc, signature + plaintext)
            else:
                # Encrypt first, sign second.
                ciphertext, capsule = pre.encrypt(recipient_pubkey_enc, plaintext)
                signature = self.stamp(ciphertext)
            alice_pubkey = self.public_key(SigningPower)
        else:
            # Don't sign.
            signature = NOT_SIGNED
            ciphertext, capsule = pre.encrypt(recipient_pubkey_enc, plaintext)
            alice_pubkey = None

        message_kit = MessageKit(ciphertext=ciphertext, capsule=capsule, alice_pubkey=alice_pubkey)

        return message_kit, signature

    def verify_from(self,
                    actor_whom_sender_claims_to_be: "Character",
                    message_kit: Union[MessageKit, bytes],
                    signature: Signature=None,
                    decrypt=False,
                    signature_is_on_cleartext=False) -> 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 messages: The messages to be verified.
        :param decrypt: Whether or not to decrypt the messages.
        :param signature_is_on_cleartext: True if we expect the signature to be
            on the cleartext. Otherwise, we presume that the ciphertext is what
            is signed.
        :return: Whether or not the signature is valid, the decrypted plaintext
            or NO_DECRYPTION_PERFORMED
        """
        # TODO: In this flow we now essentially have two copies of the public key.  See #174.
        # One from the actor (first arg) and one from the MessageKit.
        # Which do we use in which cases?

        # if not signature and not signature_is_on_cleartext:
        # TODO: Since a signature can now be in a MessageKit, this might not be accurate anymore.  See #174.
        # raise ValueError("You need to either provide the Signature or \
        #                   decrypt and find it on the cleartext.")

        cleartext = NO_DECRYPTION_PERFORMED

        if signature_is_on_cleartext:
            if decrypt:
                cleartext_with_sig = self.decrypt(message_kit)
                signature, cleartext = BytestringSplitter(Signature)(cleartext_with_sig,
                                                                     return_remainder=True)
                message_kit.signature = signature  # TODO: Obviously this is the wrong way to do this.  Let's make signature a property.
            else:
                raise ValueError(
                    "Can't look for a signature on the cleartext if we're not \
                     decrypting.")
            message = cleartext
            alice_pubkey = message_kit.alice_pubkey
        else:
            # The signature is on the ciphertext.  We might not even need to decrypt it.
            if decrypt:
                message = message_kit.ciphertext
                cleartext = self.decrypt(message_kit)
            else:
                message = bytes(message_kit)
            alice_pubkey = actor_whom_sender_claims_to_be.public_key(SigningPower)

        if signature:
            is_valid = signature.verify(message, alice_pubkey)
        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):
        return self._crypto_power.power_ups(EncryptingPower).decrypt(message_kit)

    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):
        """
        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.
        """
        # TODO: Find out about other known nodes, not just this one.  #175
        node = Ursula.from_rest_url(address, port)
        self.known_nodes[node.interface_dht_key()] = node
示例#7
0
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: "KMSConfig" = None) -> None:
        """
        :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 KMSConfig.get_config()
        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:
                self._stamp = SignatureStamp(
                    self._crypto_power.power_ups(SigningPower).keypair)
            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._crypto_power.power_ups(SigningPower).keypair)

    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,
    ) -> 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 messages: The messages to be verified.
        :param decrypt: Whether or not to decrypt the messages.
        :param signature_is_on_cleartext: True if we expect the signature to be
            on the cleartext. Otherwise, we presume that the ciphertext is what
            is signed.
        :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)
            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):
        return self._crypto_power.power_ups(EncryptingPower).decrypt(
            message_kit)

    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)
示例#8
0
class Character(object):
    """
    A base-class for any character in our cryptography protocol narrative.
    """
    _server = None
    _server_class = Server
    _default_crypto_powerups = None
    _seal = None

    class NotFound(KeyError):
        """raised when we try to interact with an actor of whom we haven't learned yet."""

    def __init__(self,
                 attach_server=True,
                 crypto_power: CryptoPower = None,
                 crypto_power_ups=[]):
        """
        :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.
        """
        self._actor_mapping = {}
        if crypto_power and crypto_power_ups:
            raise ValueError(
                "Pass crypto_power or crypto_power_ups (or neither), but not both."
            )

        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)

        class Seal(object):
            """
            Can be called to sign something or used to express the signing public key as bytes.
            """
            __call__ = self._crypto_power.sign

            def _as_tuple(seal):
                return self._crypto_power.pubkey_sig_tuple()

            def __iter__(seal):
                yield from seal._as_tuple()

            def __bytes__(seal):
                return self._crypto_power.pubkey_sig_bytes()

            def __eq__(seal, other):
                return other == seal._as_tuple() or other == bytes(seal)

        self._seal = Seal()

        if attach_server:
            self.attach_server()

    def attach_server(self,
                      ksize=20,
                      alpha=3,
                      id=None,
                      storage=None,
                      *args,
                      **kwargs) -> None:
        self._server = self._server_class(ksize, alpha, id, storage, *args,
                                          **kwargs)

    @property
    def seal(self):
        if not self._seal:
            raise AttributeError("Seal has not been set up yet.")
        else:
            return self._seal

    @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 learn_about_actor(self, actor):
        self._actor_mapping[actor.id()] = actor

    def encrypt_for(self,
                    recipient: str,
                    cleartext: bytes,
                    sign: bool = True,
                    sign_cleartext=True) -> tuple:
        """
        Looks up recipient actor, finds that actor's pubkey_enc on our keyring, and encrypts for them.
        Optionally signs the message as well.

        :param recipient: The character whose public key will be used to encrypt cleartext.
        :param cleartext: The secret    to be encrypted.
        :param sign: Whether or not to sign the message.
        :param sign_cleartext: 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.
        """
        actor = self._lookup_actor(recipient)

        ciphertext = self._crypto_power.encrypt_for(
            actor.public_key(EncryptingPower), cleartext)
        if sign:
            if sign_cleartext:
                signature = self.seal(cleartext)
            else:
                signature = self.seal(ciphertext)
        else:
            signature = NOT_SIGNED

        return ciphertext, signature

    def verify_from(self,
                    actor_whom_sender_claims_to_be: "Character",
                    signature: bytes,
                    message: bytes,
                    decrypt=False,
                    signature_is_on_cleartext=False) -> 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 messages: The messages to be verified.
        :param decrypt: Whether or not to decrypt the messages.
        :param signature_is_on_cleartext: True if we expect the signature to be on the cleartext.  Otherwise, we presume that the ciphertext is what is signed.
        :return: (Whether or not the signature is valid, the decrypted plaintext or NO_DECRYPTION_PERFORMED)
        """
        cleartext = NO_DECRYPTION_PERFORMED
        if signature_is_on_cleartext:
            if decrypt:
                cleartext = self._crypto_power.decrypt(message)
                msg_digest = API.keccak_digest(cleartext)
            else:
                raise ValueError(
                    "Can't look for a signature on the cleartext if we're not decrypting."
                )
        else:
            msg_digest = API.keccak_digest(message)

        actor = self._lookup_actor(actor_whom_sender_claims_to_be)
        signature_pub_key = actor.seal

        sig = API.ecdsa_load_sig(signature)
        return API.ecdsa_verify(*sig, msg_digest, signature_pub_key), cleartext

    def _lookup_actor(self, actor: "Character"):
        try:
            return self._actor_mapping[actor.id()]
        except KeyError:
            raise self.NotFound(
                "We haven't learned of an actor with ID {}".format(actor.id()))

    def id(self):
        return "whatever actor id ends up being - {}".format(id(self))

    def public_key(self, key_class):
        try:
            return self._crypto_power.public_keys[key_class]
        except KeyError:
            raise  # TODO: Does it make sense to have a specialized exception here?  Probably.
示例#9
0
class Character(object):
    """
    A base-class for any character in our cryptography protocol narrative.
    """
    _server = None
    _server_class = Server
    _default_crypto_powerups = None
    _seal = None

    def __init__(self, attach_server=True, crypto_power: CryptoPower = None,
                 crypto_power_ups=[], is_me=True) -> None:
        """
        :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.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 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._actor_mapping = {}

            self._seal = Seal(self)

            if attach_server:
                self.attach_server()
        else:
            self._seal = StrangerSeal(self)

    def __eq__(self, other):
        return bytes(self.seal) == bytes(other.seal)

    def __hash__(self):
        return int.from_bytes(self.seal, byteorder="big")

    class NotFound(KeyError):
        """raised when we try to interact with an actor of whom we haven't learned yet."""

    class SuspiciousActivity(RuntimeError):
        """raised when an action appears to amount to malicious conduct."""

    @classmethod
    def from_pubkey_sig_bytes(cls, pubkey_sig_bytes):
        return cls(is_me=False, crypto_power_ups=[SigningPower(keypair=Keypair.deserialize_key(pubkey_sig_bytes))])

    def attach_server(self, ksize=20, alpha=3, id=None, storage=None,
                      *args, **kwargs) -> None:
        self._server = self._server_class(ksize, alpha, id, storage, *args, **kwargs)

    @property
    def seal(self):
        if not self._seal:
            raise AttributeError("Seal has not been set up yet.")
        else:
            return self._seal

    @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 hash(self, message):
        return keccak_digest(message)

    def learn_about_actor(self, actor):
        self._actor_mapping[actor.id()] = actor

    def encrypt_for(self, recipient: "Character", cleartext: bytes, sign: bool = True,
                    sign_cleartext=True) -> tuple:
        """
        Looks up recipient actor, finds that actor's pubkey_enc on our keyring, and encrypts for them.
        Optionally signs the message as well.

        :param recipient: The character whose public key will be used to encrypt cleartext.
        :param cleartext: The secret    to be encrypted.
        :param sign: Whether or not to sign the message.
        :param sign_cleartext: 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.
        """
        actor = self._lookup_actor(recipient)

        if sign:
            if sign_cleartext:
                signature = self.seal(cleartext)
                ciphertext = self._crypto_power.encrypt_for(actor.public_key(EncryptingPower),
                                                            signature + cleartext)
            else:
                ciphertext = self._crypto_power.encrypt_for(actor.public_key(EncryptingPower),
                                                            cleartext)
                signature = self.seal(ciphertext)
        else:
            signature = NOT_SIGNED
            ciphertext = self._crypto_power.encrypt_for(actor.public_key(EncryptingPower),
                                                        cleartext)

        return ciphertext, signature

    def verify_from(self, actor_whom_sender_claims_to_be: "Character", message: bytes, signature: Signature = None,
                    decrypt=False,
                    signature_is_on_cleartext=False) -> 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 messages: The messages to be verified.
        :param decrypt: Whether or not to decrypt the messages.
        :param signature_is_on_cleartext: True if we expect the signature to be on the cleartext.  Otherwise, we presume that the ciphertext is what is signed.
        :return: (Whether or not the signature is valid, the decrypted plaintext or NO_DECRYPTION_PERFORMED)
        """
        if not signature and not signature_is_on_cleartext:
            raise ValueError("You need to either provide the Signature or decrypt and find it on the cleartext.")

        cleartext = NO_DECRYPTION_PERFORMED

        if signature_is_on_cleartext:
            if decrypt:
                cleartext = self._crypto_power.decrypt(message)
                signature, message = BytestringSplitter(Signature)(cleartext, return_remainder=True)
            else:
                raise ValueError(
                    "Can't look for a signature on the cleartext if we're not decrypting.")

        actor = self._lookup_actor(actor_whom_sender_claims_to_be)

        return signature.verify(message, actor.seal), cleartext

    def _lookup_actor(self, actor: "Character"):
        try:
            return self._actor_mapping[actor.id()]
        except KeyError:
            raise self.NotFound("We haven't learned of an actor with ID {}".format(actor.id()))

    def id(self):
        return hexlify(bytes(self.seal))

    def public_key(self, key_class):
        try:
            return self._crypto_power.public_keys[key_class]
        except KeyError:
            raise  # TODO: Does it make sense to have a specialized exception here?  Probably.