nucypher/nkms/policy/models.py

292 lines
10 KiB
Python

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 HASH_DIGEST_LENGTH, NOT_SIGNED
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
from npre.umbral import RekeyFrag
group_payload_splitter = BytestringSplitter(PublicKey)
policy_payload_splitter = BytestringSplitter((bytes, 66)) # TODO: I wish ReKeyFrag worked with this interface.
class PolicyOffer(object):
"""
An offer from Alice to Ursula to enter into a contract for Re-Encryption services.
"""
def __init__(self, n, deposit, contract_end_datetime):
"""
:param n: The total number of Policies which Alice wishes to create.
:param deposit: Funds which will pay for the timeframe of the contract (not the actual re-encryptions);
a portion will be locked for each Ursula that accepts.
:param contract_end_datetime: The moment which Alice wants the contract to end.
"""
self.n = n
self.deposit = deposit
self.contract_end_datetime = contract_end_datetime
class PolicyOfferResponse(object):
pass
class PolicyManager(object):
pass
class PolicyManagerForAlice(PolicyManager):
def __init__(self, owner: Alice):
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
re_enc_keys, encrypted_key = self.owner.generate_rekey_frags(alice_priv_enc, bob, m,
n) # TODO: Access Alice's private key inside this method.
policies = []
for kfrag_id, rekey in enumerate(re_enc_keys):
policy = Policy.from_alice(
alice=self.owner,
bob=bob,
kfrag=rekey,
)
policies.append(policy)
return PolicyGroup(uri, self.owner, bob, policies)
class PolicyGroup(object):
"""
The terms and conditions by which Alice shares with Bob.
"""
_id = None
def __init__(self, uri: str, alice: Alice, bob: Bob, policies=None):
self.policies = policies or []
self.alice = alice
self.bob = bob
self.uri = uri
self.treasure_map = TreasureMap()
@property
def n(self):
return len(self.policies)
def hash(self, message):
return keccak_digest(message)
def find_n_ursulas(self, networky_stuff, offer: PolicyOffer) -> list:
"""
:param networky_stuff: A compliant interface (maybe a Client instance) to be used to engage the DHT swarm.
:return: A list, with each element containing an Ursula and an OfferResult.
"""
for policy in self.policies:
try:
ursula, result = networky_stuff.find_ursula(self.id, offer)
# TODO: Here, we need to assess the result and see if we're actually good to go.
if result.was_accepted:
policy.activate(ursula, result)
except networky_stuff.NotEnoughQualifiedUrsulas:
pass # Tell Alice to either wait or lower the value of n.
def hrac(self):
"""
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 self.hash(bytes(self.alice.seal) + bytes(self.bob.seal) + self.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 self.hash(bytes(self.alice.seal) + self.hrac())
def enact_policies(self, networky_stuff):
for policy in self.policies:
policy_payload = policy.encrypt_payload_for_ursula()
full_payload = self.alice.seal + msgpack.dumps(policy_payload)
response = networky_stuff.enact_policy(policy.ursula,
self.hrac(),
full_payload) # TODO: Parse response for confirmation.
# Assuming response is what we hope for
self.treasure_map.add_ursula(policy.ursula)
@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
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
_id = None
def __init__(self, alice, bob=None, kfrag=UNKNOWN_KFRAG, alices_signature=NOT_SIGNED, challenge_size=20,
set_id=True, 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.kfrag = kfrag
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 = []
self.challenge_pack = []
self._encrypted_challenge_pack = encrypted_challenge_pack
@property
def id(self):
if self._id:
return self._id
else:
raise RuntimeError("No implemented way to get id yet.")
@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(kfrag,
alice,
bob,
):
policy = Policy(alice, bob, kfrag)
policy.generate_challenge_pack()
return policy
@staticmethod
def from_ursula(group_payload, ursula):
alice_pubkey_sig, (payload_encrypted_for_ursula, sig_bytes) = group_payload_splitter(group_payload,
msgpack_remainder=True)
alice = Alice.from_pubkey_sig_bytes(alice_pubkey_sig)
ursula.learn_about_actor(alice)
alices_signature = Signature(sig_bytes)
verified, policy_payload = ursula.verify_from(alice, alices_signature, 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
kfrag_bytes, encrypted_challenge_pack = policy_payload_splitter(policy_payload, return_remainder=True)
kfrag = RekeyFrag.from_bytes(kfrag_bytes)
policy = Policy(alice=alice, alices_signature=alices_signature, kfrag=kfrag,
encrypted_challenge_pack=encrypted_challenge_pack)
return policy
def payload(self):
return bytes(self.kfrag) + msgpack.dumps(self.encrypted_treasure_map)
def activate(self, ursula, negotiation_result):
self.ursula = ursula
self.negotiation_result = negotiation_result
@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
@encrypted_challenge_pack.setter
def encrypted_treasure_map(self, ecp):
self._encrypted_challenge_pack = ecp
def generate_challenge_pack(self):
if self.kfrag == UNKNOWN_KFRAG:
# TODO: Test this branch
raise TypeError(
"Can't generate a challenge pack unless we know the kfrag. Are you Alice?")
# TODO: make this work instead of being random. See #46.
import random
self._challenge_pack = [(random.getrandbits(32), random.getrandbits(32)) for x in
range(self.challenge_size)]
return True
def encrypt_payload_for_ursula(self):
"""
Craft an offer to send to Ursula.
"""
return self.alice.encrypt_for(self.ursula, self.payload())
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)