Working stop-propagation for both Ursula interfaces and TreasureMaps.

pull/110/head
jMyles 2017-11-10 02:04:01 -08:00
parent 4d1d899a04
commit b66eccb251
10 changed files with 122 additions and 90 deletions

View File

@ -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)

View File

@ -1,2 +1,2 @@
NOT_SIGNED = 445
NO_DECRYPTION_PERFORMED = 455
NO_DECRYPTION_PERFORMED = 455

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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()