mirror of https://github.com/nucypher/nucypher.git
389 lines
15 KiB
Python
389 lines
15 KiB
Python
import asyncio
|
|
import binascii
|
|
|
|
import msgpack
|
|
|
|
from nkms.characters import Alice, Bob, Ursula
|
|
from nkms.crypto import api
|
|
from nkms.crypto.api import keccak_digest
|
|
from nkms.crypto.constants import NOT_SIGNED
|
|
from nkms.crypto.fragments import KFrag, PFrag
|
|
from nkms.crypto.powers import EncryptingPower
|
|
from nkms.crypto.signature import Signature
|
|
from nkms.crypto.utils import BytestringSplitter
|
|
from nkms.keystore.keypairs import PublicKey
|
|
from npre.constants import UNKNOWN_KFRAG
|
|
|
|
group_payload_splitter = BytestringSplitter(PublicKey)
|
|
policy_payload_splitter = BytestringSplitter(KFrag)
|
|
|
|
|
|
class Contract(object):
|
|
"""
|
|
A Policy must be implemented by agreement with n Ursulas. This class tracks the status of that implementation.
|
|
"""
|
|
|
|
def __init__(self, alice, hrac, expiration, deposit=None, ursula=None, kfrag=UNKNOWN_KFRAG, alices_signature=None,
|
|
encrypted_challenge_pack=None):
|
|
"""
|
|
:param deposit: Funds which will pay for the timeframe of this Contract (not the actual re-encryptions);
|
|
a portion will be locked for each Ursula that accepts.
|
|
:param expiration: The moment which Alice wants the Contract to end.
|
|
"""
|
|
self.hrac = hrac
|
|
self.alice = alice
|
|
self.expiration = expiration
|
|
self.deposit = deposit
|
|
self.ursula = ursula
|
|
self.kfrag = kfrag
|
|
self.encrypted_challenge_pack = encrypted_challenge_pack
|
|
|
|
def activate(self, kfrag, ursula, negotiation_result):
|
|
self.kfrag = kfrag
|
|
self.ursula = ursula
|
|
self.negotiation_result = negotiation_result
|
|
|
|
def encrypt_payload_for_ursula(self):
|
|
"""
|
|
Craft an offer to send to Ursula.
|
|
"""
|
|
return self.alice.encrypt_for(self.ursula, self.payload())[0] # We don't need the signature separately.
|
|
|
|
def payload(self):
|
|
# TODO: Ship the expiration again? Or some other way of alerting Ursula to recall her previous dialogue regarding this Contract.
|
|
return bytes(self.kfrag) + b"This might be a ChallengePack" # TODO: come to a decision re: #146
|
|
|
|
def add_details_from_rest_payload(self, group_payload, ursula):
|
|
alice_pubkey_sig, payload_encrypted_for_ursula = group_payload_splitter(group_payload,
|
|
msgpack_remainder=True)
|
|
alice = Alice.from_pubkey_sig_bytes(alice_pubkey_sig)
|
|
ursula.learn_about_actor(alice)
|
|
verified, cleartext = ursula.verify_from(alice, payload_encrypted_for_ursula,
|
|
decrypt=True, signature_is_on_cleartext=True)
|
|
|
|
if not verified:
|
|
# TODO: What do we do if it's not signed properly?
|
|
pass
|
|
|
|
alices_signature, policy_payload = BytestringSplitter(Signature)(cleartext, return_remainder=True)
|
|
|
|
kfrag, encrypted_challenge_pack = policy_payload_splitter(policy_payload, return_remainder=True)
|
|
|
|
# TODO: Query stored Contract and reconstitute
|
|
|
|
self.alice = alice
|
|
self.alices_signature = alices_signature
|
|
self.kfrag = kfrag
|
|
self.encrypted_challenge_pack = encrypted_challenge_pack
|
|
|
|
|
|
|
|
class PolicyOfferResponse(object):
|
|
pass
|
|
|
|
|
|
class PolicyManager(object):
|
|
pass
|
|
|
|
|
|
class PolicyManagerForAlice(PolicyManager):
|
|
def __init__(self, owner: Alice) -> None:
|
|
self.owner = owner
|
|
|
|
def create_policy_group(self,
|
|
bob: Bob,
|
|
uri: bytes,
|
|
m: int,
|
|
n: int,
|
|
):
|
|
"""
|
|
Alice dictates a new group of policies.
|
|
"""
|
|
|
|
##### Temporary until we decide on an API for private key access
|
|
alice_priv_enc = self.owner._crypto_power._power_ups[EncryptingPower].priv_key
|
|
kfrags, pfrag = self.owner.generate_rekey_frags(alice_priv_enc, bob, m,
|
|
n) # TODO: Access Alice's private key inside this method.
|
|
policy = Policy.from_alice(
|
|
alice=self.owner,
|
|
bob=bob,
|
|
kfrags=kfrags,
|
|
pfrag=pfrag,
|
|
uri=uri,
|
|
)
|
|
|
|
return policy
|
|
|
|
|
|
class PolicyGroup(object):
|
|
"""
|
|
The terms and conditions by which Alice shares with Bob.
|
|
"""
|
|
|
|
_id = None
|
|
|
|
def __init__(self, uri: bytes, alice: Alice, bob: Bob, policies=None) -> None:
|
|
self.policies = policies or []
|
|
self.alice = alice
|
|
self.bob = bob
|
|
self.uri = uri
|
|
|
|
|
|
@staticmethod
|
|
def hash(message):
|
|
return keccak_digest(message)
|
|
|
|
# TODO: This is a stand-in; remove it.
|
|
@property
|
|
def pfrag(self):
|
|
return self.policies[0].pfrag
|
|
|
|
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 self.hash(bytes(self.alice.seal) + self.hrac())
|
|
|
|
@property
|
|
def id(self):
|
|
if not self._id:
|
|
self._id = api.keccak_digest(bytes(self.alice.seal), api.keccak_digest(self.uri))
|
|
return self._id
|
|
|
|
def publish_treasure_map(self):
|
|
encrypted_treasure_map, signature_for_bob = self.alice.encrypt_for(self.bob,
|
|
self.treasure_map.packed_payload())
|
|
signature_for_ursula = self.alice.seal(self.hrac()) # TODO: Great use-case for Ciphertext class
|
|
|
|
# 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.
|
|
dht_value = signature_for_ursula + self.alice.seal + self.hrac() + msgpack.dumps(
|
|
encrypted_treasure_map) # TODO: Ideally, this is a Ciphertext object instead of msgpack (see #112)
|
|
dht_key = self.treasure_map_dht_key()
|
|
|
|
setter = self.alice.server.set(dht_key, b"trmap" + dht_value)
|
|
event_loop = asyncio.get_event_loop()
|
|
event_loop.run_until_complete(setter)
|
|
return encrypted_treasure_map, dht_value, signature_for_bob, signature_for_ursula
|
|
|
|
|
|
class Policy(object):
|
|
"""
|
|
An individual agreement between Alice and Ursula. Together, all of the Policies by which
|
|
Ursula nodes which enter into an agreement regarding the same series of kFrags constitute
|
|
a PolicyGroup.
|
|
|
|
A Policy has a unique ID, which includes a fingerprint of Alice's public key so that
|
|
only she can set a policy with that ID. Ursula must verify this; otherwise a collision
|
|
attack is possible.
|
|
"""
|
|
_ursula = None
|
|
hashed_part = None
|
|
|
|
def __init__(self, alice, bob=None, kfrags=(UNKNOWN_KFRAG,), pfrag=None, uri=None, alices_signature=NOT_SIGNED,
|
|
challenge_size=20,
|
|
encrypted_challenge_pack=None):
|
|
"""
|
|
:param kfrag:
|
|
The kFrag obviously, but defaults to UNKNOWN_KFRAG in case the user wants to set it later.
|
|
:param deterministic_id_portion: Probably the fingerprint of Alice's public key.
|
|
Any part that Ursula can use to verify that Alice is the rightful setter of this ID.
|
|
If it's not included, the Policy ID will be completely random.
|
|
:param challenge_size: The number of challenges to create in the ChallengePack.
|
|
"""
|
|
self.alice = alice
|
|
self.bob = bob
|
|
self.alices_signature = alices_signature
|
|
self.kfrags = kfrags
|
|
self.pfrag = pfrag
|
|
self.uri = uri
|
|
self.random_id_portion = api.secure_random(32) # TOOD: Where do we actually want this to live?
|
|
self.challenge_size = challenge_size
|
|
self.treasure_map = TreasureMap()
|
|
self.challenge_pack = []
|
|
self._active_contracts = {}
|
|
|
|
self._encrypted_challenge_pack = encrypted_challenge_pack
|
|
|
|
class MoreContractsThanKFrags(TypeError):
|
|
"""
|
|
Raised when a Policy has been used to generate Contracts 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,
|
|
pfrag,
|
|
alice,
|
|
bob,
|
|
uri,
|
|
):
|
|
policy = Policy(alice, bob, kfrags, pfrag, uri)
|
|
|
|
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 PolicyGroup.hash(bytes(alice.seal) + bytes(bob.seal) + uri)
|
|
|
|
def enact(self, networky_stuff):
|
|
|
|
for kfrag in self.kfrags:
|
|
try:
|
|
contract = self._active_contracts[kfrag]
|
|
except KeyError:
|
|
raise KeyError("This contract isn't marked as active. Can't enact it.")
|
|
policy_payload = contract.encrypt_payload_for_ursula()
|
|
full_payload = self.alice.seal + msgpack.dumps(policy_payload)
|
|
response = networky_stuff.enact_policy(contract.ursula,
|
|
self.hrac(),
|
|
full_payload) # TODO: Parse response for confirmation.
|
|
|
|
# Assuming response is what we hope for
|
|
self.treasure_map.add_ursula(contract.ursula)
|
|
|
|
@property
|
|
def encrypted_challenge_pack(self):
|
|
if not self._encrypted_challenge_pack:
|
|
if not self.bob:
|
|
raise TypeError("This Policy doesn't have a Bob, so there's no way to encrypt a ChallengePack for Bob.")
|
|
else:
|
|
self._encrypted_challenge_pack = self.alice.encrypt_for(self.bob, msgpack.dumps(self.challenge_pack))
|
|
return self._encrypted_challenge_pack
|
|
|
|
def draw_up_contract(self, deposit, expiration):
|
|
return Contract(self.alice, self.hrac(), deposit, expiration)
|
|
|
|
def find_ursulas(self, networky_stuff, deposit, expiration, num_ursulas=None):
|
|
# TODO: This is a number mismatch - we need not one contract, but n contracts.
|
|
"""
|
|
: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:
|
|
contract = self.draw_up_contract(deposit, expiration)
|
|
try:
|
|
ursula, result = networky_stuff.find_ursula(contract)
|
|
found_ursulas.append((ursula, contract, 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_contract(self, contract):
|
|
for kfrag in self.kfrags:
|
|
if not kfrag in self._active_contracts:
|
|
contract.kfrag = kfrag
|
|
self._active_contracts[kfrag] = contract
|
|
return kfrag
|
|
if not contract.kfrag:
|
|
raise self.MoreContractsThanKFrags # TODO: Perhaps in a future version, we consider allowing Alice to assign *the same* KFrag to multiple Ursulas?
|
|
|
|
class TreasureMap(object):
|
|
def __init__(self, ursula_interface_ids=None):
|
|
self.ids = ursula_interface_ids or []
|
|
|
|
def packed_payload(self):
|
|
return msgpack.dumps(self.ids)
|
|
|
|
def add_ursula(self, ursula):
|
|
self.ids.append(ursula.interface_dht_key())
|
|
|
|
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, pfrags, receipt_bytes, receipt_signature, ursula_id=None):
|
|
self.bob = bob
|
|
self.kfrag_hrac = kfrag_hrac
|
|
self.pfrags = pfrags
|
|
self.receipt_bytes = receipt_bytes
|
|
self.receipt_signature = receipt_signature
|
|
self.ursula_id = ursula_id # TODO: We may still need a more elegant system for ID'ing Ursula. See #136.
|
|
|
|
def __repr__(self):
|
|
return "WorkOrder (pfrags: {}) {} for {}".format([binascii.hexlify(bytes(p))[:6] for p in self.pfrags],
|
|
binascii.hexlify(self.receipt_bytes)[:6],
|
|
binascii.hexlify(self.ursula_id)[:6])
|
|
|
|
def __eq__(self, other):
|
|
return (self.receipt_bytes, self.receipt_signature) == (other.receipt_bytes, other.receipt_signature)
|
|
|
|
def __len__(self):
|
|
return len(self.pfrags)
|
|
|
|
@classmethod
|
|
def constructed_by_bob(cls, kfrag_hrac, pfrags, ursula_dht_key, bob):
|
|
receipt_bytes = b"wo:" + ursula_dht_key # TODO: represent the pfrags as bytes and hash them as part of the receipt, ie + keccak_digest(b"".join(pfrags)) - See #137
|
|
receipt_signature = bob.seal(receipt_bytes)
|
|
return cls(bob, kfrag_hrac, pfrags, receipt_bytes, receipt_signature, ursula_dht_key)
|
|
|
|
@classmethod
|
|
def from_rest_payload(cls, kfrag_hrac, rest_payload):
|
|
payload_splitter = BytestringSplitter(Signature, PublicKey)
|
|
signature, bob_pubkey_sig, (receipt_bytes, packed_pfrags) = payload_splitter(rest_payload,
|
|
msgpack_remainder=True)
|
|
pfrags = [PFrag(p) for p in msgpack.loads(packed_pfrags)]
|
|
verified = signature.verify(receipt_bytes, bob_pubkey_sig)
|
|
if not verified:
|
|
raise ValueError("This doesn't appear to be from Bob.")
|
|
bob = Bob.from_pubkey_sig_bytes(bob_pubkey_sig)
|
|
return cls(bob, kfrag_hrac, pfrags, receipt_bytes, signature)
|
|
|
|
def payload(self):
|
|
pfrags_as_bytes = [bytes(p) for p in self.pfrags]
|
|
packed_receipt_and_pfrags = msgpack.dumps((self.receipt_bytes, msgpack.dumps(pfrags_as_bytes)))
|
|
return bytes(self.receipt_signature) + self.bob.seal + packed_receipt_and_pfrags
|
|
|
|
def complete(self, cfrags):
|
|
# TODO: Verify that this is in fact complete - right of CFrags and properly signed.
|
|
# TODO: Mark it complete with datetime.
|
|
self
|