mirror of https://github.com/nucypher/nucypher.git
390 lines
15 KiB
Python
390 lines
15 KiB
Python
import asyncio
|
|
import binascii
|
|
import uuid
|
|
from collections import OrderedDict
|
|
from datetime import datetime
|
|
|
|
import maya
|
|
import msgpack
|
|
|
|
from nkms.characters import Alice
|
|
from nkms.characters import Bob, Ursula
|
|
from nkms.crypto.api import keccak_digest
|
|
from nkms.crypto.constants import KECCAK_DIGEST_LENGTH
|
|
from nkms.crypto.powers import SigningPower, DelegatingPower
|
|
from nkms.crypto.signature import Signature
|
|
from nkms.crypto.splitters import key_splitter
|
|
from bytestring_splitter import BytestringSplitter
|
|
from nkms.blockchain.eth.policies import BlockchainArrangement
|
|
from umbral.pre import Capsule
|
|
from constant_sorrow import constants
|
|
|
|
|
|
class Arrangement(BlockchainArrangement):
|
|
"""
|
|
A Policy must be implemented by arrangements with n Ursulas. This class tracks the status of that implementation.
|
|
"""
|
|
_EXPECTED_LENGTH = 106
|
|
splitter = key_splitter + BytestringSplitter((bytes, KECCAK_DIGEST_LENGTH),
|
|
(bytes, 27), (bytes, 7))
|
|
|
|
def __init__(self, alice, hrac, expiration, deposit=None, ursula=None,
|
|
kfrag=constants.UNKNOWN_KFRAG, alices_signature=None):
|
|
"""
|
|
:param deposit: Funds which will pay for the timeframe of this Arrangement (not the actual re-encryptions);
|
|
a portion will be locked for each Ursula that accepts.
|
|
:param expiration: The moment which Alice wants the Arrangement to end.
|
|
|
|
Other params are hopefully self-evident.
|
|
"""
|
|
self.expiration = expiration
|
|
self.deposit = deposit
|
|
self.hrac = hrac
|
|
self.alice = alice
|
|
|
|
"""
|
|
These will normally not be set if Alice is drawing up this arrangement - she hasn't assigned a kfrag yet
|
|
(because she doesn't know if this Arrangement will be accepted). She doesn't have an Ursula, for the same reason.
|
|
"""
|
|
self.kfrag = kfrag
|
|
self.ursula = ursula
|
|
|
|
arrangement_delta = maya.now() - self.expiration
|
|
policy_duration = arrangement_delta.days
|
|
|
|
super().__init__(author=self.alice, miner=ursula,
|
|
value=self.deposit, periods=policy_duration,
|
|
arrangement_id=self._make_arrangement_id())
|
|
|
|
def __bytes__(self):
|
|
return bytes(self.alice.stamp) + bytes(
|
|
self.hrac) + self.expiration.iso8601().encode() + bytes(
|
|
self.deposit)
|
|
|
|
@staticmethod
|
|
def _make_arrangement_id():
|
|
arrangement_id = str(uuid.uuid4()).encode()
|
|
return arrangement_id
|
|
|
|
@classmethod
|
|
def from_bytes(cls, arrangement_as_bytes):
|
|
# Still unclear how to arrive at the correct number of bytes to represent a deposit. See #148.
|
|
alice_pubkey_sig, hrac, expiration_bytes, deposit_bytes = cls.splitter(arrangement_as_bytes)
|
|
expiration = maya.parse(expiration_bytes.decode())
|
|
alice = Alice.from_public_keys({SigningPower: alice_pubkey_sig})
|
|
return cls(alice=alice, hrac=hrac, expiration=expiration, deposit=int(deposit_bytes))
|
|
|
|
def publish(self, kfrag, ursula, negotiation_result):
|
|
self.kfrag = kfrag
|
|
self.ursula = ursula
|
|
self.negotiation_result = negotiation_result
|
|
|
|
# Publish arrangement to blockchain
|
|
# TODO Determine actual gas price here
|
|
# TODO Negotiate the receipt of a KFrag per Ursula
|
|
# super().publish(gas_price=0)
|
|
|
|
def encrypt_payload_for_ursula(self):
|
|
"""
|
|
Craft an offer to send to Ursula.
|
|
"""
|
|
# We don't need the signature separately.
|
|
return self.alice.encrypt_for(self.ursula, self.payload())[0]
|
|
|
|
def payload(self):
|
|
# TODO: Ship the expiration again? Or some other way of alerting Ursula to recall her previous dialogue regarding this Arrangement. Update: We'll probably have her store the Arrangement by hrac. See #127.
|
|
return bytes(self.kfrag)
|
|
|
|
|
|
class ArrangementResponse(object):
|
|
pass
|
|
|
|
|
|
class Policy(object):
|
|
"""
|
|
An edict by Alice, arranged with n Ursulas, to perform re-encryption for a specific Bob
|
|
for a specific path.
|
|
|
|
Once Alice is ready to enact a Policy, she generates KFrags, which become part of the Policy.
|
|
|
|
Each Ursula is offered a Arrangement (see above) for a given Policy by Alice.
|
|
|
|
Once Alice has secured agreement with n Ursulas to enact a Policy, she sends each a KFrag,
|
|
and generates a TreasureMap for the Policy, recording which Ursulas got a KFrag.
|
|
"""
|
|
_ursula = None
|
|
|
|
def __init__(self, alice, bob=None, kfrags=(constants.UNKNOWN_KFRAG,),
|
|
label=None, m=None, alices_signature=constants.NOT_SIGNED):
|
|
|
|
"""
|
|
:param kfrags: A list of KFrags to distribute per this Policy.
|
|
:param label: The identity of the resource to which Bob is granted access.
|
|
"""
|
|
self.alice = alice
|
|
self.bob = bob
|
|
self.kfrags = kfrags
|
|
self.uri = label
|
|
self.treasure_map = TreasureMap(m=m)
|
|
self._accepted_arrangements = OrderedDict()
|
|
|
|
self.alices_signature = alices_signature
|
|
|
|
class MoreArrangementsThanKFrags(TypeError):
|
|
"""
|
|
Raised when a Policy has been used to generate Arrangements with Ursulas in sufficient number
|
|
such that we don't have enough KFrags to give to each Ursula.
|
|
"""
|
|
|
|
@property
|
|
def n(self):
|
|
return len(self.kfrags)
|
|
|
|
@property
|
|
def ursula(self):
|
|
if not self._ursula:
|
|
raise Ursula.NotFound
|
|
else:
|
|
return self._ursula
|
|
|
|
@ursula.setter
|
|
def ursula(self, ursula_object):
|
|
self.alice.learn_about_actor(ursula_object)
|
|
self._ursula = ursula_object
|
|
|
|
@staticmethod
|
|
def from_alice(kfrags,
|
|
alice,
|
|
bob,
|
|
uri,
|
|
m,
|
|
):
|
|
# TODO: What happened to Alice's signature - don't we include it here?
|
|
policy = Policy(alice, bob, kfrags, uri, m)
|
|
|
|
return policy
|
|
|
|
def hrac(self):
|
|
"""
|
|
A convenience method for generating an hrac for this instance.
|
|
"""
|
|
return self.hrac_for(self.alice, self.bob, self.uri)
|
|
|
|
@staticmethod
|
|
def hrac_for(alice, bob, uri):
|
|
"""
|
|
The "hashed resource authentication code".
|
|
|
|
A hash of:
|
|
* Alice's public key
|
|
* Bob's public key
|
|
* the uri
|
|
|
|
Alice and Bob have all the information they need to construct this.
|
|
Ursula does not, so we share it with her.
|
|
"""
|
|
return keccak_digest(bytes(alice.stamp) + bytes(bob.stamp) + uri)
|
|
|
|
def treasure_map_dht_key(self):
|
|
"""
|
|
We need a key that Bob can glean from knowledge he already has *and* which Ursula can verify came from us.
|
|
Ursula will refuse to propagate this key if it she can't prove that our public key, which is included in it,
|
|
was used to sign the payload.
|
|
|
|
Our public key (which everybody knows) and the hrac above.
|
|
"""
|
|
return keccak_digest(bytes(self.alice.stamp) + self.hrac())
|
|
|
|
def publish_treasure_map(self, networky_stuff=None, use_dht=False):
|
|
if networky_stuff is None and use_dht is False:
|
|
raise ValueError("Can't engage the REST swarm without networky stuff.")
|
|
tmap_message_kit, signature_for_bob = self.alice.encrypt_for(
|
|
self.bob,
|
|
self.treasure_map.packed_payload())
|
|
signature_for_ursula = self.alice.stamp(self.hrac())
|
|
|
|
# In order to know this is safe to propagate, Ursula needs to see a signature, our public key,
|
|
# and, reasons explained in treasure_map_dht_key above, the uri_hash.
|
|
# TODO: Clean this up. See #172.
|
|
map_payload = signature_for_ursula + self.alice.stamp + self.hrac() + tmap_message_kit.to_bytes()
|
|
map_id = self.treasure_map_dht_key()
|
|
|
|
if use_dht:
|
|
# Instead of self.alice, let's say self.author. See #230.
|
|
setter = self.alice.server.set(map_id, constants.BYTESTRING_IS_TREASURE_MAP + map_payload)
|
|
event_loop = asyncio.get_event_loop()
|
|
event_loop.run_until_complete(setter)
|
|
else:
|
|
if not self.alice.known_nodes:
|
|
raise RuntimeError("Alice hasn't learned of any nodes. Thus, she can't push the TreasureMap.")
|
|
for node in self.alice.known_nodes.values():
|
|
response = networky_stuff.push_treasure_map_to_node(node, map_id, constants.BYTESTRING_IS_TREASURE_MAP + map_payload)
|
|
# TODO: Do something here based on success or failure
|
|
if response.status_code == 204:
|
|
pass
|
|
return tmap_message_kit, map_payload, signature_for_bob, signature_for_ursula
|
|
|
|
def enact(self, networky_stuff):
|
|
for arrangement in self._accepted_arrangements.values():
|
|
policy_message_kit = arrangement.encrypt_payload_for_ursula()
|
|
response = networky_stuff.enact_policy(arrangement.ursula,
|
|
self.hrac(),
|
|
policy_message_kit.to_bytes())
|
|
# TODO: Parse response for confirmation.
|
|
response
|
|
|
|
# Assuming response is what we hope for
|
|
self.treasure_map.add_ursula(arrangement.ursula)
|
|
|
|
def make_arrangement(self, deposit, expiration):
|
|
return Arrangement(self.alice, self.hrac(), expiration=expiration,
|
|
deposit=deposit)
|
|
|
|
def find_ursulas(self, networky_stuff, deposit, expiration,
|
|
num_ursulas=None):
|
|
"""
|
|
:param networky_stuff: A compliant interface (maybe a Client instance) to be used to engage the DHT swarm.
|
|
"""
|
|
if num_ursulas is None:
|
|
num_ursulas = self.n
|
|
|
|
found_ursulas = []
|
|
while len(found_ursulas) < num_ursulas:
|
|
arrangement = self.make_arrangement(deposit, expiration)
|
|
try:
|
|
ursula, result = networky_stuff.find_ursula(arrangement)
|
|
found_ursulas.append((ursula, arrangement, result))
|
|
except networky_stuff.NotEnoughQualifiedUrsulas:
|
|
pass # TODO: Tell Alice to either wait or lower the value of num_ursulas.
|
|
return found_ursulas
|
|
|
|
def assign_kfrag_to_arrangement(self, arrangement):
|
|
for kfrag in self.kfrags:
|
|
if not kfrag in self._accepted_arrangements:
|
|
arrangement.kfrag = kfrag
|
|
self._accepted_arrangements[kfrag] = arrangement
|
|
return kfrag
|
|
if not arrangement.kfrag:
|
|
raise self.MoreArrangementsThanKFrags # TODO: Perhaps in a future version, we consider allowing Alice to assign *the same* KFrag to multiple Ursulas?
|
|
|
|
def match_kfrags_to_found_ursulas(self, found_ursulas):
|
|
for ursula, arrangement, result in found_ursulas:
|
|
if result.was_accepted: # TODO: Here, we need to assess the result and see if we're actually good to go.
|
|
kfrag = self.assign_kfrag_to_arrangement(arrangement)
|
|
arrangement.publish(kfrag, ursula, result)
|
|
# TODO: What if there weren't enough Arrangements approved to distribute n kfrags? We need to raise NotEnoughQualifiedUrsulas.
|
|
|
|
def public_key(self):
|
|
return self.alice.public_key(DelegatingPower)
|
|
|
|
|
|
class TreasureMap(object):
|
|
def __init__(self, m, ursula_interface_ids=None):
|
|
self.m = m
|
|
self.ids = set(ursula_interface_ids or set())
|
|
|
|
def packed_payload(self):
|
|
return msgpack.dumps(self.nodes_as_bytes() + [self.m])
|
|
|
|
def nodes_as_bytes(self):
|
|
return [bytes(ursula_id) for ursula_id in self.ids]
|
|
|
|
def add_ursula(self, ursula):
|
|
self.ids.add(bytes(ursula.stamp))
|
|
|
|
def __eq__(self, other):
|
|
return self.ids == other.ids
|
|
|
|
def __iter__(self):
|
|
return iter(self.ids)
|
|
|
|
def __len__(self):
|
|
return len(self.ids)
|
|
|
|
|
|
class WorkOrder(object):
|
|
def __init__(self, bob, kfrag_hrac, capsules, receipt_bytes,
|
|
receipt_signature, ursula=None):
|
|
self.bob = bob
|
|
self.kfrag_hrac = kfrag_hrac
|
|
self.capsules = capsules
|
|
self.receipt_bytes = receipt_bytes
|
|
self.receipt_signature = receipt_signature
|
|
self.ursula = ursula # TODO: We may still need a more elegant system for ID'ing Ursula. See #136.
|
|
|
|
def __repr__(self):
|
|
return "WorkOrder for hrac {hrac}: (capsules: {capsule_bytes}) for {ursulas}".format(
|
|
hrac=self.kfrag_hrac.hex()[:6],
|
|
capsule_bytes=[binascii.hexlify(bytes(cap))[:6] for cap in self.capsules],
|
|
ursulas=binascii.hexlify(bytes(self.ursula.stamp))[:6])
|
|
|
|
def __eq__(self, other):
|
|
return (self.receipt_bytes, self.receipt_signature) == (
|
|
other.receipt_bytes, other.receipt_signature)
|
|
|
|
def __len__(self):
|
|
return len(self.capsules)
|
|
|
|
@classmethod
|
|
def construct_by_bob(cls, kfrag_hrac, capsules, ursula, bob):
|
|
receipt_bytes = b"wo:" + ursula.interface_information() # TODO: represent the capsules as bytes and hash them as part of the receipt, ie + keccak_digest(b"".join(capsules)) - See #137
|
|
receipt_signature = bob.stamp(receipt_bytes)
|
|
return cls(bob, kfrag_hrac, capsules, receipt_bytes, receipt_signature,
|
|
ursula)
|
|
|
|
@classmethod
|
|
def from_rest_payload(cls, kfrag_hrac, rest_payload):
|
|
payload_splitter = BytestringSplitter(Signature) + key_splitter
|
|
signature, bob_pubkey_sig, (receipt_bytes, packed_capsules) = payload_splitter(rest_payload,
|
|
msgpack_remainder=True)
|
|
capsules = [Capsule.from_bytes(p) for p in msgpack.loads(packed_capsules)]
|
|
verified = signature.verify(receipt_bytes, bob_pubkey_sig)
|
|
if not verified:
|
|
raise ValueError("This doesn't appear to be from Bob.")
|
|
bob = Bob.from_public_keys({SigningPower: bob_pubkey_sig})
|
|
return cls(bob, kfrag_hrac, capsules, receipt_bytes, signature)
|
|
|
|
def payload(self):
|
|
capsules_as_bytes = [bytes(p) for p in self.capsules]
|
|
packed_receipt_and_capsules = msgpack.dumps(
|
|
(self.receipt_bytes, msgpack.dumps(capsules_as_bytes)))
|
|
return bytes(self.receipt_signature) + self.bob.stamp + packed_receipt_and_capsules
|
|
|
|
def complete(self, cfrags):
|
|
# TODO: Verify that this is in fact complete - right number of CFrags and properly signed.
|
|
# TODO: Mark it complete with datetime.
|
|
pass
|
|
|
|
|
|
class WorkOrderHistory:
|
|
|
|
def __init__(self):
|
|
self.by_ursula = {}
|
|
|
|
def __contains__(self, item):
|
|
assert False
|
|
|
|
def __getitem__(self, item):
|
|
if isinstance(item, bytes):
|
|
return self.by_ursula.setdefault(item, {})
|
|
else:
|
|
raise TypeError("If you want to lookup a WorkOrder by Ursula, you need to pass bytes of her signing public key.")
|
|
|
|
def __setitem__(self, key, value):
|
|
assert False
|
|
|
|
def __len__(self):
|
|
return sum(len(work_orders) for work_orders in self.by_ursula.values())
|
|
|
|
@property
|
|
def ursulas(self):
|
|
return self.by_ursula.keys()
|
|
|
|
def by_capsule(self, capsule):
|
|
ursulas_by_capsules = {}
|
|
for ursula, pfrags in self.by_ursula.items():
|
|
for saved_pfrag, work_order in pfrags.items():
|
|
if saved_pfrag == capsule:
|
|
ursulas_by_capsules[ursula] = work_order
|
|
return ursulas_by_capsules
|