2017-11-03 21:50:04 +00:00
|
|
|
import asyncio
|
2017-11-21 03:49:43 +00:00
|
|
|
import binascii
|
2017-11-28 04:26:06 +00:00
|
|
|
from binascii import hexlify
|
2017-11-21 17:38:04 +00:00
|
|
|
from logging import getLogger
|
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
import msgpack
|
2018-02-06 07:08:55 +00:00
|
|
|
import requests
|
2017-11-18 01:36:08 +00:00
|
|
|
from apistar import http
|
2017-11-19 19:58:33 +00:00
|
|
|
from apistar.core import Route
|
|
|
|
from apistar.frameworks.wsgi import WSGIApp as App
|
2017-12-05 20:48:40 +00:00
|
|
|
from apistar.http import Response
|
2017-09-28 00:07:36 +00:00
|
|
|
from kademlia.network import Server
|
2017-11-03 04:54:58 +00:00
|
|
|
from kademlia.utils import digest
|
2017-12-10 01:19:30 +00:00
|
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
|
2018-02-08 00:22:28 +00:00
|
|
|
from umbral.fragments import KFrag
|
2018-02-07 10:31:27 +00:00
|
|
|
from umbral.keys import UmbralPublicKey
|
2018-02-11 02:16:12 +00:00
|
|
|
import umbral
|
2018-02-07 10:31:27 +00:00
|
|
|
|
2017-10-19 20:13:41 +00:00
|
|
|
from nkms.crypto import api as API
|
2017-11-10 10:04:01 +00:00
|
|
|
from nkms.crypto.api import secure_random, keccak_digest
|
2018-02-07 10:31:27 +00:00
|
|
|
from nkms.crypto.constants import NOT_SIGNED, NO_DECRYPTION_PERFORMED, KFRAG_LENGTH
|
2017-10-17 21:01:25 +00:00
|
|
|
from nkms.crypto.powers import CryptoPower, SigningPower, EncryptingPower
|
2017-11-22 06:02:26 +00:00
|
|
|
from nkms.crypto.signature import Signature
|
|
|
|
from nkms.crypto.utils import BytestringSplitter
|
2017-11-03 21:50:04 +00:00
|
|
|
from nkms.network import blockchain_client
|
2017-11-12 01:46:27 +00:00
|
|
|
from nkms.network.protocols import dht_value_splitter
|
2017-09-28 00:07:36 +00:00
|
|
|
from nkms.network.server import NuCypherDHTServer, NuCypherSeedOnlyDHTServer
|
2017-12-10 01:21:08 +00:00
|
|
|
from nkms.policy.constants import NOT_FROM_ALICE, NON_PAYMENT
|
2017-11-18 06:19:53 +00:00
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
class Character(object):
|
|
|
|
"""
|
|
|
|
A base-class for any character in our cryptography protocol narrative.
|
|
|
|
"""
|
|
|
|
_server = None
|
|
|
|
_server_class = Server
|
2017-10-07 02:29:07 +00:00
|
|
|
_default_crypto_powerups = None
|
2017-11-03 04:54:58 +00:00
|
|
|
_seal = None
|
2017-09-28 00:07:36 +00:00
|
|
|
|
2017-10-07 02:29:07 +00:00
|
|
|
def __init__(self, attach_server=True, crypto_power: CryptoPower = None,
|
2017-11-22 06:08:02 +00:00
|
|
|
crypto_power_ups=[], is_me=True) -> None:
|
2017-10-07 02:29:07 +00:00
|
|
|
"""
|
2018-02-10 04:15:50 +00:00
|
|
|
: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.
|
2017-10-07 02:29:07 +00:00
|
|
|
"""
|
2017-11-21 17:38:04 +00:00
|
|
|
self.log = getLogger("characters")
|
2017-11-07 22:25:03 +00:00
|
|
|
if crypto_power and crypto_power_ups:
|
|
|
|
raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.")
|
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
if is_me:
|
|
|
|
self._actor_mapping = {}
|
2017-10-07 02:29:07 +00:00
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
self._seal = Seal(self)
|
2017-11-03 04:54:58 +00:00
|
|
|
|
2017-11-07 22:25:03 +00:00
|
|
|
if attach_server:
|
|
|
|
self.attach_server()
|
|
|
|
else:
|
|
|
|
self._seal = StrangerSeal(self)
|
2017-09-28 00:07:36 +00:00
|
|
|
|
2018-02-05 19:05:28 +00:00
|
|
|
if crypto_power:
|
|
|
|
self._crypto_power = crypto_power
|
|
|
|
elif crypto_power_ups:
|
2018-02-10 04:15:50 +00:00
|
|
|
self._crypto_power = CryptoPower(power_ups=crypto_power_ups,
|
|
|
|
generate_keys_if_needed=is_me)
|
2018-02-05 19:05:28 +00:00
|
|
|
else:
|
2018-02-10 04:15:50 +00:00
|
|
|
self._crypto_power = CryptoPower(self._default_crypto_powerups,
|
|
|
|
generate_keys_if_needed=is_me)
|
2018-02-05 19:05:28 +00:00
|
|
|
|
2017-12-05 20:48:40 +00:00
|
|
|
def __eq__(self, other):
|
|
|
|
return bytes(self.seal) == bytes(other.seal)
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return int.from_bytes(self.seal, byteorder="big")
|
|
|
|
|
2017-11-22 04:20:15 +00:00
|
|
|
class NotFound(KeyError):
|
2018-02-10 04:15:50 +00:00
|
|
|
"""raised when we try to interact with an actor of whom we haven't \
|
|
|
|
learned yet."""
|
2017-11-22 04:20:15 +00:00
|
|
|
|
2017-12-16 05:46:14 +00:00
|
|
|
class SuspiciousActivity(RuntimeError):
|
|
|
|
"""raised when an action appears to amount to malicious conduct."""
|
|
|
|
|
2017-11-19 03:17:52 +00:00
|
|
|
@classmethod
|
2018-02-07 01:15:53 +00:00
|
|
|
def from_public_keys(cls, *powers_and_key_bytes):
|
|
|
|
"""
|
2018-02-10 04:15:50 +00:00
|
|
|
Sometimes we discover a Character and, at the same moment, learn one or
|
|
|
|
more of their public keys. Here, we take a collection of tuples
|
|
|
|
(powers_and_key_bytes) in the following format:
|
2018-02-07 01:15:53 +00:00
|
|
|
(CryptoPowerUp class, public_key_bytes)
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
Each item in the collection will have the CryptoPowerUp instantiated
|
|
|
|
with the public_key_bytes, and the resulting CryptoPowerUp instance
|
|
|
|
consumed by the Character.
|
2018-02-07 01:15:53 +00:00
|
|
|
"""
|
2018-02-05 19:25:17 +00:00
|
|
|
crypto_power = CryptoPower()
|
2018-02-07 01:15:53 +00:00
|
|
|
|
|
|
|
for power_up, public_key_bytes in powers_and_key_bytes:
|
|
|
|
crypto_power.consume_power_up(power_up(pubkey_bytes=public_key_bytes))
|
|
|
|
|
2018-02-05 19:25:17 +00:00
|
|
|
return cls(is_me=False, crypto_power=crypto_power)
|
2017-11-19 03:17:52 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
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)
|
2017-09-28 00:07:36 +00:00
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
@property
|
|
|
|
def seal(self):
|
|
|
|
if not self._seal:
|
|
|
|
raise AttributeError("Seal has not been set up yet.")
|
|
|
|
else:
|
|
|
|
return self._seal
|
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
@property
|
|
|
|
def server(self) -> Server:
|
|
|
|
if self._server:
|
|
|
|
return self._server
|
|
|
|
else:
|
|
|
|
raise RuntimeError("Server hasn't been attached.")
|
|
|
|
|
2017-10-14 22:07:10 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
return self.__class__.__name__
|
|
|
|
|
2017-11-10 18:47:07 +00:00
|
|
|
def hash(self, message):
|
|
|
|
return keccak_digest(message)
|
|
|
|
|
2017-10-07 02:29:07 +00:00
|
|
|
def learn_about_actor(self, actor):
|
|
|
|
self._actor_mapping[actor.id()] = actor
|
2017-10-05 19:20:53 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
def encrypt_for(self, recipient: "Character", cleartext: bytes,
|
|
|
|
sign: bool=True, sign_cleartext=True) -> tuple:
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
2018-02-10 04:15:50 +00:00
|
|
|
Looks up recipient actor, finds that actor's pubkey_enc on our keyring,
|
|
|
|
and encrypts for them. Optionally signs the message as well.
|
2017-10-05 19:20:53 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
:param recipient: The character whose public key will be used to encrypt
|
|
|
|
cleartext.
|
|
|
|
:param cleartext: The secret to be encrypted.
|
2017-10-05 19:20:53 +00:00
|
|
|
:param sign: Whether or not to sign the message.
|
2018-02-10 04:15:50 +00:00
|
|
|
: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.
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
|
|
|
actor = self._lookup_actor(recipient)
|
2017-10-13 02:11:07 +00:00
|
|
|
|
2017-10-05 19:20:53 +00:00
|
|
|
if sign:
|
|
|
|
if sign_cleartext:
|
2017-10-11 05:39:25 +00:00
|
|
|
signature = self.seal(cleartext)
|
2018-02-10 04:15:50 +00:00
|
|
|
ciphertext = self._crypto_power.encrypt_for(
|
|
|
|
actor.public_key(EncryptingPower), signature + cleartext)
|
2017-10-05 19:20:53 +00:00
|
|
|
else:
|
2018-02-10 04:15:50 +00:00
|
|
|
ciphertext = self._crypto_power.encrypt_for(
|
|
|
|
actor.public_key(EncryptingPower), cleartext)
|
2017-10-11 05:39:25 +00:00
|
|
|
signature = self.seal(ciphertext)
|
2017-10-05 19:20:53 +00:00
|
|
|
else:
|
|
|
|
signature = NOT_SIGNED
|
2018-02-10 04:15:50 +00:00
|
|
|
ciphertext = self._crypto_power.encrypt_for(
|
|
|
|
actor.public_key(EncryptingPower), cleartext)
|
2017-10-05 19:20:53 +00:00
|
|
|
|
|
|
|
return ciphertext, signature
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
def verify_from(self,
|
|
|
|
actor_whom_sender_claims_to_be: "Character", message: bytes,
|
|
|
|
signature: Signature=None, decrypt=False,
|
|
|
|
signature_is_on_cleartext=False) -> tuple:
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
|
|
|
Inverse of encrypt_for.
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
: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.
|
2017-10-13 04:31:03 +00:00
|
|
|
:param messages: The messages to be verified.
|
|
|
|
:param decrypt: Whether or not to decrypt the messages.
|
2018-02-10 04:15:50 +00:00
|
|
|
: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
|
2017-10-05 19:20:53 +00:00
|
|
|
"""
|
2017-11-22 06:02:26 +00:00
|
|
|
if not signature and not signature_is_on_cleartext:
|
2018-02-10 04:15:50 +00:00
|
|
|
raise ValueError("You need to either provide the Signature or \
|
|
|
|
decrypt and find it on the cleartext.")
|
2017-11-22 06:02:26 +00:00
|
|
|
|
2017-10-13 02:11:07 +00:00
|
|
|
cleartext = NO_DECRYPTION_PERFORMED
|
2017-11-22 06:02:26 +00:00
|
|
|
|
2017-10-13 02:11:07 +00:00
|
|
|
if signature_is_on_cleartext:
|
|
|
|
if decrypt:
|
2017-10-17 03:13:38 +00:00
|
|
|
cleartext = self._crypto_power.decrypt(message)
|
2018-02-10 04:15:50 +00:00
|
|
|
signature, message = BytestringSplitter(Signature)(cleartext,
|
|
|
|
return_remainder=True)
|
2017-10-13 02:11:07 +00:00
|
|
|
else:
|
|
|
|
raise ValueError(
|
2018-02-10 04:15:50 +00:00
|
|
|
"Can't look for a signature on the cleartext if we're not \
|
|
|
|
decrypting.")
|
2017-10-13 02:11:07 +00:00
|
|
|
|
2017-10-05 19:20:53 +00:00
|
|
|
actor = self._lookup_actor(actor_whom_sender_claims_to_be)
|
2017-10-13 02:11:07 +00:00
|
|
|
|
2017-11-11 07:36:21 +00:00
|
|
|
return signature.verify(message, actor.seal), cleartext
|
2017-10-05 19:20:53 +00:00
|
|
|
|
2017-10-07 02:29:07 +00:00
|
|
|
def _lookup_actor(self, actor: "Character"):
|
2017-10-05 19:20:53 +00:00
|
|
|
try:
|
2017-10-07 02:29:07 +00:00
|
|
|
return self._actor_mapping[actor.id()]
|
2017-10-06 00:26:41 +00:00
|
|
|
except KeyError:
|
2018-02-10 04:15:50 +00:00
|
|
|
raise self.NotFound(
|
|
|
|
"We haven't learned of an actor with ID {}".format(actor.id()))
|
2017-10-07 02:29:07 +00:00
|
|
|
|
|
|
|
def id(self):
|
2017-11-28 04:26:06 +00:00
|
|
|
return hexlify(bytes(self.seal))
|
2017-10-05 19:20:53 +00:00
|
|
|
|
2017-10-17 04:45:43 +00:00
|
|
|
def public_key(self, key_class):
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: Does it make sense to have a specialized exception here? Probably.
|
2017-10-17 03:13:38 +00:00
|
|
|
try:
|
2017-10-17 04:45:43 +00:00
|
|
|
return self._crypto_power.public_keys[key_class]
|
2017-10-17 03:13:38 +00:00
|
|
|
except KeyError:
|
2018-02-10 04:15:50 +00:00
|
|
|
raise
|
2017-10-17 03:13:38 +00:00
|
|
|
|
2017-09-28 00:07:36 +00:00
|
|
|
|
|
|
|
class Alice(Character):
|
|
|
|
_server_class = NuCypherSeedOnlyDHTServer
|
2017-10-17 21:01:25 +00:00
|
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
2017-09-28 00:07:36 +00:00
|
|
|
|
2018-02-11 02:16:12 +00:00
|
|
|
def generate_kfrags(self, bob, m, n):
|
2017-10-19 20:13:41 +00:00
|
|
|
"""
|
|
|
|
Generates re-encryption key frags and returns the frags and encrypted
|
|
|
|
ephemeral key data.
|
|
|
|
|
|
|
|
:param alice_privkey: Alice's private key
|
|
|
|
:param bob_pubkey: Bob's public key
|
|
|
|
:param m: Minimum number of rekey shares needed to rebuild ciphertext
|
|
|
|
:param n: Total number of rekey shares to generate
|
|
|
|
|
|
|
|
:return: Tuple(kfrags, eph_key_data)
|
|
|
|
"""
|
2018-02-11 02:16:12 +00:00
|
|
|
# TODO: Is this how we want to access Alice's private key?
|
|
|
|
alice_priv_enc = self._crypto_power._power_ups[EncryptingPower].keypair.privkey
|
|
|
|
k_frags, _v_keys = umbral.umbral.split_rekey(alice_priv_enc, bob.public_key(EncryptingPower), m, n)
|
|
|
|
return k_frags
|
2017-10-12 22:30:17 +00:00
|
|
|
|
2017-12-16 05:47:02 +00:00
|
|
|
def create_policy(self,
|
2018-02-05 19:25:17 +00:00
|
|
|
bob: "Bob",
|
|
|
|
uri: bytes,
|
|
|
|
m: int,
|
|
|
|
n: int,
|
|
|
|
):
|
2017-12-16 05:47:02 +00:00
|
|
|
"""
|
|
|
|
Alice dictates a new group of policies.
|
|
|
|
"""
|
2018-02-11 02:19:32 +00:00
|
|
|
kfrags = self.generate_kfrags(bob, m, n)
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: Access Alice's private key inside this method.
|
2017-12-16 05:47:02 +00:00
|
|
|
from nkms.policy.models import Policy
|
|
|
|
policy = Policy.from_alice(
|
|
|
|
alice=self,
|
|
|
|
bob=bob,
|
|
|
|
kfrags=kfrags,
|
|
|
|
uri=uri,
|
|
|
|
)
|
|
|
|
|
|
|
|
return policy
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
def grant(self, bob, uri, networky_stuff,
|
|
|
|
m=None, n=None, expiration=None, deposit=None):
|
2017-12-10 01:21:08 +00:00
|
|
|
if not m:
|
|
|
|
# TODO: get m from config
|
|
|
|
raise NotImplementedError
|
|
|
|
if not n:
|
|
|
|
# TODO: get n from config
|
|
|
|
raise NotImplementedError
|
|
|
|
if not expiration:
|
|
|
|
# TODO: check default duration in config
|
|
|
|
raise NotImplementedError
|
2017-12-11 22:43:51 +00:00
|
|
|
if not deposit:
|
2017-12-12 00:55:00 +00:00
|
|
|
default_deposit = None # TODO: Check default deposit in config.
|
2017-12-11 22:43:51 +00:00
|
|
|
if not default_deposit:
|
|
|
|
deposit = networky_stuff.get_competitive_rate()
|
|
|
|
if deposit == NotImplemented:
|
|
|
|
deposit = NON_PAYMENT
|
2017-12-10 01:21:08 +00:00
|
|
|
|
2017-12-16 05:47:02 +00:00
|
|
|
policy = self.create_policy(bob, uri, m, n)
|
2017-12-10 01:21:08 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
# We'll find n Ursulas by default. It's possible to "play the field"
|
|
|
|
# by trying differet
|
2017-12-15 04:31:54 +00:00
|
|
|
# deposits and expirations on a limited number of Ursulas.
|
|
|
|
# Users may decide to inject some market strategies here.
|
2018-02-10 04:15:50 +00:00
|
|
|
found_ursulas = policy.find_ursulas(networky_stuff, deposit,
|
|
|
|
expiration, num_ursulas=n)
|
2017-12-15 05:18:25 +00:00
|
|
|
policy.match_kfrags_to_found_ursulas(found_ursulas)
|
2018-02-10 04:15:50 +00:00
|
|
|
# REST call happens here, as does population of TreasureMap.
|
|
|
|
policy.enact(networky_stuff)
|
2017-12-15 04:31:54 +00:00
|
|
|
|
|
|
|
return policy
|
2017-12-10 01:21:08 +00:00
|
|
|
|
2017-10-12 22:30:17 +00:00
|
|
|
|
|
|
|
class Bob(Character):
|
2017-10-28 04:32:32 +00:00
|
|
|
_server_class = NuCypherSeedOnlyDHTServer
|
2017-10-17 21:01:25 +00:00
|
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
2017-10-12 22:30:17 +00:00
|
|
|
|
2017-12-05 20:49:14 +00:00
|
|
|
def __init__(self, alice=None, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2017-11-07 22:25:03 +00:00
|
|
|
self._ursulas = {}
|
2017-12-12 00:55:00 +00:00
|
|
|
self.treasure_maps = {}
|
2017-10-28 04:32:32 +00:00
|
|
|
if alice:
|
|
|
|
self.alice = alice
|
2017-12-08 04:35:38 +00:00
|
|
|
self._saved_work_orders = {}
|
2017-10-28 04:32:32 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def alice(self):
|
|
|
|
if not self._alice:
|
|
|
|
raise Alice.NotFound
|
|
|
|
else:
|
|
|
|
return self._alice
|
2017-10-28 02:26:23 +00:00
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
@alice.setter
|
|
|
|
def alice(self, alice_object):
|
|
|
|
self.learn_about_actor(alice_object)
|
|
|
|
self._alice = alice_object
|
|
|
|
|
2017-12-15 05:18:50 +00:00
|
|
|
def follow_treasure_map(self, hrac):
|
|
|
|
for ursula_interface_id in self.treasure_maps[hrac]:
|
2017-12-12 00:55:35 +00:00
|
|
|
# TODO: perform this part concurrently.
|
2017-11-07 20:51:30 +00:00
|
|
|
getter = self.server.get(ursula_interface_id)
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
value = loop.run_until_complete(getter)
|
2018-02-10 04:15:50 +00:00
|
|
|
|
|
|
|
# TODO: Make this much prettier
|
|
|
|
signature, ursula_pubkey_sig, hrac, (port, interface, ttl) =\
|
|
|
|
dht_value_splitter(value.lstrip(b"uaddr"), msgpack_remainder=True)
|
2017-11-12 02:00:32 +00:00
|
|
|
|
|
|
|
# TODO: If we're going to implement TTL, it will be here.
|
2018-02-10 04:15:50 +00:00
|
|
|
self._ursulas[ursula_interface_id] =\
|
|
|
|
Ursula.as_discovered_on_network(
|
|
|
|
dht_port=port,
|
|
|
|
dht_interface=interface,
|
|
|
|
pubkey_sig_bytes=ursula_pubkey_sig
|
|
|
|
)
|
2017-11-07 20:51:30 +00:00
|
|
|
|
2017-11-22 06:02:26 +00:00
|
|
|
def get_treasure_map(self, policy_group):
|
2017-11-10 10:04:01 +00:00
|
|
|
|
2017-11-10 18:47:07 +00:00
|
|
|
dht_key = policy_group.treasure_map_dht_key()
|
2017-11-10 10:04:01 +00:00
|
|
|
|
|
|
|
ursula_coro = self.server.get(dht_key)
|
2017-10-28 02:26:23 +00:00
|
|
|
event_loop = asyncio.get_event_loop()
|
2017-10-28 17:53:56 +00:00
|
|
|
packed_encrypted_treasure_map = event_loop.run_until_complete(ursula_coro)
|
2018-02-10 04:15:50 +00:00
|
|
|
|
|
|
|
# TODO: Make this prettier
|
|
|
|
_signature_for_ursula, pubkey_sig_alice, hrac, encrypted_treasure_map =\
|
|
|
|
dht_value_splitter(
|
|
|
|
packed_encrypted_treasure_map[5::], msgpack_remainder=True
|
|
|
|
)
|
|
|
|
verified, cleartext = self.verify_from(
|
|
|
|
self.alice, encrypted_treasure_map,
|
|
|
|
signature_is_on_cleartext=True, decrypt=True
|
|
|
|
)
|
|
|
|
alices_signature, packed_node_list =\
|
|
|
|
BytestringSplitter(Signature)(cleartext, return_remainder=True)
|
|
|
|
|
2017-10-28 04:32:32 +00:00
|
|
|
if not verified:
|
|
|
|
return NOT_FROM_ALICE
|
|
|
|
else:
|
|
|
|
from nkms.policy.models import TreasureMap
|
2018-02-10 04:15:50 +00:00
|
|
|
self.treasure_maps[policy_group.hrac] = TreasureMap(
|
|
|
|
msgpack.loads(packed_node_list)
|
|
|
|
)
|
2017-12-12 00:56:00 +00:00
|
|
|
return self.treasure_maps[policy_group.hrac]
|
2017-10-12 22:30:17 +00:00
|
|
|
|
2017-12-12 00:58:40 +00:00
|
|
|
def generate_work_orders(self, kfrag_hrac, *pfrags, num_ursulas=None):
|
2017-12-01 20:57:51 +00:00
|
|
|
from nkms.policy.models import WorkOrder # Prevent circular import
|
2017-12-01 23:17:18 +00:00
|
|
|
|
2017-12-12 01:00:36 +00:00
|
|
|
try:
|
|
|
|
treasure_map_to_use = self.treasure_maps[kfrag_hrac]
|
|
|
|
except KeyError:
|
2018-02-10 04:15:50 +00:00
|
|
|
raise KeyError(
|
|
|
|
"Bob doesn't have a TreasureMap matching the hrac {}".format(kfrag_hrac))
|
2017-12-12 01:00:36 +00:00
|
|
|
|
2017-12-01 23:17:18 +00:00
|
|
|
generated_work_orders = {}
|
|
|
|
|
2017-12-12 01:02:45 +00:00
|
|
|
if not treasure_map_to_use:
|
|
|
|
raise ValueError("Bob doesn't have a TreasureMap to match any of these pfrags: {}".format(pfrags))
|
|
|
|
|
|
|
|
for ursula_dht_key in treasure_map_to_use:
|
|
|
|
ursula = self._ursulas[ursula_dht_key]
|
2017-12-08 04:35:38 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
completed_work_orders_for_this_ursula =\
|
|
|
|
self._saved_work_orders.setdefault(ursula_dht_key, [])
|
2017-12-08 04:35:38 +00:00
|
|
|
|
|
|
|
pfrags_to_include = []
|
|
|
|
for pfrag in pfrags:
|
2017-12-12 01:02:45 +00:00
|
|
|
if not pfrag in sum([wo.pfrags for wo in completed_work_orders_for_this_ursula],
|
|
|
|
[]): # TODO: This is inane - probably push it down into a WorkOrderHistory concept.
|
2017-12-08 04:35:38 +00:00
|
|
|
pfrags_to_include.append(pfrag)
|
|
|
|
|
|
|
|
if pfrags_to_include:
|
2018-02-10 04:15:50 +00:00
|
|
|
work_order = WorkOrder.constructed_by_bob(
|
|
|
|
kfrag_hrac, pfrags_to_include, ursula_dht_key, self)
|
2017-12-08 04:35:38 +00:00
|
|
|
generated_work_orders[ursula_dht_key] = work_order
|
2017-12-01 23:17:18 +00:00
|
|
|
|
|
|
|
if num_ursulas is not None:
|
|
|
|
if num_ursulas == len(generated_work_orders):
|
|
|
|
break
|
|
|
|
|
|
|
|
return generated_work_orders
|
|
|
|
|
2017-12-08 04:36:09 +00:00
|
|
|
def get_reencrypted_c_frags(self, networky_stuff, work_order):
|
2017-12-05 20:51:01 +00:00
|
|
|
cfrags = networky_stuff.reencrypt(work_order)
|
2017-12-08 04:36:09 +00:00
|
|
|
if not len(work_order) == len(cfrags):
|
|
|
|
raise ValueError("Ursula gave back the wrong number of cfrags. She's up to something.")
|
|
|
|
for counter, pfrag in enumerate(work_order.pfrags):
|
|
|
|
# TODO: Ursula is actually supposed to sign this. See #141.
|
|
|
|
self._saved_work_orders[work_order.ursula_id].append(work_order)
|
2017-12-05 20:51:01 +00:00
|
|
|
return cfrags
|
|
|
|
|
|
|
|
def get_ursula(self, ursula_id):
|
|
|
|
return self._ursulas[ursula_id]
|
2017-12-01 20:57:51 +00:00
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
|
2017-10-12 22:30:17 +00:00
|
|
|
class Ursula(Character):
|
|
|
|
_server_class = NuCypherDHTServer
|
2017-10-17 21:01:25 +00:00
|
|
|
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
2017-10-27 01:39:13 +00:00
|
|
|
|
2018-02-05 07:39:40 +00:00
|
|
|
dht_port = None
|
|
|
|
dht_interface = None
|
|
|
|
dht_ttl = 0
|
|
|
|
rest_address = None
|
|
|
|
rest_port = None
|
2017-11-03 04:54:58 +00:00
|
|
|
|
2017-11-16 23:36:31 +00:00
|
|
|
def __init__(self, urulsas_keystore=None, *args, **kwargs):
|
2017-11-16 23:14:43 +00:00
|
|
|
super().__init__(*args, **kwargs)
|
2017-11-21 20:04:33 +00:00
|
|
|
self.keystore = urulsas_keystore
|
2017-11-16 23:14:43 +00:00
|
|
|
|
2017-11-19 19:58:33 +00:00
|
|
|
self._rest_app = None
|
2017-12-05 20:51:44 +00:00
|
|
|
self._work_orders = []
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: This needs to actually be a persistent data store. See #127.
|
|
|
|
self._contracts = {}
|
2017-11-19 19:58:33 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def rest_app(self):
|
|
|
|
if not self._rest_app:
|
2017-11-21 17:38:04 +00:00
|
|
|
raise AttributeError(
|
|
|
|
"This Ursula doesn't have a REST app attached. If you want one, init with is_me and attach_server.")
|
2017-11-19 19:58:33 +00:00
|
|
|
else:
|
|
|
|
return self._rest_app
|
|
|
|
|
2018-02-05 07:39:40 +00:00
|
|
|
@classmethod
|
2018-02-10 04:15:50 +00:00
|
|
|
def as_discovered_on_network(cls, dht_port, dht_interface, pubkey_sig_bytes,
|
|
|
|
rest_address=None, rest_port=None):
|
2018-02-05 19:25:17 +00:00
|
|
|
# TODO: We also need the encrypting public key here.
|
2018-02-07 01:15:53 +00:00
|
|
|
ursula = cls.from_public_keys((SigningPower, pubkey_sig_bytes))
|
2018-02-05 07:39:40 +00:00
|
|
|
ursula.dht_port = dht_port
|
|
|
|
ursula.dht_interface = dht_interface
|
|
|
|
ursula.rest_address = rest_address
|
|
|
|
ursula.rest_port = rest_port
|
2017-11-07 22:25:03 +00:00
|
|
|
return ursula
|
2017-11-07 20:51:30 +00:00
|
|
|
|
2018-02-06 07:09:21 +00:00
|
|
|
@classmethod
|
|
|
|
def from_rest_url(cls, url):
|
|
|
|
response = requests.get(url)
|
|
|
|
if not response.status_code == 200:
|
|
|
|
raise RuntimeError("Got a bad response: {}".format(response))
|
2018-02-10 04:15:50 +00:00
|
|
|
signing_key_bytes, encrypting_key_bytes =\
|
|
|
|
BytestringSplitter(PublicKey)(response.content,
|
|
|
|
return_remainder=True)
|
|
|
|
stranger_ursula_from_public_keys = cls.from_public_keys(
|
|
|
|
signing=signing_key_bytes, encrypting=encrypting_key_bytes)
|
2018-02-06 07:09:21 +00:00
|
|
|
return stranger_ursula_from_public_keys
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
def attach_server(self, ksize=20, alpha=3, id=None,
|
|
|
|
storage=None, *args, **kwargs):
|
2017-11-03 04:54:58 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: Network-wide deterministic ID generation (ie, auction or
|
|
|
|
# whatever) See #136.
|
2017-11-03 04:54:58 +00:00
|
|
|
if not id:
|
2018-02-10 04:15:50 +00:00
|
|
|
id = digest(secure_random(32))
|
2017-11-03 04:54:58 +00:00
|
|
|
|
|
|
|
super().attach_server(ksize, alpha, id, storage)
|
|
|
|
|
2017-11-19 19:58:33 +00:00
|
|
|
routes = [
|
2018-02-10 04:15:50 +00:00
|
|
|
Route('/kFrag/{hrac_as_hex}',
|
|
|
|
'POST',
|
|
|
|
self.set_policy),
|
|
|
|
Route('/kFrag/{hrac_as_hex}/reencrypt',
|
|
|
|
'POST',
|
|
|
|
self.reencrypt_via_rest),
|
|
|
|
Route('/public_keys', 'GET',
|
|
|
|
self.get_signing_and_encrypting_public_keys),
|
|
|
|
Route('/consider_contract',
|
|
|
|
'POST',
|
|
|
|
self.consider_contract),
|
2017-11-19 19:58:33 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
self._rest_app = App(routes=routes)
|
|
|
|
|
2017-11-03 04:54:58 +00:00
|
|
|
def listen(self, port, interface):
|
2018-02-07 01:16:21 +00:00
|
|
|
self.dht_port = port
|
|
|
|
self.dht_interface = interface
|
2017-11-03 04:54:58 +00:00
|
|
|
return self.server.listen(port, interface)
|
|
|
|
|
2017-12-01 20:58:30 +00:00
|
|
|
def dht_interface_info(self):
|
2018-02-06 11:08:23 +00:00
|
|
|
return self.dht_port, self.dht_interface, self.dht_ttl
|
2017-11-10 10:04:01 +00:00
|
|
|
|
|
|
|
def interface_dht_key(self):
|
2017-11-12 01:46:27 +00:00
|
|
|
return self.hash(self.seal + self.interface_hrac())
|
2017-11-10 10:04:01 +00:00
|
|
|
|
|
|
|
def interface_dht_value(self):
|
2017-11-12 01:46:27 +00:00
|
|
|
signature = self.seal(self.interface_hrac())
|
2018-02-10 04:15:50 +00:00
|
|
|
return (
|
|
|
|
b"uaddr" + signature + self.seal + self.interface_hrac()
|
|
|
|
+ msgpack.dumps(self.dht_interface_info())
|
|
|
|
)
|
2017-11-12 01:46:27 +00:00
|
|
|
|
|
|
|
def interface_hrac(self):
|
2017-12-01 20:58:30 +00:00
|
|
|
return self.hash(msgpack.dumps(self.dht_interface_info()))
|
2017-11-10 10:04:01 +00:00
|
|
|
|
2018-02-07 01:16:21 +00:00
|
|
|
def publish_dht_information(self):
|
|
|
|
if not self.dht_port and self.dht_interface:
|
2017-11-03 04:54:58 +00:00
|
|
|
raise RuntimeError("Must listen before publishing interface information.")
|
2017-11-06 03:04:04 +00:00
|
|
|
|
2017-11-10 10:04:01 +00:00
|
|
|
dht_key = self.interface_dht_key()
|
|
|
|
value = self.interface_dht_value()
|
|
|
|
setter = self.server.set(key=dht_key, value=value)
|
|
|
|
blockchain_client._ursulas_on_blockchain.append(dht_key)
|
2017-11-03 04:54:58 +00:00
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
loop.run_until_complete(setter)
|
|
|
|
|
2018-02-06 07:09:46 +00:00
|
|
|
def get_signing_and_encrypting_public_keys(self):
|
|
|
|
"""
|
|
|
|
REST endpoint for getting both signing and encrypting public keys.
|
|
|
|
"""
|
2018-02-10 04:15:50 +00:00
|
|
|
return Response(
|
|
|
|
content=bytes(self.seal) + bytes(self.public_key(EncryptingPower)),
|
|
|
|
content_type="application/octet-stream")
|
2018-02-06 07:09:46 +00:00
|
|
|
|
|
|
|
def consider_contract(self, hrac_as_hex, request: http.Request):
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: This actually needs to be a REST endpoint, with the payload
|
|
|
|
# carrying the kfrag hash separately.
|
2018-02-06 07:09:46 +00:00
|
|
|
from nkms.policy.models import Contract
|
2018-02-10 04:15:50 +00:00
|
|
|
contract, deposit_as_bytes =\
|
|
|
|
BytestringSplitter(Contract)(request.body, return_remainder=True)
|
2018-02-06 07:09:46 +00:00
|
|
|
contract.deposit = deposit_as_bytes
|
|
|
|
|
|
|
|
contract_to_store = { # TODO: This needs to be a datastore - see #127.
|
|
|
|
"alice_pubkey_sig": bytes(contract.alice.seal),
|
|
|
|
"deposit": contract.deposit,
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: Whatever type "deposit" ends up being, we'll need to
|
|
|
|
# serialize it here. See #148.
|
2018-02-06 07:09:46 +00:00
|
|
|
"expiration": contract.expiration,
|
|
|
|
}
|
|
|
|
self._contracts[contract.hrac] = contract_to_store
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: Make the rest of this logic actually work - do something here
|
|
|
|
# to decide if this Contract is worth accepting.
|
|
|
|
return Response(
|
|
|
|
b"This will eventually be an actual acceptance of the contract.",
|
|
|
|
content_type="application/octet-stream")
|
2018-02-06 07:09:46 +00:00
|
|
|
|
2017-11-21 03:49:43 +00:00
|
|
|
def set_policy(self, hrac_as_hex, request: http.Request):
|
2017-11-16 23:07:12 +00:00
|
|
|
"""
|
|
|
|
REST endpoint for setting a kFrag.
|
2018-02-10 04:15:50 +00:00
|
|
|
TODO: Instead of taking a Request, use the apistar typing system to type
|
|
|
|
a payload and validate / split it.
|
|
|
|
TODO: Validate that the kfrag being saved is pursuant to an approved
|
|
|
|
Policy (see #121).
|
2017-11-16 23:07:12 +00:00
|
|
|
"""
|
2017-12-14 20:46:36 +00:00
|
|
|
from nkms.policy.models import Contract # Avoid circular import
|
2017-11-21 03:49:43 +00:00
|
|
|
hrac = binascii.unhexlify(hrac_as_hex)
|
2017-12-16 05:47:52 +00:00
|
|
|
|
|
|
|
group_payload_splitter = BytestringSplitter(PublicKey)
|
2018-02-07 10:31:27 +00:00
|
|
|
policy_payload_splitter = BytestringSplitter((KFrag, KFRAG_LENGTH))
|
2017-12-16 05:47:52 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
alice_pubkey_sig, payload_encrypted_for_ursula =\
|
|
|
|
group_payload_splitter(request.body, msgpack_remainder=True)
|
2018-02-07 01:15:53 +00:00
|
|
|
alice = Alice.from_public_keys((SigningPower, alice_pubkey_sig))
|
2017-12-16 05:47:52 +00:00
|
|
|
self.learn_about_actor(alice)
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
verified, cleartext = self.verify_from(
|
|
|
|
alice, payload_encrypted_for_ursula,
|
|
|
|
decrypt=True, signature_is_on_cleartext=True)
|
2017-12-16 05:47:52 +00:00
|
|
|
|
|
|
|
if not verified:
|
|
|
|
# TODO: What do we do if the Policy isn't signed properly?
|
|
|
|
pass
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
alices_signature, policy_payload =\
|
|
|
|
BytestringSplitter(Signature)(cleartext, return_remainder=True)
|
2017-12-16 05:47:52 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: If we're not adding anything else in the payload, stop using the
|
|
|
|
# splitter here.
|
|
|
|
kfrag = policy_payload_splitter(policy_payload)[0]
|
2017-12-16 05:47:52 +00:00
|
|
|
|
|
|
|
# TODO: Query stored Contract and reconstitute
|
|
|
|
contract_details = self._contracts[hrac]
|
|
|
|
stored_alice_pubkey_sig = contract_details.pop("alice_pubkey_sig")
|
|
|
|
|
|
|
|
if stored_alice_pubkey_sig != alice_pubkey_sig:
|
|
|
|
raise Alice.SuspiciousActivity
|
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
contract = Contract(alice=alice, hrac=hrac,
|
|
|
|
kfrag=kfrag, **contract_details)
|
2017-11-19 03:19:39 +00:00
|
|
|
|
2017-11-17 00:21:48 +00:00
|
|
|
try:
|
2017-12-16 05:47:52 +00:00
|
|
|
self.keystore.add_kfrag(hrac, contract.kfrag, alices_signature)
|
2017-11-21 03:19:18 +00:00
|
|
|
except IntegrityError:
|
2017-11-17 00:21:48 +00:00
|
|
|
raise
|
2017-11-19 03:19:39 +00:00
|
|
|
# Do something appropriately RESTful (ie, 4xx).
|
2017-11-17 00:21:48 +00:00
|
|
|
|
2017-12-16 05:47:52 +00:00
|
|
|
return # TODO: Return A 200, with whatever policy metadata.
|
2017-12-02 00:01:07 +00:00
|
|
|
|
|
|
|
def reencrypt_via_rest(self, hrac_as_hex, request: http.Request):
|
|
|
|
from nkms.policy.models import WorkOrder # Avoid circular import
|
|
|
|
hrac = binascii.unhexlify(hrac_as_hex)
|
|
|
|
work_order = WorkOrder.from_rest_payload(hrac, request.body)
|
2017-12-02 01:09:02 +00:00
|
|
|
kfrag = self.keystore.get_kfrag(hrac) # Careful! :-)
|
2017-12-05 01:17:56 +00:00
|
|
|
cfrag_byte_stream = b""
|
2017-12-02 01:09:02 +00:00
|
|
|
|
|
|
|
for pfrag in work_order.pfrags:
|
2017-12-08 04:36:36 +00:00
|
|
|
# TODO: Sign the result of this. See #141.
|
2017-12-05 20:51:44 +00:00
|
|
|
cfrag_byte_stream += API.ecies_reencrypt(kfrag, pfrag.encrypted_key)
|
2017-12-02 01:09:02 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
# TODO: Put this in Ursula's datastore
|
|
|
|
self._work_orders.append(work_order)
|
2017-12-05 20:51:44 +00:00
|
|
|
|
2018-02-10 04:15:50 +00:00
|
|
|
return Response(content=cfrag_byte_stream,
|
|
|
|
content_type="application/octet-stream")
|
2017-12-05 20:51:44 +00:00
|
|
|
|
|
|
|
def work_orders(self, bob=None):
|
|
|
|
"""
|
|
|
|
TODO: This is better written as a model method for Ursula's datastore.
|
|
|
|
"""
|
|
|
|
if not bob:
|
|
|
|
return self._work_orders
|
|
|
|
else:
|
|
|
|
work_orders_from_bob = []
|
|
|
|
for work_order in self._work_orders:
|
|
|
|
if work_order.bob == bob:
|
|
|
|
work_orders_from_bob.append(work_order)
|
|
|
|
return work_orders_from_bob
|
2017-11-16 23:07:12 +00:00
|
|
|
|
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
class Seal(object):
|
|
|
|
"""
|
2018-02-10 04:15:50 +00:00
|
|
|
Can be called to sign something or used to express the signing public
|
|
|
|
key as bytes.
|
2017-11-07 20:51:30 +00:00
|
|
|
"""
|
2017-11-07 22:25:03 +00:00
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
def __init__(self, character):
|
|
|
|
self.character = character
|
|
|
|
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
return self.character._crypto_power.sign(*args, **kwargs)
|
|
|
|
|
|
|
|
def _as_tuple(self):
|
|
|
|
return self.character._crypto_power.pubkey_sig_tuple()
|
|
|
|
|
|
|
|
def __iter__(seal):
|
|
|
|
yield from seal._as_tuple()
|
|
|
|
|
|
|
|
def __bytes__(self):
|
|
|
|
return self.character._crypto_power.pubkey_sig_bytes()
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return other == self._as_tuple() or other == bytes(self)
|
|
|
|
|
2017-11-11 23:49:15 +00:00
|
|
|
def __add__(self, other):
|
|
|
|
return bytes(self) + other
|
|
|
|
|
|
|
|
def __radd__(self, other):
|
|
|
|
return other + bytes(self)
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(bytes(self))
|
|
|
|
|
2017-11-18 21:11:27 +00:00
|
|
|
def without_metabytes(self):
|
|
|
|
return self.character._crypto_power.pubkey_sig_bytes().without_metabytes()
|
|
|
|
|
2017-11-07 22:25:03 +00:00
|
|
|
|
|
|
|
class StrangerSeal(Seal):
|
|
|
|
"""
|
|
|
|
Seal of a stranger (ie, can only be used to glean public key, not to sign)
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
raise TypeError(
|
|
|
|
"This isn't your Seal; it belongs to {} (a Stranger). You can't sign with it.".format(self.character))
|
|
|
|
|
2017-11-07 20:51:30 +00:00
|
|
|
|
|
|
|
def congregate(*characters):
|
2017-10-27 01:39:13 +00:00
|
|
|
for character in characters:
|
|
|
|
for newcomer in characters:
|
2017-11-03 04:54:58 +00:00
|
|
|
character.learn_about_actor(newcomer)
|