mirror of https://github.com/nucypher/nucypher.git
Working stop-propagation for both Ursula interfaces and TreasureMaps.
parent
4d1d899a04
commit
b66eccb251
|
@ -5,7 +5,7 @@ import msgpack
|
|||
from kademlia.network import Server
|
||||
from kademlia.utils import digest
|
||||
from nkms.crypto import api as API
|
||||
from nkms.crypto.api import secure_random
|
||||
from nkms.crypto.api import secure_random, keccak_digest
|
||||
from nkms.crypto.constants import NOT_SIGNED, NO_DECRYPTION_PERFORMED
|
||||
from nkms.crypto.powers import CryptoPower, SigningPower, EncryptingPower
|
||||
from nkms.crypto.utils import verify
|
||||
|
@ -175,6 +175,30 @@ class Alice(Character):
|
|||
alice_privkey, bytes(bob.seal), m, n)
|
||||
return (kfrags, eph_key_data)
|
||||
|
||||
def publish_treasure_map(self, policy_group):
|
||||
encrypted_treasure_map, signature_for_bob = self.encrypt_for(policy_group.bob,
|
||||
policy_group.treasure_map.packed_payload())
|
||||
signature_for_ursula = self.seal(
|
||||
msgpack.dumps(encrypted_treasure_map)) # TODO: Great use-case for Ciphertext class
|
||||
uri_hash = keccak_digest(policy_group.uri)
|
||||
|
||||
# In order to know this is safe to propagate, Ursula needs to see a signature, our public key,
|
||||
# and, reasons explained below, the uri_hash.
|
||||
dht_value = msgpack.dumps((signature_for_ursula, bytes(self.seal), uri_hash, encrypted_treasure_map))
|
||||
|
||||
# This next line is one of the zaniest (but sanest) in the codebase.
|
||||
# We need a key that Bob can glean from knowledge he already has *and* which Ursula can verify came from us.
|
||||
# So, we'll use a hash of:
|
||||
# Our public key (which everybody knows) and
|
||||
# *a hash of* the policy_group's uri, which only we and Bob know.
|
||||
# We'll give Ursula *the hash of* the policy group's uri, but not the URI itself.
|
||||
# So, to reiterate, only Alice and Bob know the URI - so either can make a hash out of it - and this hash
|
||||
# is shared with Ursula.
|
||||
dht_key = keccak_digest(bytes(self.seal) + uri_hash)
|
||||
|
||||
setter = self.server.set(dht_key, b"trmap" + dht_value)
|
||||
return setter, encrypted_treasure_map, dht_value, signature_for_bob, signature_for_ursula
|
||||
|
||||
|
||||
class Bob(Character):
|
||||
_server_class = NuCypherSeedOnlyDHTServer
|
||||
|
@ -204,15 +228,20 @@ class Bob(Character):
|
|||
getter = self.server.get(ursula_interface_id)
|
||||
loop = asyncio.get_event_loop()
|
||||
value = loop.run_until_complete(getter)
|
||||
signature, ursula_pubkey_sig, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
||||
signature, ursula_pubkey_sig, ttl, interface_info = msgpack.loads(value.lstrip(b"uaddr")) # TODO: If we're going to implement TTL, it'll be here.
|
||||
port, interface = msgpack.loads(interface_info)
|
||||
self._ursulas[ursula_interface_id] = Ursula.as_discovered_on_network(port=port, interface=interface, pubkey_sig_bytes=ursula_pubkey_sig)
|
||||
self._ursulas[ursula_interface_id] = Ursula.as_discovered_on_network(port=port, interface=interface,
|
||||
pubkey_sig_bytes=ursula_pubkey_sig)
|
||||
|
||||
def get_treasure_map(self, policy_group, signature):
|
||||
ursula_coro = self.server.get(policy_group.id)
|
||||
|
||||
dht_key = keccak_digest(bytes(self.alice.seal) + keccak_digest(policy_group.uri))
|
||||
|
||||
ursula_coro = self.server.get(dht_key)
|
||||
event_loop = asyncio.get_event_loop()
|
||||
packed_encrypted_treasure_map = event_loop.run_until_complete(ursula_coro)
|
||||
encrypted_treasure_map = msgpack.loads(packed_encrypted_treasure_map)
|
||||
_signature_for_ursula, pubkey_sig_alice, uri_hash, encrypted_treasure_map = msgpack.loads(
|
||||
packed_encrypted_treasure_map[5::])
|
||||
verified, packed_node_list = self.verify_from(self.alice, signature, encrypted_treasure_map,
|
||||
signature_is_on_cleartext=True, decrypt=True)
|
||||
if not verified:
|
||||
|
@ -236,9 +265,6 @@ class Ursula(Character):
|
|||
ursula.interface = interface
|
||||
return ursula
|
||||
|
||||
def ip_dht_key(self):
|
||||
return bytes(self.seal)
|
||||
|
||||
def attach_server(self, ksize=20, alpha=3, id=None, storage=None,
|
||||
*args, **kwargs):
|
||||
|
||||
|
@ -252,17 +278,29 @@ class Ursula(Character):
|
|||
self.interface = interface
|
||||
return self.server.listen(port, interface)
|
||||
|
||||
def interface_info(self):
|
||||
return msgpack.dumps((self.port, self.interface))
|
||||
|
||||
def interface_dht_key(self):
|
||||
return keccak_digest(bytes(self.seal) + bytes(self.interface_ttl()))
|
||||
|
||||
def interface_ttl(self):
|
||||
return 0
|
||||
|
||||
def interface_dht_value(self):
|
||||
signature = self.seal(self.interface_info())
|
||||
ttl = 0 # TODO: We don't actually need this - and it's not currently implemented in a meaningful way,
|
||||
# but it matches the schema for a shared TreasureMap. Maybe we use it to indicate a TTL?
|
||||
return b"uaddr" + msgpack.dumps((signature, bytes(self.seal), ttl, self.interface_info()))
|
||||
|
||||
def publish_interface_information(self):
|
||||
if not self.port and self.interface:
|
||||
raise RuntimeError("Must listen before publishing interface information.")
|
||||
ip_dht_key = self.ip_dht_key()
|
||||
|
||||
interface_info = msgpack.dumps((self.port, self.interface))
|
||||
signature = self.seal(interface_info)
|
||||
|
||||
value = b"uaddr-" + msgpack.dumps([signature, bytes(self.seal), interface_info])
|
||||
setter = self.server.set(key=ip_dht_key, value=value)
|
||||
blockchain_client._ursulas_on_blockchain.append(ip_dht_key)
|
||||
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)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(setter)
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
NOT_SIGNED = 445
|
||||
NO_DECRYPTION_PERFORMED = 455
|
||||
NO_DECRYPTION_PERFORMED = 455
|
||||
|
|
|
@ -5,6 +5,7 @@ class Signature(object):
|
|||
"""
|
||||
The Signature object allows signatures to be made and verified.
|
||||
"""
|
||||
LENGTH = 32
|
||||
|
||||
def __init__(self, v: int=None, r: int=None, s: int=None, sig: bytes=None):
|
||||
"""
|
||||
|
@ -19,9 +20,9 @@ class Signature(object):
|
|||
if sig:
|
||||
v, r, s = API.ecdsa_load_sig(sig)
|
||||
|
||||
self.v = v
|
||||
self.r = r
|
||||
self.s = s
|
||||
self._v = v
|
||||
self._r = r
|
||||
self._s = s
|
||||
|
||||
def verify(self, message: bytes, pubkey: bytes) -> bool:
|
||||
"""
|
||||
|
@ -33,11 +34,11 @@ class Signature(object):
|
|||
:return: True if valid, False if invalid
|
||||
"""
|
||||
msg_digest = API.keccak_digest(message)
|
||||
return API.ecdsa_verify(self.v, self.r, self.s, msg_digest, pubkey)
|
||||
return API.ecdsa_verify(self._v, self._r, self._s, msg_digest, pubkey)
|
||||
|
||||
def __bytes__(self):
|
||||
"""
|
||||
Implements the __bytes__ call for Signature to transform into a
|
||||
transportable mode.
|
||||
"""
|
||||
return API.ecdsa_gen_sig(self.v, self.r, self.s)
|
||||
return API.ecdsa_gen_sig(self._v, self._r, self._s)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from nkms.crypto import api
|
||||
|
||||
|
||||
def verify(signature: bytes, message: bytes, pubkey: bytes) -> bool:
|
||||
msg_digest = api.keccak_digest(message)
|
||||
ecdsa_sig = api.ecdsa_load_sig(signature)
|
||||
|
|
|
@ -4,6 +4,7 @@ from kademlia.node import Node
|
|||
from kademlia.protocol import KademliaProtocol
|
||||
from kademlia.utils import digest
|
||||
from nkms.crypto import utils
|
||||
from nkms.crypto.api import keccak_digest
|
||||
from nkms.network.constants import NODE_HAS_NO_STORAGE
|
||||
from nkms.network.node import NuCypherNode
|
||||
from nkms.network.routing import NuCypherRoutingTable
|
||||
|
@ -38,21 +39,42 @@ class NuCypherHashProtocol(KademliaProtocol):
|
|||
else:
|
||||
return NODE_HAS_NO_STORAGE, False
|
||||
|
||||
def determine_legality_of_dht_key(self, signature, sender_pubkey_sig, message, extra_info, dht_key, dht_value):
|
||||
proper_key = digest(keccak_digest(bytes(sender_pubkey_sig) + bytes(extra_info)))
|
||||
|
||||
try:
|
||||
# Ursula uaddr scenario
|
||||
verified = utils.verify(signature, message, sender_pubkey_sig)
|
||||
except Exception as e:
|
||||
# trmap scenario
|
||||
verified = utils.verify(signature, msgpack.dumps(message), sender_pubkey_sig)
|
||||
|
||||
if not verified or not proper_key == dht_key:
|
||||
self.log.warning("Got request to store illegal k/v: {} / {}".format(dht_key, dht_value))
|
||||
self.illegal_keys_seen.append(dht_key)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def rpc_store(self, sender, nodeid, key, value):
|
||||
source = NuCypherNode(nodeid, sender[0], sender[1])
|
||||
self.welcomeIfNewNode(source)
|
||||
self.log.debug("got a store request from %s" % str(sender))
|
||||
if value.startswith(b"uaddr"):
|
||||
signature, ursula_pubkey_sig, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
||||
proper_key = digest(ursula_pubkey_sig)
|
||||
verified = utils.verify(signature, interface_info, ursula_pubkey_sig)
|
||||
if not verified or not proper_key == key:
|
||||
# TODO: What exactly to do in this scenario?
|
||||
self.log.warning("Possible Vladimir detected - tried to set incorrect Ursula interface key.")
|
||||
self.illegal_keys_seen.append(key)
|
||||
return
|
||||
self.storage[key] = value
|
||||
return True
|
||||
|
||||
if value.startswith(b"uaddr") or value.startswith(b"trmap"):
|
||||
signature, sender_pubkey_sig, extra_info, message = msgpack.loads(value[5::])
|
||||
# extra_info is a hash of the policy_group.id in the case of a treasure map, or a TTL in the case
|
||||
# of an Ursula interface. TODO: Decide whether to keep this notion and, if so, use the TTL.
|
||||
do_store = self.determine_legality_of_dht_key(signature, sender_pubkey_sig, message, extra_info, key, value)
|
||||
else:
|
||||
self.log.info("Got request to store bad k/v: {} / {}".format(key, value))
|
||||
do_store = False
|
||||
|
||||
if do_store:
|
||||
self.log.info("Storing k/v: {} / {}".format(key, value))
|
||||
self.storage[key] = value
|
||||
|
||||
return do_store
|
||||
|
||||
|
||||
class NuCypherSeedOnlyProtocol(NuCypherHashProtocol):
|
||||
|
|
|
@ -57,7 +57,7 @@ class PolicyManagerForAlice(PolicyManager):
|
|||
)
|
||||
policies.append(policy)
|
||||
|
||||
return PolicyGroup(uri, bob, policies)
|
||||
return PolicyGroup(uri, self.owner, bob, policies)
|
||||
|
||||
|
||||
class PolicyGroup(object):
|
||||
|
@ -67,8 +67,9 @@ class PolicyGroup(object):
|
|||
|
||||
_id = None
|
||||
|
||||
def __init__(self, uri: str, bob: Bob, policies=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()
|
||||
|
@ -105,7 +106,7 @@ class PolicyGroup(object):
|
|||
@property
|
||||
def id(self):
|
||||
if not self._id:
|
||||
self._id = api.keccak_digest(self.uri, bytes(self.bob.seal))
|
||||
self._id = api.keccak_digest(bytes(self.alice.seal), api.keccak_digest(self.uri))
|
||||
return self._id
|
||||
|
||||
|
||||
|
@ -223,7 +224,7 @@ class TreasureMap(object):
|
|||
return msgpack.dumps(self.ids)
|
||||
|
||||
def add_ursula(self, ursula):
|
||||
self.ids.append(ursula.ip_dht_key())
|
||||
self.ids.append(ursula.interface_dht_key())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.ids == other.ids
|
||||
|
|
|
@ -59,7 +59,7 @@ def test_vladimir_illegal_interface_key_does_not_propagate():
|
|||
# Vladimir does almost everything right....
|
||||
interface_info = msgpack.dumps((vladimir.port, vladimir.interface))
|
||||
signature = vladimir.seal(interface_info)
|
||||
value = b"uaddr-" + msgpack.dumps([signature, bytes(vladimir.seal), interface_info])
|
||||
value = b"uaddr" + msgpack.dumps([signature, bytes(vladimir.seal), 0, interface_info])
|
||||
|
||||
# Except he sets an illegal key for his interface.
|
||||
illegal_key = "Not allowed to set arbitrary key for this."
|
||||
|
@ -111,7 +111,7 @@ def test_alice_finds_ursula():
|
|||
getter = ALICE.server.get(all_ursulas[ursula_index])
|
||||
loop = asyncio.get_event_loop()
|
||||
value = loop.run_until_complete(getter)
|
||||
signature, ursula_pubkey_sig, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
||||
signature, ursula_pubkey_sig, ttl, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
||||
port, interface = msgpack.loads(interface_info)
|
||||
assert port == URSULA_PORT + ursula_index
|
||||
|
||||
|
@ -152,17 +152,14 @@ def test_alice_sets_treasure_map_on_network():
|
|||
"""
|
||||
policy_group = test_alice_has_ursulas_public_key_and_uses_it_to_encode_policy_payload()
|
||||
|
||||
treasure_map = policy_group.treasure_map
|
||||
|
||||
encrypted_treasure_map, signature = ALICE.encrypt_for(BOB, treasure_map.packed_payload())
|
||||
packed_encrypted_treasure_map = msgpack.dumps(encrypted_treasure_map)
|
||||
|
||||
setter = ALICE.server.set(policy_group.id, packed_encrypted_treasure_map)
|
||||
setter, encrypted_treasure_map, packed_encrypted_treasure_map, signature_for_bob, signature_for_ursula = ALICE.publish_treasure_map(
|
||||
policy_group)
|
||||
_set_event = EVENT_LOOP.run_until_complete(setter)
|
||||
|
||||
treasure_map_as_set_on_network = URSULAS[0].server.storage[digest(policy_group.id)]
|
||||
assert treasure_map_as_set_on_network == packed_encrypted_treasure_map # IE, Ursula stores it properly.
|
||||
return treasure_map, treasure_map_as_set_on_network, signature, policy_group
|
||||
treasure_map_as_set_on_network = URSULAS[0].server.storage[
|
||||
digest(policy_group.id)]
|
||||
assert treasure_map_as_set_on_network == b"trmap" + packed_encrypted_treasure_map
|
||||
return treasure_map_as_set_on_network, signature_for_bob, policy_group
|
||||
|
||||
|
||||
def test_treasure_map_with_bad_id_does_not_propagate():
|
||||
|
@ -188,14 +185,15 @@ def test_treasure_map_stored_by_ursula_is_the_correct_one_for_bob():
|
|||
"""
|
||||
The TreasureMap given by Alice to Ursula is the correct one for Bob; he can decrypt and read it.
|
||||
"""
|
||||
treasure_map, treasure_map_as_set_on_network, signature, _ = test_alice_sets_treasure_map_on_network()
|
||||
encrypted_treasure_map = msgpack.loads(treasure_map_as_set_on_network)
|
||||
treasure_map_as_set_on_network, signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
_signature_for_ursula, pubkey_sig_alice, uri_hash, encrypted_treasure_map = msgpack.loads(
|
||||
treasure_map_as_set_on_network[5::]) # 5:: to account for prepended "trmap"
|
||||
verified, treasure_map_as_decrypted_by_bob = BOB.verify_from(ALICE, signature,
|
||||
encrypted_treasure_map,
|
||||
decrypt=True,
|
||||
signature_is_on_cleartext=True,
|
||||
)
|
||||
assert treasure_map_as_decrypted_by_bob == treasure_map.packed_payload()
|
||||
assert treasure_map_as_decrypted_by_bob == policy_group.treasure_map.packed_payload()
|
||||
assert verified is True
|
||||
|
||||
|
||||
|
@ -204,25 +202,26 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it():
|
|||
Above, we showed that the TreasureMap saved on the network is the correct one for Bob. Here, we show
|
||||
that Bob can retrieve it with only the information about which he is privy pursuant to the PolicyGroup.
|
||||
"""
|
||||
treasure_map, treasure_map_as_set_on_network, signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
treasure_map_as_set_on_network, signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
networky_stuff = MockNetworkyStuff(URSULAS)
|
||||
|
||||
# Of course, in the real world, Bob has sufficient information to reconstitute a PolicyGroup, gleaned, we presume, through a side-channel with Alice.
|
||||
# Of course, in the real world, Bob has sufficient information to reconstitute a PolicyGroup, gleaned, we presume,
|
||||
# through a side-channel with Alice.
|
||||
treasure_map_from_wire = BOB.get_treasure_map(policy_group, signature)
|
||||
|
||||
assert treasure_map == treasure_map_from_wire
|
||||
assert policy_group.treasure_map == treasure_map_from_wire
|
||||
|
||||
|
||||
def test_treaure_map_is_legit():
|
||||
"""
|
||||
Sure, the TreasureMap can get to Bob, but we also need to know that each Ursula in the TreasureMap is on the network.
|
||||
"""
|
||||
treasure_map, treasure_map_as_set_on_network, signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
treasure_map_as_set_on_network, signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
|
||||
for ursula_interface_id in treasure_map:
|
||||
for ursula_interface_id in policy_group.treasure_map:
|
||||
getter = ALICE.server.get(ursula_interface_id)
|
||||
loop = asyncio.get_event_loop()
|
||||
value = loop.run_until_complete(getter)
|
||||
signature, ursula_pubkey_sig, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
||||
signature, ursula_pubkey_sig, ttl, interface_info = msgpack.loads(value.lstrip(b"uaddr-"))
|
||||
port, _interface = msgpack.loads(interface_info)
|
||||
assert port in URSULA_PORTS
|
||||
|
|
|
@ -95,33 +95,3 @@ def test_full_node_does_not_try_to_store_on_seed_only_node():
|
|||
seed_only_server.stop()
|
||||
full_server.stop()
|
||||
event_loop.close()
|
||||
|
||||
|
||||
def test_seed_only_node_knows_it_can_store_on_full_node():
|
||||
"""
|
||||
On the other hand, a seed-only node knows that it can store on a full node.
|
||||
"""
|
||||
|
||||
event_loop = asyncio.get_event_loop()
|
||||
|
||||
full_server = NuCypherDHTServer()
|
||||
full_server.listen(8468)
|
||||
event_loop.run_until_complete(full_server.bootstrap([("127.0.0.1", 8468)]))
|
||||
|
||||
seed_only_server = NuCypherSeedOnlyDHTServer()
|
||||
seed_only_server.listen(8471)
|
||||
event_loop.run_until_complete(seed_only_server.bootstrap([("127.0.0.1", 8468)]))
|
||||
|
||||
# The seed-only will try to store a value.
|
||||
key_to_store = b"llamas"
|
||||
value_to_store = b"tons_of_things_keyed_llamas"
|
||||
setter = seed_only_server.set(key_to_store, value_to_store)
|
||||
|
||||
# But watch - unlike before, this node knows it can set values.
|
||||
result = event_loop.run_until_complete(setter)
|
||||
assert result
|
||||
assert seed_only_server.digests_set == 1
|
||||
|
||||
# annnnd stop.
|
||||
seed_only_server.stop()
|
||||
full_server.stop()
|
||||
|
|
|
@ -3,9 +3,9 @@ from tests.network.test_network_actors import test_alice_sets_treasure_map_on_ne
|
|||
|
||||
def test_bob_can_follow_treasure_map():
|
||||
"""
|
||||
Upon receiving a
|
||||
Upon receiving a TreasureMap, Bob populates his list of Ursulas with the correct number.
|
||||
"""
|
||||
assert len(BOB._ursulas) == 0
|
||||
treasure_map, treasure_map_as_set_on_network, signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
BOB.follow_treasure_map(treasure_map)
|
||||
_treasure_map_as_set_on_network, _signature, policy_group = test_alice_sets_treasure_map_on_network()
|
||||
BOB.follow_treasure_map(policy_group.treasure_map)
|
||||
assert len(BOB._ursulas) == len(URSULAS)
|
||||
|
|
|
@ -51,4 +51,4 @@ class MockNetworkyStuff(NetworkyStuff):
|
|||
return super().find_ursula(id)
|
||||
|
||||
def animate_policy(self, ursula, payload):
|
||||
return
|
||||
return True, ursula.interface_dht_key()
|
||||
|
|
Loading…
Reference in New Issue