mirror of https://github.com/nucypher/nucypher.git
Merge pull request #366 from jMyles/learning-loop
Learning Loop Part IV and Working n==3 Federated Demo.pull/370/merge
commit
ecce0963da
|
@ -19,4 +19,8 @@ _temp_test_datastore
|
|||
chains
|
||||
*logs*
|
||||
*.ipynb*
|
||||
.ethash
|
||||
.ethash
|
||||
examples/examples-runtime-cruft/*
|
||||
examples/finnegans-wake.txt
|
||||
mypy_reports/
|
||||
reports/
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
# WIP w/ hendrix@3.0.0
|
||||
|
||||
import binascii
|
||||
import datetime
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import maya
|
||||
from sandbox_resources import SandboxRestMiddleware
|
||||
|
||||
from nucypher.characters import Alice, Bob, Ursula
|
||||
from nucypher.data_sources import DataSource
|
||||
|
@ -15,29 +16,43 @@ from nucypher.data_sources import DataSource
|
|||
from nucypher.network.middleware import RestMiddleware
|
||||
from umbral.keys import UmbralPublicKey
|
||||
|
||||
URSULA = Ursula.from_rest_url(network_middleware=RestMiddleware(),
|
||||
host="localhost",
|
||||
port=3601,
|
||||
federated_only=True)
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.DEBUG)
|
||||
|
||||
network_middleware = SandboxRestMiddleware([URSULA])
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
ch.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
root.addHandler(ch)
|
||||
|
||||
teacher_dht_port = 3552
|
||||
teacher_rest_port = 3652
|
||||
with open("examples-runtime-cruft/node-metadata-{}".format(teacher_rest_port), "r") as f:
|
||||
f.seek(0)
|
||||
teacher_bytes = binascii.unhexlify(f.read())
|
||||
URSULA = Ursula.from_bytes(teacher_bytes, federated_only=True)
|
||||
print("Will learn from {}".format(URSULA))
|
||||
|
||||
# network_middleware = SandboxRestMiddleware([URSULA])
|
||||
|
||||
#########
|
||||
# Alice #
|
||||
#########
|
||||
|
||||
ALICE = Alice(network_middleware=network_middleware,
|
||||
ALICE = Alice(network_middleware=RestMiddleware(),
|
||||
known_nodes=(URSULA,), # in lieu of seed nodes
|
||||
federated_only=True) # TODO: 289
|
||||
federated_only=True,
|
||||
always_be_learning=True) # TODO: 289
|
||||
|
||||
# Here are our Policy details.
|
||||
policy_end_datetime = maya.now() + datetime.timedelta(days=5)
|
||||
m = 1
|
||||
n = 1
|
||||
m = 2
|
||||
n = 3
|
||||
label = b"secret/files/and/stuff"
|
||||
|
||||
# Alice grants to Bob.
|
||||
BOB = Bob(known_nodes=(URSULA,), federated_only=True)
|
||||
BOB = Bob(known_nodes=(URSULA,), federated_only=True, always_be_learning=True)
|
||||
ALICE.start_learning_loop(now=True)
|
||||
policy = ALICE.grant(BOB, label, m=m, n=n,
|
||||
expiration=policy_end_datetime)
|
||||
|
||||
|
@ -52,15 +67,16 @@ del ALICE
|
|||
#####################
|
||||
# some time passes. #
|
||||
# ... #
|
||||
# #
|
||||
# ... #
|
||||
# And now for Bob. #
|
||||
#####################
|
||||
|
||||
# Bob wants to join the policy so that he can receive any future
|
||||
# data shared on it.
|
||||
# He needs a few piece of knowledge to do that.
|
||||
# He needs a few pieces of knowledge to do that.
|
||||
BOB.join_policy(label, # The label - he needs to know what data he's after.
|
||||
alices_pubkey_bytes_saved_for_posterity, # To verify the signature, he'll need Alice's public key.
|
||||
verify_sig=True, # And yes, he usually wants to verify that signature.
|
||||
# He can also bootstrap himself onto the network more quickly
|
||||
# by providing a list of known nodes at this time.
|
||||
node_list=[("localhost", 3601)]
|
||||
|
@ -136,8 +152,8 @@ for counter, plaintext in enumerate(finnegans_wake):
|
|||
# and the DataSource which produced it.
|
||||
alice_pubkey_restored_from_ancient_scroll = UmbralPublicKey.from_bytes(alices_pubkey_bytes_saved_for_posterity)
|
||||
delivered_cleartexts = BOB.retrieve(message_kit=message_kit,
|
||||
data_source=datasource_as_understood_by_bob,
|
||||
alice_verifying_key=alice_pubkey_restored_from_ancient_scroll)
|
||||
data_source=datasource_as_understood_by_bob,
|
||||
alice_verifying_key=alice_pubkey_restored_from_ancient_scroll)
|
||||
|
||||
# We show that indeed this is the passage originally encrypted by the DataSource.
|
||||
assert plaintext == delivered_cleartexts[0]
|
||||
|
|
|
@ -5,40 +5,69 @@
|
|||
|
||||
# WIP w/ hendrix@tags/3.3.0rc1
|
||||
|
||||
import os
|
||||
import os, sys
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
|
||||
from hendrix.deploy.tls import HendrixDeployTLS
|
||||
from hendrix.facilities.services import ExistingKeyTLSContextFactory
|
||||
from nucypher.characters import Ursula
|
||||
from OpenSSL.crypto import X509
|
||||
from OpenSSL.SSL import TLSv1_2_METHOD
|
||||
|
||||
from nucypher.crypto.api import generate_self_signed_certificate
|
||||
|
||||
DB_NAME = "non-mining-proxy-node"
|
||||
DB_NAME = "examples-runtime-cruft/db"
|
||||
STARTING_PORT = 3501
|
||||
|
||||
_URSULA = Ursula(dht_port=3501,
|
||||
rest_port=3601,
|
||||
rest_host="localhost",
|
||||
dht_host="localhost",
|
||||
db_name=DB_NAME,
|
||||
federated_only=True)
|
||||
_URSULA.dht_listen()
|
||||
|
||||
CURVE = ec.SECP256R1
|
||||
cert, private_key = generate_self_signed_certificate(_URSULA.stamp.fingerprint().decode(), CURVE)
|
||||
import logging, binascii
|
||||
import sys
|
||||
|
||||
deployer = HendrixDeployTLS("start",
|
||||
{"wsgi":_URSULA.rest_app, "https_port": _URSULA.rest_interface.port},
|
||||
key=private_key,
|
||||
cert=X509.from_cryptography(cert),
|
||||
context_factory=ExistingKeyTLSContextFactory,
|
||||
context_factory_kwargs={"curve_name": "prime256v1",
|
||||
"sslmethod": TLSv1_2_METHOD})
|
||||
root = logging.getLogger()
|
||||
root.setLevel(logging.DEBUG)
|
||||
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
ch.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
root.addHandler(ch)
|
||||
|
||||
|
||||
def spin_up_ursula(dht_port, rest_port, db_name, teachers=()):
|
||||
metadata_file = "examples-runtime-cruft/node-metadata-{}".format(rest_port)
|
||||
|
||||
asyncio.set_event_loop(asyncio.new_event_loop()) # Ugh. Awful. But needed until we shed the DHT.
|
||||
_URSULA = Ursula(dht_port=dht_port,
|
||||
rest_port=rest_port,
|
||||
rest_host="localhost",
|
||||
dht_host="localhost",
|
||||
db_name=db_name,
|
||||
federated_only=True,
|
||||
known_nodes=teachers,
|
||||
)
|
||||
_URSULA.dht_listen()
|
||||
try:
|
||||
with open(metadata_file, "w") as f:
|
||||
f.write(bytes(_URSULA).hex())
|
||||
_URSULA.start_learning_loop()
|
||||
_URSULA.get_deployer().run()
|
||||
finally:
|
||||
os.remove(db_name)
|
||||
os.remove(metadata_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
teacher_dht_port = sys.argv[2]
|
||||
teacher_rest_port = int(teacher_dht_port) + 100
|
||||
with open("examples-runtime-cruft/node-metadata-{}".format(teacher_rest_port), "r") as f:
|
||||
f.seek(0)
|
||||
teacher_bytes = binascii.unhexlify(f.read())
|
||||
teacher = Ursula.from_bytes(teacher_bytes, federated_only=True)
|
||||
teachers = (teacher, )
|
||||
print("Will learn from {}".format(teacher))
|
||||
except (IndexError, FileNotFoundError):
|
||||
teachers = ()
|
||||
|
||||
dht_port = sys.argv[1]
|
||||
rest_port = int(dht_port) + 100
|
||||
db_name = DB_NAME + str(rest_port)
|
||||
spin_up_ursula(dht_port, rest_port, db_name, teachers=teachers)
|
||||
|
||||
try:
|
||||
deployer.run()
|
||||
finally:
|
||||
os.remove(DB_NAME)
|
||||
|
|
|
@ -12,9 +12,9 @@ import kademlia
|
|||
import maya
|
||||
import time
|
||||
from eth_keys import KeyAPI as EthKeyAPI
|
||||
from eth_keys.datatypes import Signature
|
||||
from kademlia.network import Server
|
||||
from kademlia.utils import digest
|
||||
from twisted.internet import task, threads
|
||||
|
||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||
from constant_sorrow import constants, default_constant_splitter
|
||||
|
@ -32,7 +32,6 @@ from nucypher.network.middleware import RestMiddleware
|
|||
from nucypher.network.nodes import VerifiableNode
|
||||
from nucypher.network.protocols import InterfaceInfo
|
||||
from nucypher.network.server import NucypherDHTServer, NucypherSeedOnlyDHTServer, ProxyRESTServer
|
||||
from twisted.internet import task, threads
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
|
||||
|
@ -47,7 +46,9 @@ class Character:
|
|||
_default_crypto_powerups = None
|
||||
_stamp = None
|
||||
|
||||
_SECONDS_DELAY_BETWEEN_LEARNING = 2
|
||||
_SHORT_LEARNING_DELAY = 5
|
||||
_LONG_LEARNING_DELAY = 90
|
||||
_ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN = 10
|
||||
|
||||
from nucypher.network.protocols import SuspiciousActivity # Ship this exception with every Character.
|
||||
|
||||
|
@ -71,7 +72,7 @@ class Character:
|
|||
checksum_address: bytes = None,
|
||||
always_be_learning=False,
|
||||
start_learning_on_same_thread=False,
|
||||
known_nodes: Set = (),
|
||||
known_nodes: tuple = (),
|
||||
abort_on_learning_error: bool = False,
|
||||
):
|
||||
"""
|
||||
|
@ -136,6 +137,7 @@ class Character:
|
|||
self._current_teacher_node = None
|
||||
self._learning_task = task.LoopingCall(self.keep_learning_about_nodes)
|
||||
self._learning_round = 0
|
||||
self._rounds_without_new_nodes = 0
|
||||
|
||||
if always_be_learning:
|
||||
self.start_learning_loop(now=start_learning_on_same_thread)
|
||||
|
@ -234,8 +236,8 @@ class Character:
|
|||
|
||||
def remember_node(self, node):
|
||||
# TODO: 334
|
||||
listeners = self._learning_listeners.pop(node.canonical_public_address, ())
|
||||
address = node.canonical_public_address
|
||||
listeners = self._learning_listeners.pop(node.checksum_public_address, ())
|
||||
address = node.checksum_public_address
|
||||
|
||||
self._known_nodes[address] = node
|
||||
self.log.info("Remembering {}, popping {} listeners.".format(node.checksum_public_address, len(listeners)))
|
||||
|
@ -247,7 +249,7 @@ class Character:
|
|||
if self._learning_task.running:
|
||||
return False
|
||||
else:
|
||||
d = self._learning_task.start(interval=self._SECONDS_DELAY_BETWEEN_LEARNING, now=now)
|
||||
d = self._learning_task.start(interval=self._SHORT_LEARNING_DELAY, now=now)
|
||||
d.addErrback(self.handle_learning_errors)
|
||||
return d
|
||||
|
||||
|
@ -300,7 +302,7 @@ class Character:
|
|||
"Learning loop isn't started; can't learn about nodes now. You can ovverride this with force=True.")
|
||||
elif force:
|
||||
self.log.info("Learning loop wasn't started; forcing start now.")
|
||||
self._learning_task.start(self._SECONDS_DELAY_BETWEEN_LEARNING, now=True)
|
||||
self._learning_task.start(self._SHORT_LEARNING_DELAY, now=True)
|
||||
|
||||
def keep_learning_about_nodes(self):
|
||||
"""
|
||||
|
@ -312,25 +314,58 @@ class Character:
|
|||
self._node_ids_to_learn_about_immediately.update(canonical_addresses) # hmmmm
|
||||
self.learn_about_nodes_now()
|
||||
|
||||
def block_until_nodes_are_known(self, canonical_addresses: Set, timeout=10, allow_missing=0,
|
||||
learn_on_this_thread=False):
|
||||
# TODO: Dehydrate these next two methods.
|
||||
|
||||
def block_until_number_of_known_nodes_is(self, number_of_nodes_to_know: int,
|
||||
timeout=10,
|
||||
learn_on_this_thread=False):
|
||||
start = maya.now()
|
||||
starting_round = self._learning_round
|
||||
|
||||
while True:
|
||||
rounds_undertaken = self._learning_round - starting_round
|
||||
if len(self._known_nodes) >= number_of_nodes_to_know:
|
||||
if rounds_undertaken:
|
||||
self.log.info("Learned about enough nodes after {} rounds.".format(rounds_undertaken))
|
||||
return True
|
||||
|
||||
if not self._learning_task.running:
|
||||
self.log.warning("Blocking to learn about nodes, but learning loop isn't running.")
|
||||
if learn_on_this_thread:
|
||||
self.learn_from_teacher_node(eager=True)
|
||||
rounds_undertaken = self._learning_round - starting_round
|
||||
if (maya.now() - start).seconds < timeout:
|
||||
if canonical_addresses.issubset(self._known_nodes):
|
||||
|
||||
self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken))
|
||||
return True
|
||||
if (maya.now() - start).seconds > timeout:
|
||||
if not self._learning_task.running:
|
||||
raise self.NotEnoughUrsulas(
|
||||
"We didn't discover any nodes because the learning loop isn't running. Start it with start_learning().")
|
||||
else:
|
||||
time.sleep(.1)
|
||||
raise self.NotEnoughUrsulas("After {} seconds and {} rounds, didn't find {} nodes".format(
|
||||
timeout, rounds_undertaken, number_of_nodes_to_know))
|
||||
else:
|
||||
time.sleep(.1)
|
||||
|
||||
def block_until_specific_nodes_are_known(self,
|
||||
canonical_addresses: Set,
|
||||
timeout=10,
|
||||
allow_missing=0,
|
||||
learn_on_this_thread=False):
|
||||
start = maya.now()
|
||||
starting_round = self._learning_round
|
||||
|
||||
while True:
|
||||
rounds_undertaken = self._learning_round - starting_round
|
||||
if canonical_addresses.issubset(self._known_nodes):
|
||||
if rounds_undertaken:
|
||||
self.log.info("Learned about all nodes after {} rounds.".format(rounds_undertaken))
|
||||
return True
|
||||
|
||||
if not self._learning_task.running:
|
||||
self.log.warning("Blocking to learn about nodes, but learning loop isn't running.")
|
||||
if learn_on_this_thread:
|
||||
self.learn_from_teacher_node(eager=True)
|
||||
|
||||
if (maya.now() - start).seconds > timeout:
|
||||
|
||||
still_unknown = canonical_addresses.difference(self._known_nodes)
|
||||
|
||||
if len(still_unknown) <= allow_missing:
|
||||
|
@ -342,31 +377,46 @@ class Character:
|
|||
raise self.NotEnoughUrsulas("After {} seconds and {} rounds, didn't find these {} nodes: {}".format(
|
||||
timeout, rounds_undertaken, len(still_unknown), still_unknown))
|
||||
|
||||
else:
|
||||
time.sleep(.1)
|
||||
|
||||
def learn_from_teacher_node(self, eager=True):
|
||||
"""
|
||||
Sends a request to node_url to find out about known nodes.
|
||||
"""
|
||||
self._learning_round += 1
|
||||
|
||||
current_teacher = self.current_teacher_node()
|
||||
try:
|
||||
current_teacher = self.current_teacher_node()
|
||||
except self.NotEnoughUrsulas as e:
|
||||
self.log.warning("Can't learn right now: {}".format(e.args[0]))
|
||||
return
|
||||
|
||||
rest_address = current_teacher.rest_interface.host
|
||||
port = current_teacher.rest_interface.port
|
||||
|
||||
# TODO: Do we really want to try to learn about all these nodes instantly? Hearing this traffic might give insight to an attacker.
|
||||
response = self.network_middleware.get_nodes_via_rest(rest_address,
|
||||
port, node_ids=self._node_ids_to_learn_about_immediately)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError
|
||||
signature, nodes = signature_splitter(response.content, return_remainder=True)
|
||||
node_list = Ursula.batch_from_bytes(nodes, federated_only=self.federated_only) # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node.
|
||||
if VerifiableNode in self.__class__.__bases__:
|
||||
announce_nodes = [self]
|
||||
else:
|
||||
announce_nodes = None
|
||||
|
||||
self.log.info("Learning round {}. Teacher: {} knew about {} nodes.".format(self._learning_round,
|
||||
current_teacher.checksum_public_address,
|
||||
len(node_list)))
|
||||
response = self.network_middleware.get_nodes_via_rest(rest_address,
|
||||
port,
|
||||
nodes_i_need=self._node_ids_to_learn_about_immediately,
|
||||
announce_nodes=announce_nodes)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError("Bad response from teacher: {} - {}".format(response, response.content
|
||||
))
|
||||
signature, nodes = signature_splitter(response.content, return_remainder=True)
|
||||
node_list = Ursula.batch_from_bytes(nodes,
|
||||
federated_only=self.federated_only) # TODO: This doesn't make sense - a decentralized node can still learn about a federated-only node.
|
||||
|
||||
new_nodes = []
|
||||
|
||||
for node in node_list:
|
||||
|
||||
if node.checksum_public_address in self._known_nodes:
|
||||
if node.checksum_public_address in self._known_nodes or node.checksum_public_address == self.checksum_public_address:
|
||||
continue # TODO: 168 Check version and update if required.
|
||||
|
||||
try:
|
||||
|
@ -380,10 +430,37 @@ class Character:
|
|||
"Propagated by: {}:{}".format(current_teacher.checksum_public_address,
|
||||
rest_address, port)
|
||||
self.log.warning(message)
|
||||
|
||||
self.log.info("Prevously unknown node: {}".format(node.checksum_public_address))
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
self.remember_node(node)
|
||||
new_nodes.append(node)
|
||||
|
||||
self._adjust_learning(new_nodes)
|
||||
|
||||
self.log.info("Learning round {}. Teacher: {} knew about {} nodes, {} were new.".format(self._learning_round,
|
||||
current_teacher.checksum_public_address,
|
||||
len(node_list),
|
||||
len(new_nodes)),
|
||||
)
|
||||
|
||||
def _adjust_learning(self, node_list):
|
||||
"""
|
||||
Takes a list of new nodes, adjusts learning accordingly.
|
||||
|
||||
Currently, simply slows down learning loop when no new nodes have been discovered in a while.
|
||||
TODO: Do other important things - scrub, bucket, etc.
|
||||
"""
|
||||
if node_list:
|
||||
self._rounds_without_new_nodes = 0
|
||||
self._learning_task.interval = self._SHORT_LEARNING_DELAY
|
||||
else:
|
||||
self._rounds_without_new_nodes += 1
|
||||
if self._rounds_without_new_nodes > self._ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN:
|
||||
self.log.info("After {} rounds with no new nodes, it's time to slow down to {} seconds.".format(
|
||||
self._ROUNDS_WITHOUT_NODES_AFTER_WHICH_TO_SLOW_DOWN,
|
||||
self._LONG_LEARNING_DELAY))
|
||||
self._learning_task.interval = self._LONG_LEARNING_DELAY
|
||||
|
||||
def _push_certain_newly_discovered_nodes_here(self, queue_to_push, node_addresses):
|
||||
"""
|
||||
|
@ -500,13 +577,14 @@ class Character:
|
|||
if signature_to_use:
|
||||
is_valid = signature_to_use.verify(message, sender_pubkey_sig)
|
||||
if not is_valid:
|
||||
raise mystery_stranger.InvalidSignature("Signature for message isn't valid: {}".format(signature_to_use))
|
||||
raise mystery_stranger.InvalidSignature(
|
||||
"Signature for message isn't valid: {}".format(signature_to_use))
|
||||
else:
|
||||
raise self.InvalidSignature("No signature provided -- signature presumed invalid.")
|
||||
return cleartext
|
||||
|
||||
"""
|
||||
Next we have decrypt(), sign(), and generate_self_signed_certificate() - these use the private
|
||||
Next we have decrypt() and sign() - these use the private
|
||||
keys of their respective powers; any character who has these powers can use these functions.
|
||||
|
||||
If they don't have the correct Power, the appropriate PowerUpError is raised.
|
||||
|
@ -518,10 +596,6 @@ class Character:
|
|||
def sign(self, message):
|
||||
return self._crypto_power.power_ups(SigningPower).sign(message)
|
||||
|
||||
def generate_self_signed_certificate(self):
|
||||
signing_power = self._crypto_power.power_ups(SigningPower)
|
||||
return signing_power.generate_self_signed_cert(self.stamp.fingerprint().decode())
|
||||
|
||||
"""
|
||||
And finally, some miscellaneous but generally-applicable abilities:
|
||||
"""
|
||||
|
@ -633,7 +707,7 @@ class Alice(Character, PolicyAuthor):
|
|||
|
||||
return policy
|
||||
|
||||
def grant(self, bob, uri, m=None, n=None, expiration=None, deposit=None, ursulas=None):
|
||||
def grant(self, bob, uri, m=None, n=None, expiration=None, deposit=None, handpicked_ursulas=None):
|
||||
if not m:
|
||||
# TODO: get m from config #176
|
||||
raise NotImplementedError
|
||||
|
@ -649,6 +723,8 @@ class Alice(Character, PolicyAuthor):
|
|||
deposit = self.network_middleware.get_competitive_rate()
|
||||
if deposit == NotImplemented:
|
||||
deposit = constants.NON_PAYMENT(b"0000000")
|
||||
if handpicked_ursulas is None:
|
||||
handpicked_ursulas = set()
|
||||
|
||||
policy = self.create_policy(bob, uri, m, n)
|
||||
|
||||
|
@ -658,10 +734,26 @@ class Alice(Character, PolicyAuthor):
|
|||
# Users may decide to inject some market strategies here.
|
||||
#
|
||||
# TODO: 289
|
||||
|
||||
# If we're federated only, we need to block to make sure we have enough nodes.
|
||||
if self.federated_only and len(self._known_nodes) < n:
|
||||
good_to_go = self.block_until_number_of_known_nodes_is(n, learn_on_this_thread=True)
|
||||
if not good_to_go:
|
||||
raise ValueError(
|
||||
"To make a Policy in federated mode, you need to know about\
|
||||
all the Ursulas you need (in this case, {}); there's no other way to\
|
||||
know which nodes to use. Either pass them here or when you make\
|
||||
the Policy, or run the learning loop on a network with enough Ursulas.".format(self.n))
|
||||
|
||||
if len(handpicked_ursulas) < n:
|
||||
number_of_ursulas_needed = n - len(handpicked_ursulas)
|
||||
new_ursulas = random.sample(list(self._known_nodes.values()), number_of_ursulas_needed)
|
||||
handpicked_ursulas.update(new_ursulas)
|
||||
|
||||
policy.make_arrangements(network_middleware=self.network_middleware,
|
||||
deposit=deposit,
|
||||
expiration=expiration,
|
||||
ursulas=ursulas,
|
||||
handpicked_ursulas=handpicked_ursulas,
|
||||
)
|
||||
|
||||
# REST call happens here, as does population of TreasureMap.
|
||||
|
@ -746,14 +838,14 @@ class Bob(Character):
|
|||
|
||||
if block:
|
||||
if new_thread:
|
||||
return threads.deferToThread(self.block_until_nodes_are_known, unknown_ursulas,
|
||||
return threads.deferToThread(self.block_until_specific_nodes_are_known, unknown_ursulas,
|
||||
timeout=timeout,
|
||||
allow_missing=allow_missing)
|
||||
else:
|
||||
self.block_until_nodes_are_known(unknown_ursulas,
|
||||
timeout=timeout,
|
||||
allow_missing=allow_missing,
|
||||
learn_on_this_thread=True)
|
||||
self.block_until_specific_nodes_are_known(unknown_ursulas,
|
||||
timeout=timeout,
|
||||
allow_missing=allow_missing,
|
||||
learn_on_this_thread=True)
|
||||
|
||||
return unknown_ursulas, known_ursulas
|
||||
|
||||
|
@ -854,15 +946,14 @@ class Bob(Character):
|
|||
for counter, capsule in enumerate(work_order.capsules):
|
||||
# TODO: Ursula is actually supposed to sign this. See #141.
|
||||
# TODO: Maybe just update the work order here instead of setting it anew.
|
||||
work_orders_by_ursula = self._saved_work_orders[bytes(work_order.ursula.canonical_public_address)]
|
||||
work_orders_by_ursula = self._saved_work_orders[work_order.ursula.checksum_public_address]
|
||||
work_orders_by_ursula[capsule] = work_order
|
||||
return cfrags
|
||||
|
||||
def get_ursula(self, ursula_id):
|
||||
return self._ursulas[ursula_id]
|
||||
|
||||
def join_policy(self, label, alice_pubkey_sig,
|
||||
node_list=None, verify_sig=True):
|
||||
def join_policy(self, label, alice_pubkey_sig, node_list=None):
|
||||
if node_list:
|
||||
self._node_ids_to_learn_about_immediately.update(node_list)
|
||||
treasure_map = self.get_treasure_map(alice_pubkey_sig, label)
|
||||
|
@ -876,7 +967,7 @@ class Bob(Character):
|
|||
verifying=alice_verifying_key)
|
||||
|
||||
hrac, map_id = self.construct_hrac_and_map_id(alice_verifying_key, data_source.label)
|
||||
self.follow_treasure_map(map_id=map_id)
|
||||
self.follow_treasure_map(map_id=map_id, block=True)
|
||||
|
||||
work_orders = self.generate_work_orders(map_id, message_kit.capsule)
|
||||
|
||||
|
@ -886,15 +977,16 @@ class Bob(Character):
|
|||
cfrags = self.get_reencrypted_cfrags(work_order)
|
||||
message_kit.capsule.attach_cfrag(cfrags[0])
|
||||
|
||||
try:
|
||||
delivered_cleartext = self.verify_from(data_source,
|
||||
message_kit,
|
||||
decrypt=True,
|
||||
delegator_signing_key=alice_verifying_key)
|
||||
except self.InvalidSignature as e:
|
||||
raise RuntimeError(e)
|
||||
else:
|
||||
cleartexts.append(delivered_cleartext)
|
||||
verified, delivered_cleartext = self.verify_from(data_source,
|
||||
message_kit,
|
||||
decrypt=True,
|
||||
delegator_signing_key=alice_verifying_key)
|
||||
|
||||
if verified:
|
||||
cleartexts.append(delivered_cleartext)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Not verified - replace this with real message.") # TODO: Actually raise an error in verify_from instead of here 358
|
||||
return cleartexts
|
||||
|
||||
|
||||
|
@ -908,6 +1000,7 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
|
|||
InterfaceInfo)
|
||||
_dht_server_class = NucypherDHTServer
|
||||
_alice_class = Alice
|
||||
# TODO: Maybe this wants to be a registry, so that, for example, TLSHostingPower still can enjoy default status, but on a different class
|
||||
_default_crypto_powerups = [SigningPower, EncryptingPower]
|
||||
|
||||
class NotFound(Exception):
|
||||
|
@ -930,7 +1023,11 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
|
|||
federated_only=False,
|
||||
checksum_address=None,
|
||||
always_be_learning=None,
|
||||
crypto_power=None
|
||||
crypto_power=None,
|
||||
tls_curve=None,
|
||||
tls_private_key=None, # Obviously config here. 361
|
||||
known_nodes=(),
|
||||
**character_kwargs
|
||||
):
|
||||
|
||||
VerifiableNode.__init__(self, interface_signature=interface_signature)
|
||||
|
@ -946,13 +1043,18 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
|
|||
always_be_learning=always_be_learning,
|
||||
federated_only=federated_only,
|
||||
crypto_power=crypto_power,
|
||||
abort_on_learning_error=abort_on_learning_error)
|
||||
abort_on_learning_error=abort_on_learning_error,
|
||||
known_nodes=known_nodes,
|
||||
**character_kwargs)
|
||||
|
||||
if not federated_only:
|
||||
Miner.__init__(self, miner_agent=miner_agent, is_me=is_me, checksum_address=checksum_address)
|
||||
blockchain_power = BlockchainPower(blockchain=self.blockchain, account=self.checksum_public_address)
|
||||
self._crypto_power.consume_power_up(blockchain_power)
|
||||
ProxyRESTServer.__init__(self, host=rest_host, port=rest_port, db_name=db_name)
|
||||
ProxyRESTServer.__init__(self, host=rest_host, port=rest_port,
|
||||
db_name=db_name,
|
||||
tls_private_key=tls_private_key, tls_curve=tls_curve
|
||||
)
|
||||
|
||||
if is_me is True:
|
||||
# TODO: 340
|
||||
|
@ -1035,7 +1137,7 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
|
|||
@classmethod
|
||||
def from_bytes(cls, ursula_as_bytes, federated_only=False):
|
||||
signature, identity_evidence, verifying_key, encrypting_key, public_address, rest_info, dht_info = cls._internal_splitter(
|
||||
ursula_as_bytes)
|
||||
ursula_as_bytes)
|
||||
stranger_ursula_from_public_keys = cls.from_public_keys(
|
||||
{SigningPower: verifying_key, EncryptingPower: encrypting_key},
|
||||
interface_signature=signature,
|
||||
|
@ -1055,7 +1157,8 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
|
|||
stranger_ursulas = []
|
||||
|
||||
ursulas_attrs = cls._internal_splitter.repeat(ursulas_as_bytes)
|
||||
for (signature, identity_evidence, verifying_key, encrypting_key, public_address, rest_info, dht_info) in ursulas_attrs:
|
||||
for (signature, identity_evidence, verifying_key, encrypting_key, public_address, rest_info,
|
||||
dht_info) in ursulas_attrs:
|
||||
stranger_ursula_from_public_keys = cls.from_public_keys(
|
||||
{SigningPower: verifying_key, EncryptingPower: encrypting_key},
|
||||
interface_signature=signature,
|
||||
|
@ -1071,11 +1174,9 @@ class Ursula(Character, VerifiableNode, ProxyRESTServer, Miner):
|
|||
return stranger_ursulas
|
||||
|
||||
def __bytes__(self):
|
||||
message = self.canonical_public_address + self.rest_interface
|
||||
interface_info = VariableLengthBytestring(self.rest_interface)
|
||||
|
||||
if self.dht_interface:
|
||||
message += self.dht_interface
|
||||
interface_info += VariableLengthBytestring(self.dht_interface)
|
||||
|
||||
identity_evidence = VariableLengthBytestring(self._evidence_of_decentralized_identity)
|
||||
|
|
|
@ -125,7 +125,7 @@ def generate_self_signed_certificate(common_name, curve, private_key=None, days_
|
|||
cert = cert.serial_number(x509.random_serial_number())
|
||||
cert = cert.not_valid_before(now)
|
||||
cert = cert.not_valid_after(now + datetime.timedelta(days=days_valid))
|
||||
# TODO: What domain here? Not localhost presumably - ENS? #146
|
||||
# TODO: What are we going to do about domain name here? 179
|
||||
cert = cert.add_extension(x509.SubjectAlternativeName([x509.DNSName(u"localhost")]), critical=False)
|
||||
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
|
||||
return cert, private_key
|
||||
|
|
|
@ -5,7 +5,7 @@ from eth_keys.datatypes import PublicKey, Signature as EthSignature
|
|||
|
||||
from eth_utils import keccak
|
||||
from nucypher.keystore import keypairs
|
||||
from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair
|
||||
from nucypher.keystore.keypairs import SigningKeypair, EncryptingKeypair, HostingKeypair
|
||||
from umbral import pre
|
||||
from umbral.keys import UmbralPublicKey, UmbralPrivateKey, UmbralKeyingMaterial
|
||||
|
||||
|
@ -131,6 +131,7 @@ class BlockchainPower(CryptoPowerUp):
|
|||
class KeyPairBasedPower(CryptoPowerUp):
|
||||
confers_public_key = True
|
||||
_keypair_class = keypairs.Keypair
|
||||
_default_private_key_class = UmbralPrivateKey
|
||||
|
||||
def __init__(self,
|
||||
pubkey: UmbralPublicKey = None,
|
||||
|
@ -146,17 +147,17 @@ class KeyPairBasedPower(CryptoPowerUp):
|
|||
# UmbralPublicKey if they provided such a thing.
|
||||
if pubkey:
|
||||
try:
|
||||
key_to_pass_to_keypair = pubkey.as_umbral_pubkey()
|
||||
public_key = pubkey.as_umbral_pubkey()
|
||||
except AttributeError:
|
||||
try:
|
||||
key_to_pass_to_keypair = UmbralPublicKey.from_bytes(pubkey)
|
||||
public_key = UmbralPublicKey.from_bytes(pubkey)
|
||||
except TypeError:
|
||||
key_to_pass_to_keypair = pubkey
|
||||
public_key = pubkey
|
||||
self.keypair = self._keypair_class(
|
||||
public_key=public_key)
|
||||
else:
|
||||
# They didn't even pass pubkey_bytes. We'll generate a keypair.
|
||||
key_to_pass_to_keypair = UmbralPrivateKey.gen_key()
|
||||
self.keypair = self._keypair_class(
|
||||
umbral_key=key_to_pass_to_keypair)
|
||||
# They didn't even pass a public key. We have no choice but to generate a keypair.
|
||||
self.keypair = self._keypair_class(generate_keys_if_needed=True)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item in self.provides:
|
||||
|
@ -184,7 +185,7 @@ class DerivedKeyBasedPower(CryptoPowerUp):
|
|||
class SigningPower(KeyPairBasedPower):
|
||||
_keypair_class = SigningKeypair
|
||||
not_found_error = NoSigningPower
|
||||
provides = ("sign", "generate_self_signed_cert", "get_signature_stamp")
|
||||
provides = ("sign", "get_signature_stamp")
|
||||
|
||||
|
||||
class EncryptingPower(KeyPairBasedPower):
|
||||
|
@ -193,6 +194,11 @@ class EncryptingPower(KeyPairBasedPower):
|
|||
provides = ("decrypt",)
|
||||
|
||||
|
||||
class TLSHostingPower(KeyPairBasedPower):
|
||||
_keypair_class = HostingKeypair
|
||||
provides = ("get_deployer",)
|
||||
|
||||
|
||||
class DelegatingPower(DerivedKeyBasedPower):
|
||||
|
||||
def __init__(self):
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import sha3
|
||||
from typing import Union
|
||||
|
||||
import sha3
|
||||
from OpenSSL.SSL import TLSv1_2_METHOD
|
||||
from OpenSSL.crypto import X509
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from constant_sorrow import constants
|
||||
from hendrix.deploy.tls import HendrixDeployTLS
|
||||
from hendrix.facilities.services import ExistingKeyTLSContextFactory
|
||||
from nucypher.crypto import api as API
|
||||
from nucypher.crypto.api import generate_self_signed_certificate
|
||||
from constant_sorrow.constants import PUBLIC_ONLY
|
||||
from umbral.keys import UmbralPrivateKey, UmbralPublicKey
|
||||
from umbral import pre
|
||||
from umbral.config import default_curve
|
||||
from nucypher.crypto.kits import MessageKit
|
||||
from nucypher.crypto.signing import SignatureStamp, StrangerStamp
|
||||
from umbral import pre
|
||||
from umbral.config import default_curve
|
||||
from umbral.keys import UmbralPrivateKey, UmbralPublicKey
|
||||
from umbral.signing import Signature, Signer
|
||||
|
||||
|
||||
|
@ -17,29 +22,31 @@ class Keypair(object):
|
|||
A parent Keypair class for all types of Keypairs.
|
||||
"""
|
||||
|
||||
_private_key_source = UmbralPrivateKey.gen_key
|
||||
_public_key_method = "get_pubkey"
|
||||
|
||||
def __init__(self,
|
||||
umbral_key: Union[UmbralPrivateKey, UmbralPublicKey] = None,
|
||||
private_key=None,
|
||||
public_key=None,
|
||||
generate_keys_if_needed=True):
|
||||
"""
|
||||
Initalizes a Keypair object with an Umbral key object.
|
||||
|
||||
:param umbral_key: An UmbralPrivateKey or UmbralPublicKey
|
||||
:param generate_keys_if_needed: Generate keys or not?
|
||||
"""
|
||||
try:
|
||||
self.pubkey = umbral_key.get_pubkey()
|
||||
self._privkey = umbral_key
|
||||
except NotImplementedError:
|
||||
self.pubkey = umbral_key
|
||||
self._privkey = PUBLIC_ONLY
|
||||
except AttributeError:
|
||||
# They didn't pass anything we recognize as a valid key.
|
||||
if generate_keys_if_needed:
|
||||
self._privkey = UmbralPrivateKey.gen_key()
|
||||
self.pubkey = self._privkey.get_pubkey()
|
||||
else:
|
||||
raise ValueError(
|
||||
"Either pass a valid key as umbral_key or, if you want to generate keys, set generate_keys_if_needed to True.")
|
||||
if private_key and public_key:
|
||||
raise ValueError("Pass either private_key or public_key - not both.")
|
||||
elif private_key:
|
||||
self.pubkey = getattr(private_key, self._public_key_method)()
|
||||
self._privkey = private_key
|
||||
elif public_key:
|
||||
self.pubkey = public_key
|
||||
self._privkey = constants.PUBLIC_ONLY
|
||||
elif generate_keys_if_needed:
|
||||
self._privkey = self._private_key_source()
|
||||
self.pubkey = getattr(self._privkey, self._public_key_method)()
|
||||
else:
|
||||
raise ValueError(
|
||||
"Either pass a valid key or, if you want to generate keys, set generate_keys_if_needed to True.")
|
||||
|
||||
def serialize_pubkey(self, as_b64=False) -> bytes:
|
||||
"""
|
||||
|
@ -103,14 +110,49 @@ class SigningKeypair(Keypair):
|
|||
signature_der_bytes = API.ecdsa_sign(message, self._privkey)
|
||||
return Signature.from_bytes(signature_der_bytes, der_encoded=True)
|
||||
|
||||
def generate_self_signed_cert(self, common_name):
|
||||
cryptography_key = self._privkey.to_cryptography_privkey()
|
||||
return generate_self_signed_certificate(common_name, default_curve(), cryptography_key)
|
||||
|
||||
def get_signature_stamp(self):
|
||||
if self._privkey == PUBLIC_ONLY:
|
||||
if self._privkey == constants.PUBLIC_ONLY:
|
||||
return StrangerStamp(verifying_key=self.pubkey)
|
||||
else:
|
||||
signer = Signer(self._privkey)
|
||||
return SignatureStamp(verifying_key=self.pubkey, signer=signer)
|
||||
|
||||
|
||||
class HostingKeypair(Keypair):
|
||||
"""
|
||||
A keypair for TLS'ing.
|
||||
"""
|
||||
_private_key_source = ec.generate_private_key
|
||||
_public_key_method = "public_key"
|
||||
|
||||
_DEFAULT_CURVE = ec.SECP384R1
|
||||
|
||||
def __init__(self,
|
||||
common_name,
|
||||
private_key: Union[UmbralPrivateKey, UmbralPublicKey] = None,
|
||||
certificate=None,
|
||||
curve=None,
|
||||
generate_keys_if_needed=True):
|
||||
|
||||
self.curve = curve or self._DEFAULT_CURVE
|
||||
|
||||
if not certificate:
|
||||
self._certificate, private_key = generate_self_signed_certificate(common_name=common_name,
|
||||
private_key=private_key,
|
||||
curve=self.curve)
|
||||
else:
|
||||
self._certificate = certificate
|
||||
super().__init__(private_key=private_key)
|
||||
|
||||
def generate_self_signed_cert(self, common_name):
|
||||
cryptography_key = self._privkey.to_cryptography_privkey()
|
||||
return generate_self_signed_certificate(common_name, default_curve(), cryptography_key)
|
||||
|
||||
def get_deployer(self, rest_app, port):
|
||||
return HendrixDeployTLS("start",
|
||||
key=self._privkey,
|
||||
cert=X509.from_cryptography(self._certificate),
|
||||
context_factory=ExistingKeyTLSContextFactory,
|
||||
context_factory_kwargs={"curve_name": self.curve.name,
|
||||
"sslmethod": TLSv1_2_METHOD},
|
||||
options={"wsgi": rest_app, "https_port": port})
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
import requests
|
||||
|
||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||
from umbral.fragments import CapsuleFrag
|
||||
|
||||
|
||||
class RestMiddleware:
|
||||
|
||||
def consider_arrangement(self, ursula, arrangement=None):
|
||||
pass
|
||||
def consider_arrangement(self, arrangement):
|
||||
node = arrangement.ursula
|
||||
port = node.rest_interface.port
|
||||
address = node.rest_interface.host
|
||||
response = requests.post("https://{}:{}/consider_arrangement".format(address, port), bytes(arrangement), verify=False)
|
||||
if not response.status_code == 200:
|
||||
raise RuntimeError("Bad response: {}".format(response.content))
|
||||
return response
|
||||
|
||||
def enact_policy(self, ursula, id, payload):
|
||||
port = ursula.rest_interface.port
|
||||
address = ursula.rest_interface.host
|
||||
response = requests.post('https://{}:{}/kFrag/{}'.format(address, port, id.hex()), payload, verify=False)
|
||||
if not response.status_code == 200:
|
||||
raise RuntimeError("Bad response: {}".format(response.content))
|
||||
return True, ursula.stamp.as_umbral_pubkey()
|
||||
|
||||
def reencrypt(self, work_order):
|
||||
ursula_rest_response = self.send_work_order_payload_to_ursula(work_order)
|
||||
|
@ -40,10 +55,17 @@ class RestMiddleware:
|
|||
def node_information(self, host, port):
|
||||
return requests.get("https://{}:{}/public_information".format(host, port), verify=False) # TODO: TLS-only.
|
||||
|
||||
def get_nodes_via_rest(self, address, port, node_ids=None):
|
||||
if node_ids:
|
||||
def get_nodes_via_rest(self, address, port, announce_nodes=None, nodes_i_need=None):
|
||||
if nodes_i_need:
|
||||
# TODO: This needs to actually do something.
|
||||
# Include node_ids in the request; if the teacher node doesn't know about the
|
||||
# nodes matching these ids, then it will ask other nodes via the DHT or whatever.
|
||||
raise NotImplementedError
|
||||
response = requests.get("https://{}:{}/list_nodes".format(address, port), verify=False) # TODO: TLS-only.
|
||||
pass
|
||||
if announce_nodes:
|
||||
response = requests.post("https://{}:{}/node_metadata".format(address, port),
|
||||
verify=False,
|
||||
data=bytes().join(bytes(n) for n in announce_nodes)) # TODO: TLS-only.
|
||||
else:
|
||||
response = requests.get("https://{}:{}/node_metadata".format(address, port),
|
||||
verify=False) # TODO: TLS-only.
|
||||
return response
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from nucypher.crypto.powers import BlockchainPower, SigningPower, EncryptingPower, PowerUpError, NoSigningPower
|
||||
from nucypher.blockchain.eth.actors import only_me
|
||||
from nucypher.crypto.powers import BlockchainPower, SigningPower, EncryptingPower, NoSigningPower
|
||||
from constant_sorrow import constants
|
||||
from nucypher.network.protocols import SuspiciousActivity
|
||||
from eth_keys.datatypes import Signature as EthSignature
|
||||
import maya
|
||||
|
||||
|
||||
class VerifiableNode:
|
||||
|
@ -11,7 +13,7 @@ class VerifiableNode:
|
|||
verified_interface = False
|
||||
_verified_node = False
|
||||
|
||||
def __init__(self, interface_signature):
|
||||
def __init__(self, interface_signature=constants.NOT_SIGNED.bool_value(False)):
|
||||
self._interface_signature_object = interface_signature
|
||||
|
||||
class InvalidNode(SuspiciousActivity):
|
||||
|
|
|
@ -136,7 +136,7 @@ class InterfaceInfo:
|
|||
|
||||
def __init__(self, host, port):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.port = int(port)
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, url_string):
|
||||
|
|
|
@ -6,23 +6,22 @@ from typing import ClassVar
|
|||
import kademlia
|
||||
from apistar import http, Route, App
|
||||
from apistar.http import Response
|
||||
from constant_sorrow import constants
|
||||
from kademlia.crawling import NodeSpiderCrawl
|
||||
from kademlia.network import Server
|
||||
from kademlia.utils import digest
|
||||
|
||||
from bytestring_splitter import VariableLengthBytestring, BytestringSplitter
|
||||
from bytestring_splitter import VariableLengthBytestring
|
||||
from constant_sorrow import constants
|
||||
from hendrix.experience import crosstown_traffic
|
||||
from nucypher.config.configs import NetworkConfiguration
|
||||
from nucypher.crypto.constants import PUBLIC_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH
|
||||
from nucypher.crypto.kits import UmbralMessageKit
|
||||
from nucypher.crypto.powers import EncryptingPower, SigningPower
|
||||
from nucypher.crypto.powers import SigningPower, TLSHostingPower
|
||||
from nucypher.keystore.keypairs import HostingKeypair
|
||||
from nucypher.keystore.threading import ThreadedSession
|
||||
from nucypher.network.protocols import NucypherSeedOnlyProtocol, NucypherHashProtocol, InterfaceInfo
|
||||
from nucypher.network.storage import SeedOnlyStorage
|
||||
from umbral import pre
|
||||
from umbral.fragments import KFrag
|
||||
from umbral.keys import UmbralPublicKey
|
||||
from umbral.signing import Signature
|
||||
|
||||
|
||||
class NucypherDHTServer(Server):
|
||||
|
@ -104,11 +103,21 @@ class NucypherSeedOnlyDHTServer(NucypherDHTServer):
|
|||
|
||||
class ProxyRESTServer:
|
||||
|
||||
def __init__(self, host=None, port=None, db_name=None, *args, **kwargs):
|
||||
def __init__(self,
|
||||
host=None,
|
||||
port=None,
|
||||
db_name=None,
|
||||
tls_private_key=None,
|
||||
tls_curve=None,
|
||||
*args, **kwargs):
|
||||
self.rest_interface = InterfaceInfo(host=host, port=port)
|
||||
|
||||
self.db_name = db_name
|
||||
self._rest_app = None
|
||||
tls_hosting_keypair = HostingKeypair(common_name=self.checksum_public_address,
|
||||
private_key=tls_private_key, curve=tls_curve)
|
||||
tls_hosting_power = TLSHostingPower(keypair=tls_hosting_keypair)
|
||||
self._crypto_power.consume_power_up(tls_hosting_power)
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, network_config: NetworkConfiguration = None):
|
||||
|
@ -140,8 +149,10 @@ class ProxyRESTServer:
|
|||
self.reencrypt_via_rest),
|
||||
Route('/public_information', 'GET',
|
||||
self.public_information),
|
||||
Route('/list_nodes', 'GET',
|
||||
self.list_all_active_nodes_about_which_we_know),
|
||||
Route('/node_metadata', 'GET',
|
||||
self.all_known_nodes),
|
||||
Route('/node_metadata', 'POST',
|
||||
self.node_metadata_exchange),
|
||||
Route('/consider_arrangement',
|
||||
'POST',
|
||||
self.consider_arrangement),
|
||||
|
@ -188,14 +199,38 @@ class ProxyRESTServer:
|
|||
|
||||
return response
|
||||
|
||||
def list_all_active_nodes_about_which_we_know(self, request: http.Request):
|
||||
def all_known_nodes(self, request: http.Request):
|
||||
headers = {'Content-Type': 'application/octet-stream'}
|
||||
# TODO: mm hmmph *slowly exhales* fffff. Some 227 right here.
|
||||
ursulas_as_bytes = bytes().join(bytes(n) for n in self._known_nodes.values())
|
||||
ursulas_as_bytes += bytes(self)
|
||||
signature = self.stamp(ursulas_as_bytes)
|
||||
return Response(bytes(signature) + ursulas_as_bytes, headers=headers)
|
||||
|
||||
def node_metadata_exchange(self, request: http.Request, query_params: http.QueryParams):
|
||||
|
||||
nodes = self.batch_from_bytes(request.body, federated_only=self.federated_only)
|
||||
# TODO: This logic is basically repeated in learn_from_teacher_node. Let's find a better way.
|
||||
for node in nodes:
|
||||
|
||||
if node.checksum_public_address in self._known_nodes:
|
||||
continue # TODO: 168 Check version and update if required.
|
||||
|
||||
@crosstown_traffic()
|
||||
def learn_about_announced_nodes():
|
||||
try:
|
||||
node.verify_node(self.network_middleware, accept_federated_only=self.federated_only)
|
||||
except node.SuspiciousActivity:
|
||||
# TODO: Account for possibility that stamp, rather than interface, was bad.
|
||||
message = "Suspicious Activity: Discovered node with bad signature: {}. " \
|
||||
" Announced via REST." # TODO: Include data about caller?
|
||||
self.log.warning(message)
|
||||
|
||||
self.log.info("Previously unknown node: {}".format(node.checksum_public_address))
|
||||
self.remember_node(node)
|
||||
|
||||
# TODO: What's the right status code here? 202? Different if we already knew about the node?
|
||||
return self.all_known_nodes(request)
|
||||
|
||||
def consider_arrangement(self, request: http.Request):
|
||||
from nucypher.policy.models import Arrangement
|
||||
arrangement = Arrangement.from_bytes(request.body)
|
||||
|
@ -232,12 +267,6 @@ class ProxyRESTServer:
|
|||
# TODO: What do we do if the Policy isn't signed properly?
|
||||
pass
|
||||
|
||||
#
|
||||
# alices_signature, policy_payload =BytestringSplitter(Signature)(cleartext, return_remainder=True)
|
||||
|
||||
# TODO: If we're not adding anything else in the payload, stop using the
|
||||
# splitter here.
|
||||
# kfrag = policy_payload_splitter(policy_payload)[0]
|
||||
kfrag = KFrag.from_bytes(cleartext)
|
||||
|
||||
with ThreadedSession(self.db_engine) as session:
|
||||
|
@ -253,6 +282,7 @@ class ProxyRESTServer:
|
|||
from nucypher.policy.models import WorkOrder # Avoid circular import
|
||||
id = binascii.unhexlify(id_as_hex)
|
||||
work_order = WorkOrder.from_rest_payload(id, request.body)
|
||||
self.log.info("Work Order from {}, signed {}".format(work_order.bob, work_order.receipt_signature))
|
||||
with ThreadedSession(self.db_engine) as session:
|
||||
kfrag_bytes = self.datastore.get_policy_arrangement(id.hex().encode(),
|
||||
session=session).k_frag # Careful! :-)
|
||||
|
@ -262,7 +292,9 @@ class ProxyRESTServer:
|
|||
|
||||
for capsule in work_order.capsules:
|
||||
# TODO: Sign the result of this. See #141.
|
||||
cfrag_byte_stream += VariableLengthBytestring(pre.reencrypt(kfrag, capsule))
|
||||
cfrag = pre.reencrypt(kfrag, capsule)
|
||||
self.log.info("Re-encrypting for Capsule {}, made CFrag {}.".format(capsule, cfrag))
|
||||
cfrag_byte_stream += VariableLengthBytestring(cfrag)
|
||||
|
||||
# TODO: Put this in Ursula's datastore
|
||||
self._work_orders.append(work_order)
|
||||
|
@ -309,3 +341,8 @@ class ProxyRESTServer:
|
|||
# TODO: Make this a proper 500 or whatever.
|
||||
self.log.info("Bad TreasureMap ID; not storing {}".format(treasure_map_id))
|
||||
assert False
|
||||
|
||||
def get_deployer(self):
|
||||
deployer = self._crypto_power.power_ups(TLSHostingPower).get_deployer(rest_app=self._rest_app,
|
||||
port=self.rest_interface.port)
|
||||
return deployer
|
||||
|
|
|
@ -9,6 +9,7 @@ import msgpack
|
|||
|
||||
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
|
||||
from constant_sorrow import constants
|
||||
from eth_utils import to_canonical_address, to_checksum_address
|
||||
from nucypher.characters import Alice
|
||||
from nucypher.characters import Bob, Ursula, Character
|
||||
from nucypher.crypto.api import keccak_digest, encrypt_and_sign, secure_random
|
||||
|
@ -280,9 +281,11 @@ class FederatedPolicy(Policy):
|
|||
def make_arrangements(self, network_middleware,
|
||||
deposit: int,
|
||||
expiration: maya.MayaDT,
|
||||
ursulas: Set[Ursula] = None) -> None:
|
||||
if ursulas is None:
|
||||
handpicked_ursulas: Set[Ursula] = None) -> None:
|
||||
if handpicked_ursulas is None:
|
||||
ursulas = set()
|
||||
else:
|
||||
ursulas = handpicked_ursulas
|
||||
ursulas.update(self.ursulas)
|
||||
|
||||
if len(ursulas) < self.n:
|
||||
|
@ -308,7 +311,7 @@ class TreasureMap:
|
|||
(bytes, KECCAK_DIGEST_LENGTH), # hrac
|
||||
(UmbralMessageKit, VariableLengthBytestring)
|
||||
)
|
||||
node_id_splitter = BytestringSplitter(int(PUBLIC_ADDRESS_LENGTH), Arrangement.ID_LENGTH)
|
||||
node_id_splitter = BytestringSplitter((to_checksum_address, int(PUBLIC_ADDRESS_LENGTH)), Arrangement.ID_LENGTH)
|
||||
|
||||
class InvalidSignature(Exception):
|
||||
"""Raised when the public signature (typically intended for Ursula) is not valid."""
|
||||
|
@ -378,12 +381,12 @@ class TreasureMap:
|
|||
if self.destinations == constants.NO_DECRYPTION_PERFORMED:
|
||||
return constants.NO_DECRYPTION_PERFORMED
|
||||
else:
|
||||
return bytes().join(ursula_id + arrangement_id for ursula_id, arrangement_id in self.destinations.items())
|
||||
return bytes().join(to_canonical_address(ursula_id) + arrangement_id for ursula_id, arrangement_id in self.destinations.items())
|
||||
|
||||
def add_arrangement(self, arrangement):
|
||||
if self.destinations == constants.NO_DECRYPTION_PERFORMED:
|
||||
raise TypeError("This TreasureMap is encrypted. You can't add another node without decrypting it.")
|
||||
self.destinations[arrangement.ursula.canonical_public_address] = arrangement.id
|
||||
self.destinations[arrangement.ursula.checksum_public_address] = arrangement.id
|
||||
|
||||
def public_id(self):
|
||||
"""
|
||||
|
@ -505,11 +508,7 @@ class WorkOrderHistory:
|
|||
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.")
|
||||
return self.by_ursula.setdefault(item, {})
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
assert False
|
||||
|
|
|
@ -87,7 +87,7 @@ def test_bob_can_follow_treasure_map_even_if_he_only_knows_of_one_node(enacted_f
|
|||
bob.start_learning_loop()
|
||||
|
||||
# ...and block until the unknown_nodes have all been found.
|
||||
yield threads.deferToThread(bob.block_until_nodes_are_known, unknown_nodes)
|
||||
yield threads.deferToThread(bob.block_until_specific_nodes_are_known, unknown_nodes)
|
||||
|
||||
# ...and he now has no more unknown_nodes.
|
||||
print(len(bob._known_nodes))
|
||||
|
|
|
@ -136,6 +136,14 @@ def test_anybody_can_encrypt():
|
|||
assert ciphertext is not None
|
||||
|
||||
|
||||
def test_node_deployer(ursulas):
|
||||
for ursula in ursulas:
|
||||
deployer = ursula.get_deployer()
|
||||
assert deployer.options['https_port'] == ursula.rest_interface.port
|
||||
assert deployer.application == ursula.rest_app
|
||||
|
||||
|
||||
|
||||
"""
|
||||
What follows are various combinations of signing and encrypting, to match
|
||||
real-world scenarios.
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import pytest
|
||||
|
||||
from nucypher.characters import Ursula
|
||||
from nucypher.crypto.api import secure_random
|
||||
from nucypher.crypto.powers import SigningPower, CryptoPower
|
||||
from eth_keys.datatypes import Signature as EthSignature
|
||||
|
||||
|
||||
@pytest.mark.usesfixtures('testerchain')
|
||||
def test_ursula_generates_self_signed_cert():
|
||||
ursula = Ursula(is_me=False, rest_port=5000, rest_host="not used", federated_only=True)
|
||||
cert, cert_private_key = ursula.generate_self_signed_certificate()
|
||||
public_key_numbers = ursula.public_key(SigningPower).to_cryptography_pubkey().public_numbers()
|
||||
assert cert.public_key().public_numbers() == public_key_numbers
|
||||
from tests.utilities import make_ursulas, MockRestMiddleware
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
|
@ -18,6 +13,27 @@ def test_federated_ursula_substantiates_stamp():
|
|||
assert False
|
||||
|
||||
|
||||
def test_new_ursula_announces_herself():
|
||||
ursula_here, ursula_there = make_ursulas(2,
|
||||
know_each_other=False,
|
||||
network_middleware=MockRestMiddleware())
|
||||
|
||||
# Neither Ursula knows about the other.
|
||||
assert ursula_here._known_nodes == ursula_there._known_nodes == {}
|
||||
|
||||
ursula_here.remember_node(ursula_there)
|
||||
|
||||
# OK, now, ursula_here knows about ursula_there, but not vice-versa.
|
||||
assert ursula_there in ursula_here._known_nodes.values()
|
||||
assert not ursula_here in ursula_there._known_nodes.values()
|
||||
|
||||
# But as ursula_here learns, she'll announce herself to ursula_there.
|
||||
ursula_here.learn_from_teacher_node()
|
||||
|
||||
assert ursula_there in ursula_here._known_nodes.values()
|
||||
assert ursula_here in ursula_there._known_nodes.values()
|
||||
|
||||
|
||||
def test_blockchain_ursula_substantiates_stamp(mining_ursulas):
|
||||
first_ursula = list(mining_ursulas)[0]
|
||||
signature_as_bytes = first_ursula._evidence_of_decentralized_identity
|
||||
|
|
|
@ -81,7 +81,7 @@ def enacted_federated_policy(idle_federated_policy, ursulas):
|
|||
idle_federated_policy.make_arrangements(network_middleware,
|
||||
deposit=deposit,
|
||||
expiration=contract_end_datetime,
|
||||
ursulas=ursulas)
|
||||
handpicked_ursulas=ursulas)
|
||||
idle_federated_policy.enact(network_middleware) # REST call happens here, as does population of TreasureMap.
|
||||
|
||||
return idle_federated_policy
|
||||
|
@ -168,7 +168,6 @@ def ursulas(three_agents):
|
|||
token_agent, miner_agent, policy_agent = three_agents
|
||||
ether_addresses = [to_checksum_address(os.urandom(20)) for _ in range(constants.NUMBER_OF_URSULAS_IN_NETWORK)]
|
||||
_ursulas = make_ursulas(ether_addresses=ether_addresses,
|
||||
ursula_starting_port=int(constants.URSULA_PORT_SEED),
|
||||
miner_agent=miner_agent
|
||||
)
|
||||
try:
|
||||
|
@ -190,7 +189,6 @@ def mining_ursulas(three_agents):
|
|||
ursula_addresses = all_yall[:int(constants.NUMBER_OF_URSULAS_IN_NETWORK)]
|
||||
|
||||
_ursulas = make_ursulas(ether_addresses=ursula_addresses,
|
||||
ursula_starting_port=int(starting_point),
|
||||
miner_agent=miner_agent,
|
||||
miners=True)
|
||||
try:
|
||||
|
@ -215,7 +213,6 @@ def non_ursula_miners(three_agents):
|
|||
starting_point = constants.URSULA_PORT_SEED + 500
|
||||
|
||||
_ursulas = make_ursulas(ether_addresses=ursula_addresses,
|
||||
ursula_starting_port=int(starting_point),
|
||||
miner_agent=miner_agent,
|
||||
miners=True,
|
||||
bare=True)
|
||||
|
|
|
@ -23,14 +23,14 @@ def test_keypair_with_umbral_keys():
|
|||
assert new_keypair_from_priv._privkey.bn_key.to_bytes() == umbral_privkey.bn_key.to_bytes()
|
||||
assert new_keypair_from_priv.pubkey.to_bytes() == umbral_pubkey.to_bytes()
|
||||
|
||||
new_keypair_from_pub = keypairs.Keypair(umbral_pubkey)
|
||||
new_keypair_from_pub = keypairs.Keypair(public_key=umbral_pubkey)
|
||||
assert new_keypair_from_pub.pubkey.to_bytes() == umbral_pubkey.to_bytes()
|
||||
assert new_keypair_from_pub._privkey == PUBLIC_ONLY
|
||||
|
||||
|
||||
def test_keypair_serialization():
|
||||
umbral_pubkey = UmbralPrivateKey.gen_key().get_pubkey()
|
||||
new_keypair = keypairs.Keypair(umbral_pubkey)
|
||||
new_keypair = keypairs.Keypair(public_key=umbral_pubkey)
|
||||
|
||||
pubkey_bytes = new_keypair.serialize_pubkey()
|
||||
assert pubkey_bytes == bytes(umbral_pubkey)
|
||||
|
@ -41,7 +41,7 @@ def test_keypair_serialization():
|
|||
|
||||
def test_keypair_fingerprint():
|
||||
umbral_pubkey = UmbralPrivateKey.gen_key().get_pubkey()
|
||||
new_keypair = keypairs.Keypair(umbral_pubkey)
|
||||
new_keypair = keypairs.Keypair(public_key=umbral_pubkey)
|
||||
|
||||
fingerprint = new_keypair.fingerprint()
|
||||
assert fingerprint != None
|
||||
|
|
|
@ -142,7 +142,7 @@ def test_bob_can_retreive_the_treasure_map_and_decrypt_it(enacted_federated_poli
|
|||
enacted_federated_policy.label)
|
||||
|
||||
# Let's imagine he has learned about some - say, from the blockchain.
|
||||
bob._known_nodes = {u.canonical_public_address: u for u in ursulas}
|
||||
bob._known_nodes = {u.checksum_public_address: u for u in ursulas}
|
||||
|
||||
# Now try.
|
||||
treasure_map_from_wire = bob.get_treasure_map(enacted_federated_policy.alice.stamp,
|
||||
|
|
|
@ -6,12 +6,14 @@ from typing import List, Set
|
|||
import maya
|
||||
from apistar.test import TestClient
|
||||
from constant_sorrow import constants
|
||||
from eth_utils import to_checksum_address
|
||||
|
||||
from nucypher.blockchain.eth.agents import MinerAgent
|
||||
from nucypher.characters import Ursula
|
||||
#
|
||||
# Setup
|
||||
#
|
||||
from nucypher.crypto.api import secure_random
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.policy.models import Arrangement, Policy
|
||||
|
||||
|
@ -24,8 +26,12 @@ constants.NUMBER_OF_URSULAS_IN_NETWORK(10)
|
|||
_ALL_URSULAS = {}
|
||||
|
||||
|
||||
def make_ursulas(ether_addresses: list, ursula_starting_port: int,
|
||||
miner_agent=None, miners=False, bare=False) -> Set[Ursula]:
|
||||
def make_ursulas(ether_addresses: list,
|
||||
miner_agent=None,
|
||||
miners=False,
|
||||
bare=False,
|
||||
know_each_other=True,
|
||||
**ursula_kwargs) -> Set[Ursula]:
|
||||
"""
|
||||
:param ether_addresses: Ethereum addresses to create ursulas with.
|
||||
:param ursula_starting_port: The port of the first created Ursula; subsequent Ursulas will increment the port number by 1.
|
||||
|
@ -39,10 +45,18 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
|
|||
:return: A list of created Ursulas
|
||||
"""
|
||||
|
||||
if isinstance(ether_addresses, int):
|
||||
ether_addresses = [to_checksum_address(secure_random(20)) for _ in range(ether_addresses)]
|
||||
|
||||
|
||||
event_loop = asyncio.get_event_loop()
|
||||
if not _ALL_URSULAS:
|
||||
starting_port = constants.URSULA_PORT_SEED
|
||||
else:
|
||||
starting_port = max(_ALL_URSULAS.keys()) + 1
|
||||
|
||||
ursulas = set()
|
||||
for port, ether_address in enumerate(ether_addresses, start=ursula_starting_port):
|
||||
for port, ether_address in enumerate(ether_addresses, start=starting_port):
|
||||
|
||||
if bare:
|
||||
ursula = Ursula(is_me=False, # do not attach dht server
|
||||
|
@ -51,7 +65,8 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
|
|||
checksum_address=ether_address,
|
||||
always_be_learning=False,
|
||||
miner_agent=miner_agent,
|
||||
abort_on_learning_error=True)
|
||||
abort_on_learning_error=True,
|
||||
**ursula_kwargs)
|
||||
|
||||
ursula.is_me = True # Patch to allow execution of transacting methods in tests
|
||||
|
||||
|
@ -68,7 +83,8 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
|
|||
rest_port=port+100,
|
||||
always_be_learning=False,
|
||||
miner_agent=miner_agent,
|
||||
federated_only=federated_only)
|
||||
federated_only=federated_only,
|
||||
**ursula_kwargs)
|
||||
|
||||
ursula.attach_rest_server()
|
||||
|
||||
|
@ -79,16 +95,6 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
|
|||
ursula.datastore_threadpool = MockDatastoreThreadPool()
|
||||
ursula.dht_listen()
|
||||
|
||||
for ursula_to_teach in ursulas:
|
||||
# Add other Ursulas as known nodes.
|
||||
for ursula_to_learn_about in ursulas:
|
||||
ursula_to_teach.remember_node(ursula_to_learn_about)
|
||||
|
||||
event_loop.run_until_complete(
|
||||
ursula.dht_server.bootstrap(
|
||||
[("127.0.0.1", ursula_starting_port + _c) for _c in range(len(ursulas))]))
|
||||
ursula.publish_dht_information()
|
||||
|
||||
if miners is True:
|
||||
# TODO: 309
|
||||
# stake a random amount
|
||||
|
@ -106,6 +112,18 @@ def make_ursulas(ether_addresses: list, ursula_starting_port: int,
|
|||
ursulas.add(ursula)
|
||||
_ALL_URSULAS[ursula.rest_interface.port] = ursula
|
||||
|
||||
if know_each_other and not bare:
|
||||
|
||||
for ursula_to_teach in ursulas:
|
||||
# Add other Ursulas as known nodes.
|
||||
for ursula_to_learn_about in ursulas:
|
||||
ursula_to_teach.remember_node(ursula_to_learn_about)
|
||||
|
||||
event_loop.run_until_complete(
|
||||
ursula.dht_server.bootstrap(
|
||||
[("127.0.0.1", starting_port + _c) for _c in range(len(ursulas))]))
|
||||
ursula.publish_dht_information()
|
||||
|
||||
return ursulas
|
||||
|
||||
|
||||
|
@ -177,15 +195,22 @@ class MockRestMiddleware(RestMiddleware):
|
|||
response = mock_client.get("http://localhost/public_information")
|
||||
return response
|
||||
|
||||
def get_nodes_via_rest(self, address, port, node_ids):
|
||||
def get_nodes_via_rest(self, address, port, announce_nodes=None, nodes_i_need=None):
|
||||
mock_client = self.__get_mock_client_by_port(port)
|
||||
# TODO: Better passage of node IDs here.
|
||||
# if node_ids:
|
||||
# node_address_bytestring = bytes().join(bytes(id) for id in node_ids)
|
||||
# params = {'nodes': node_address_bytestring}
|
||||
# else:
|
||||
# params = None
|
||||
response = mock_client.get("http://localhost/list_nodes")
|
||||
|
||||
if nodes_i_need:
|
||||
# TODO: This needs to actually do something.
|
||||
# Include node_ids in the request; if the teacher node doesn't know about the
|
||||
# nodes matching these ids, then it will ask other nodes via the DHT or whatever.
|
||||
pass
|
||||
|
||||
if announce_nodes:
|
||||
response = mock_client.post("https://{}:{}/node_metadata".format(address, port),
|
||||
verify=False,
|
||||
data=bytes().join(bytes(n) for n in announce_nodes)) # TODO: TLS-only.
|
||||
else:
|
||||
response = mock_client.get("https://{}:{}/node_metadata".format(address, port),
|
||||
verify=False) # TODO: TLS-only.
|
||||
return response
|
||||
|
||||
def put_treasure_map_on_node(self, node, map_id, map_payload):
|
||||
|
|
Loading…
Reference in New Issue