1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471 |
- #!/usr/bin/env python3
- import random # For simulation, not cryptography!
- import math
- import sys
- import logging
- import nacl.utils
- import nacl.signing
- import nacl.public
- import nacl.hash
- import nacl.bindings
- import network
- import dirauth
- vrftable = dict()
- # Simulated VRF. The output and proof are of the correct size (32 bytes
- # for the output, 48 bytes for the proof), and we charge the right
- # number of group operations (1 keygen, 2 DH for proving, 3 DH for
- # verifiying -- that last number charges 1 DH for a (s*G+c*Y) multiexp,
- # but 2 for a (s*A+c*B) multiexp)
- class VRF:
- @staticmethod
- def get_output(vrf_privkey, vrf_input, perfstats):
- """Given the VRF private key and the input, produce
- the output value and the proof."""
- # nacl can't do the group operations we need to actually
- # compute the VRF, so we just fake it for the simulation. The
- # output value is sha256(privkey, input), and the proof is
- # sha256(pubkey, input, output) + 16 bytes of 0.
- # ***THIS IS NOT A REAL VRF!***
- val = nacl.hash.sha256(bytes(vrf_privkey) + vrf_input,
- encoder=nacl.encoding.RawEncoder)
- vrf_pubkey = vrf_privkey.public_key
- proof = nacl.hash.sha256(bytes(vrf_pubkey) + val + vrf_input,
- encoder=nacl.encoding.RawEncoder) + bytes(16)
- perfstats.keygens += 1
- perfstats.dhs += 2
- vrftable[proof] = (bytes(vrf_privkey), vrf_input, val, bytes(vrf_pubkey))
- return val, proof
- @staticmethod
- def check_output(vrf_pubkey, vrf_input, vrf_output, perfstats):
- """Given the VRF public key, the input, and the claimed output
- and proof, raise an exception if the proof fails to check.
- Returns the VRF output."""
- # Again, NOT A REAL VRF!
- val, proof = vrf_output
- if nacl.hash.sha256(vrf_pubkey + val + vrf_input,
- encoder=nacl.encoding.RawEncoder) + bytes(16) != \
- proof:
- t = [nacl.encoding.HexEncoder.encode(x) for x in vrftable[proof]]
- raise ValueError("VRF proof did not verify: %s %s %s 00x16 ?= %s: %s" % \
- (nacl.encoding.HexEncoder.encode(vrf_pubkey),
- nacl.encoding.HexEncoder.encode(val),
- nacl.encoding.HexEncoder.encode(vrf_input),
- nacl.encoding.HexEncoder.encode(proof),
- t))
- perfstats.dhs += 3
- del vrftable[proof]
- return val
- class RelayNetMsg(network.NetMsg):
- """The subclass of NetMsg for messages between relays and either
- relays or clients."""
- class RelayGetConsensusMsg(RelayNetMsg):
- """The subclass of RelayNetMsg for fetching the consensus. Sent by
- clients to relays."""
- class RelayConsensusMsg(RelayNetMsg):
- """The subclass of RelayNetMsg for returning the consensus from
- relays to clients, in response to RelayGetConsensusMsg."""
- def __init__(self, consensus):
- self.consensus = consensus
- class RelayGetDescMsg(RelayNetMsg):
- """The subclass of RelayNetMsg sent by clients to their guards for
- retrieving the guard's current descriptor."""
- class RelayDescMsg(RelayNetMsg):
- """The subclass of RelayNetMsg sent by guards to clients for
- reporting their current descriptor."""
- def __init__(self, desc):
- self.desc = desc
- class RelayGetConsensusDiffMsg(RelayNetMsg):
- """The subclass of RelayNetMsg for fetching the consensus, if the
- requestor already has the previous consensus. Sent by clients to
- relays."""
- class RelayConsensusDiffMsg(RelayNetMsg):
- """The subclass of RelayNetMsg for returning the consensus, if the
- requestor already has the previous consensus. We don't _actually_
- produce the diff at this time; we just charge fewer bytes for this
- message. Sent by relays to clients in response to
- RelayGetConsensusDiffMsg."""
- def __init__(self, consensus):
- self.consensus = consensus
- def size(self):
- if network.symbolic_byte_counters:
- return super().size()
- return math.ceil(RelayConsensusMsg(self.consensus).size() \
- * network.P_Delta)
- class RelayRandomHopMsg(RelayNetMsg):
- """A message used for testing, that hops from relay to relay
- randomly until its TTL expires."""
- def __init__(self, ttl):
- self.ttl = ttl
- def __str__(self):
- return "RandomHop TTL=%d" % self.ttl
- class CircuitCellMsg(RelayNetMsg):
- """Send a message tagged with a circuit id. This is the container
- class for all RelayCell messages."""
- def __init__(self, circuitid, cell):
- self.circid = circuitid
- self.cell = cell
- def __str__(self):
- return "C%d:%s" % (self.circid, self.cell)
- def size(self):
- # circuitids are 4 bytes
- return 4 + self.cell.size()
- class RelayCell(RelayNetMsg):
- """All cells (which are sent inside a CircuitCellMsg, and so do not
- need their own circuitid) should be a subclass of this class."""
- class StringCell(RelayCell):
- """Send an arbitrary string as a cell."""
- def __init__(self, str):
- self.data = str
- def __str__(self):
- return self.data.__str__()
- class CloseCell(RelayCell):
- """Close the circuit this cell was sent on. It should be sent
- _unencrypted_ (not within an EncryptedCell), and relays that receive
- one should forward it along the adjacent circuit, then close both
- the circuit it was received on and the adjacent one."""
- # It is intentional that VanillaCreateCircuitMsg is a RelayNetMsg and
- # not a RelayCell. This is the message that _creates_ the circuit, so
- # it can't be sent as a cell _within_ the circuit.
- class VanillaCreateCircuitMsg(RelayNetMsg):
- """The message for requesting circuit creation in Vanilla Onion
- Routing."""
- def __init__(self, circid, ntor_request):
- self.circid = circid
- self.ntor_request = ntor_request
- class VanillaCreatedCircuitCell(RelayCell):
- """The message for responding to circuit creation in Vanilla Onion
- Routing."""
- def __init__(self, ntor_reply):
- self.ntor_reply = ntor_reply
- class VanillaExtendCircuitCell(RelayCell):
- """The message for requesting circuit extension in Vanilla Onion
- Routing."""
- def __init__(self, hopaddr, ntor_request):
- self.hopaddr = hopaddr
- self.ntor_request = ntor_request
- class VanillaExtendedCircuitCell(RelayCell):
- """The message for responding to circuit extension in Vanilla Onion
- Routing."""
- def __init__(self, ntor_reply):
- self.ntor_reply = ntor_reply
- # It is intentional that TelescopingCreateCircuitMsg is a RelayNetMsg and
- # not a RelayCell. This is the message that _creates_ the circuit, so
- # it can't be sent as a cell _within_ the circuit.
- class TelescopingCreateCircuitMsg(RelayNetMsg):
- """The message for requesting circuit creation in Telescoping Onion
- Routing."""
- def __init__(self, circid, ntor_request):
- self.circid = circid
- self.ntor_request = ntor_request
- class TelescopingCreatedCircuitCell(RelayCell):
- """The message for responding to circuit creation in Telescoping Walking
- Onions."""
- def __init__(self, ntor_reply):
- self.ntor_reply = ntor_reply
- class TelescopingExtendCircuitCell(RelayCell):
- """The message for requesting circuit extension in Telescoping Walking
- Onions."""
- def __init__(self, idx, ntor_request):
- self.idx = idx
- self.ntor_request = ntor_request
- class TelescopingExtendedCircuitCell(RelayCell):
- """The message for responding to circuit extension in Telescoping Walking
- Onions."""
- def __init__(self, ntor_reply, snip):
- self.ntor_reply = ntor_reply
- self.snip = snip
- class SinglePassCreateCircuitMsg(RelayNetMsg):
- """The message for requesting circuit creation in Single Pass Onion
- Routing. This is used to extend a Single-Pass circuit by clients."""
- def __init__(self, circid, ntor_request, client_path_selection_key,
- ttl=2):
- self.circid = circid
- self.ntor_request = ntor_request
- self.clipathselkey = bytes(client_path_selection_key)
- self.ttl = ttl
- class SinglePassCreatedCircuitCell(RelayCell):
- """The message for responding to circuit creation in Single-Pass Walking
- Onions."""
- def __init__(self, ntor_reply, encrypted_cell):
- self.ntor_reply = ntor_reply
- # The above field is sent in plaintext; the below is an
- # SinglePassCreatedEnc, or None if this is the last layer
- self.enc = encrypted_cell
- class EncryptedCell(RelayCell):
- """Send a message encrypted with a symmetric key. In this
- implementation, the encryption is not really done. A hash of the
- key is stored with the message so that it can be checked at
- decryption time."""
- def __init__(self, key, msg):
- self.keyhash = nacl.hash.sha256(key)
- self.plaintext = msg
- def decrypt(self, key):
- keyhash = nacl.hash.sha256(key)
- if keyhash != self.keyhash:
- raise ValueError("EncryptedCell key mismatch")
- return self.plaintext
- def size(self):
- # Current Tor actually has no overhead for encryption
- return self.plaintext.size()
- class SinglePassCreatedEnc(EncryptedCell):
- """The nested encrypted informaion inside a
- SinglePassCreatedCircuitCell. nextlayer is itself a
- SinglePassCreatedCircuitCell."""
- def __init__(self, key, snip, vrf_output, nextlayer):
- super().__init__(key, (snip, vrf_output, nextlayer))
- class RelayFallbackTerminationError(Exception):
- """An exception raised when someone tries to terminate a fallback
- relay."""
- class Sphinx:
- """Implement the public-key reblinding technique based on Sphinx.
- This does a few more public-key operations than it would strictly
- need to if we were using a group implementation that (unlike nacl)
- supported the operations we needed directly. The biggest issue is
- that nacl insists the high bit is set on private keys, which means
- we can't just multiply private keys together to get a new private
- key, and do a single DH operation with that resulting key; we have
- to perform a linear number of DH operations instead, per node in the
- circuit, so a quadratic number of DH operations total."""
- @staticmethod
- def makeblindkey(shared_secret, domain_separator, perfstats):
- """Create a Sphinx reblinding key (a PrivateKey) out of a shared
- secret and a domain separator (both bytestrings). The domain
- separator is just a constant bytestring like b'data' or
- b'circuit' for the data-protecting and circuit-protecting
- public-key elements respectively."""
- rawkey = nacl.hash.sha256(domain_separator + shared_secret,
- encoder=nacl.encoding.RawEncoder)
- perfstats.keygens += 1
- # The PrivateKey constructor does the Curve25519 pinning of
- # certain bits of the key to 0 and 1
- return nacl.public.PrivateKey(rawkey)
- @staticmethod
- def reblindpubkey(blindkey, pubkey, perfstats):
- """Create a Sphinx reblinded PublicKey out of a reblinding key
- (output by makeblindkey) and a (possibly already reblinded)
- PublicKey."""
- new_pubkey = nacl.bindings.crypto_scalarmult(bytes(blindkey),
- bytes(pubkey))
- perfstats.dhs += 1
- return nacl.public.PublicKey(new_pubkey)
- @staticmethod
- def client(client_privkey, blindkey_list, server_pubkey,
- domain_separator, is_last, perfstats):
- """Given the client's PrivateKey, a (possibly empty) list of
- reblinding keys, and the server's PublicKey, produce the shared
- secret and the new blinding key (to add to the list). The
- domain separator is as above. If is_last is true, don't bother
- creating the new blinding key, since this is the last iteration,
- and we won't be using it."""
- if type(server_pubkey) is bytes:
- server_pubkey = nacl.public.PublicKey(server_pubkey)
- reblinded_server_pubkey = server_pubkey
- for blindkey in blindkey_list:
- reblinded_server_pubkey = Sphinx.reblindpubkey(blindkey,
- reblinded_server_pubkey, perfstats)
- sharedsecret = nacl.public.Box(client_privkey,
- reblinded_server_pubkey).shared_key()
- perfstats.dhs += 1
- if is_last:
- blindkey = None
- else:
- blindkey = Sphinx.makeblindkey(sharedsecret,
- domain_separator, perfstats)
- return sharedsecret, blindkey
- @staticmethod
- def server(client_pubkey, server_privkey, domain_separator, is_last,
- perfstats):
- """Given the client's PublicKey and the server's PrivateKey,
- produce the shared secret and the new reblinded client
- PublicKey. The domain separator is as above. If is_last is
- True, don't bother generating the new PublicKey, since we're the
- last server in the chain, and won't be using it."""
- sharedsecret = nacl.public.Box(server_privkey,
- client_pubkey).shared_key()
- perfstats.dhs += 1
- if is_last:
- blinded_pubkey = None
- else:
- blindkey = Sphinx.makeblindkey(sharedsecret, domain_separator,
- perfstats)
- blinded_pubkey = Sphinx.reblindpubkey(blindkey, client_pubkey,
- perfstats)
- return sharedsecret, blinded_pubkey
- class NTor:
- """A class implementing the ntor one-way authenticated key agreement
- scheme. The details are not exactly the same as either the ntor
- paper or Tor's implementation, but it will agree on keys and have
- the same number of public key operations."""
- def __init__(self, perfstats):
- self.perfstats = perfstats
- # Only used for Single-Pass Walking Onions; it is the sequence
- # of blinding keys used by Sphinx
- self.blinding_keys = []
- def request(self):
- """Create the ntor request message: X = g^x."""
- self.client_ephem_key = nacl.public.PrivateKey.generate()
- self.perfstats.keygens += 1
- return bytes(self.client_ephem_key.public_key)
- @staticmethod
- def reply(onion_privkey, idpubkey, client_pubkey, perfstats,
- sphinx_domainsep=None):
- """The server calls this static method to produce the ntor reply
- message: (Y = g^y, B = g^b, A = H(M, "verify")) and the shared
- secret S = H(M, "secret") for M = (X^y,X^b,ID,B,X,Y). If
- sphinx_domainsep is not None, also compute and return the Sphinx
- reblinded client request to pass to the next server."""
- if type(idpubkey) is not bytes:
- idpubkey = bytes(idpubkey)
- if type(client_pubkey) is bytes:
- client_pubkey = nacl.public.PublicKey(client_pubkey)
- server_ephem_key = nacl.public.PrivateKey.generate()
- perfstats.keygens += 1
- xykey = nacl.public.Box(server_ephem_key, client_pubkey).shared_key()
- xbkey = nacl.public.Box(onion_privkey, client_pubkey).shared_key()
- perfstats.dhs += 2
- M = xykey + xbkey + \
- idpubkey + \
- onion_privkey.public_key.encode(encoder=nacl.encoding.RawEncoder) + \
- server_ephem_key.public_key.encode(encoder=nacl.encoding.RawEncoder)
- A = nacl.hash.sha256(M + b'verify', encoder=nacl.encoding.RawEncoder)
- S = nacl.hash.sha256(M + b'secret', encoder=nacl.encoding.RawEncoder)
- if sphinx_domainsep is not None:
- blindkey = Sphinx.makeblindkey(S, sphinx_domainsep, perfstats)
- blinded_client_pubkey = Sphinx.reblindpubkey(blindkey,
- client_pubkey, perfstats)
- return ((bytes(server_ephem_key.public_key),
- bytes(onion_privkey.public_key), A),
- S), blinded_client_pubkey
- else:
- return ((bytes(server_ephem_key.public_key),
- bytes(onion_privkey.public_key), A), S)
- def verify(self, reply, onion_pubkey, idpubkey, sphinx_domainsep=None):
- """The client calls this method to verify the ntor reply
- message, passing the onion and id public keys for the server
- it's expecting to be talking to. If sphinx_domainsep is not
- None, also compute the reblinding key so that the client can
- reuse this same NTor object for the next server. Returns the
- shared secret on success, or raises ValueError on failure."""
- server_ephem_pubkey, server_onion_pubkey, authtag = reply
- if type(idpubkey) is not bytes:
- idpubkey = bytes(idpubkey)
- if type(server_ephem_pubkey) is bytes:
- server_ephem_pubkey = nacl.public.PublicKey(server_ephem_pubkey)
- if type(server_onion_pubkey) is bytes:
- server_onion_pubkey = nacl.public.PublicKey(server_onion_pubkey)
- if type(onion_pubkey) is bytes:
- onion_pubkey = nacl.public.PublicKey(onion_pubkey)
- if onion_pubkey != server_onion_pubkey:
- raise ValueError("NTor onion pubkey mismatch")
- # We use the blinding keys if present; if they're not present
- # (because we're not in Single-Pass Walking Onions), the loops
- # are just empty anyway, so everything will work in the usual
- # unblinded way.
- reblinded_server_ephem_pubkey = server_ephem_pubkey
- for blindkey in self.blinding_keys:
- reblinded_server_ephem_pubkey = Sphinx.reblindpubkey(blindkey,
- reblinded_server_ephem_pubkey, self.perfstats)
- xykey = nacl.public.Box(self.client_ephem_key,
- reblinded_server_ephem_pubkey).shared_key()
- reblinded_onion_pubkey = onion_pubkey
- for blindkey in self.blinding_keys:
- reblinded_onion_pubkey = Sphinx.reblindpubkey(blindkey,
- reblinded_onion_pubkey, self.perfstats)
- xbkey = nacl.public.Box(self.client_ephem_key,
- reblinded_onion_pubkey).shared_key()
- self.perfstats.dhs += 2
- M = xykey + xbkey + \
- idpubkey + \
- onion_pubkey.encode(encoder=nacl.encoding.RawEncoder) + \
- server_ephem_pubkey.encode(encoder=nacl.encoding.RawEncoder)
- Acheck = nacl.hash.sha256(M + b'verify', encoder=nacl.encoding.RawEncoder)
- S = nacl.hash.sha256(M + b'secret', encoder=nacl.encoding.RawEncoder)
- if Acheck != authtag:
- raise ValueError("NTor auth mismatch")
- if sphinx_domainsep is not None:
- blindkey = Sphinx.makeblindkey(S, sphinx_domainsep,
- self.perfstats)
- self.blinding_keys.append(blindkey)
- return S
- class VanillaExtendCircuitHandler:
- """A handler for VanillaExtendCircuitCell cells. It allocates a new
- circuit id on the Channel to the requested next hop, connects the
- existing and new circuits together, and forwards a
- VanillaCreateCircuitMsg to the next hop."""
- def received_cell(self, circhandler, cell):
- # Remove ourselves from handling a second
- # VanillaExtendCircuitCell on this circuit
- circhandler.replace_celltype_handler(VanillaExtendCircuitCell, None)
- # Allocate a new circuit id to the requested next hop
- channelmgr = circhandler.channel.channelmgr
- nexthopchannel = channelmgr.get_channel_to(cell.hopaddr)
- newcircid, newcirchandler = nexthopchannel.new_circuit()
- # Connect the existing and new circuits together
- circhandler.adjacent_circuit_handler = newcirchandler
- newcirchandler.adjacent_circuit_handler = circhandler
- # Set up a handler for when the VanillaCreatedCircuitCell comes
- # back
- newcirchandler.replace_celltype_handler(
- VanillaCreatedCircuitCell,
- VanillaCreatedRelayHandler())
- # Forward a VanillaCreateCircuitMsg to the next hop
- nexthopchannel.send_msg(
- VanillaCreateCircuitMsg(newcircid, cell.ntor_request))
- class TelescopingExtendCircuitHandler:
- """A handler for TelescopingExtendCircuitCell cells. It allocates a new
- circuit id on the Channel to the requested next hop, connects the
- existing and new circuits together, and forwards a
- TelescopingCreateCircuitMsg to the next hop."""
- def __init__(self, relaypicker, current_relay_idkey):
- self.relaypicker = relaypicker
- self.current_relay_idkey = bytes(current_relay_idkey)
- def received_cell(self, circhandler, cell):
- # Remove ourselves from handling a second
- # TelescopingExtendCircuitCell on this circuit
- circhandler.replace_celltype_handler(TelescopingExtendCircuitCell, None)
- # Find the SNIP corresponding to the index sent by the client
- next_snip = self.relaypicker.pick_relay_by_uniform_index(cell.idx)
- # Check to make sure that we aren't extending to ourselves. If we are,
- # close the circuit.
- if next_snip.snipdict["idkey"] == self.current_relay_idkey:
- logging.debug("Client requested extending the circuit to a relay already in the path; aborting. my circid: %s", str(circhandler.circid))
- circhandler.close()
- return
- # Allocate a new circuit id to the requested next hop
- channelmgr = circhandler.channel.channelmgr
- nexthopchannel = channelmgr.get_channel_to(next_snip.snipdict["addr"])
- newcircid, newcirchandler = nexthopchannel.new_circuit()
- # Connect the existing and new circuits together
- circhandler.adjacent_circuit_handler = newcirchandler
- newcirchandler.adjacent_circuit_handler = circhandler
- # Set up a handler for when the TelescopingCreatedCircuitCell comes
- # back
- newcirchandler.replace_celltype_handler(
- TelescopingCreatedCircuitCell,
- TelescopingCreatedRelayHandler(next_snip))
- # Forward a TelescopingCreateCircuitMsg to the next hop
- nexthopchannel.send_msg(
- TelescopingCreateCircuitMsg(newcircid, cell.ntor_request))
- class VanillaCreatedRelayHandler:
- """Handle a VanillaCreatedCircuitCell received by a _relay_ that
- recently received a VanillaExtendCircuitCell from a client, and so
- forwarded a VanillaCreateCircuitCell to the next hop."""
- def received_cell(self, circhandler, cell):
- # Remove ourselves from handling a second
- # VanillaCreatedCircuitCell on this circuit
- circhandler.replace_celltype_handler(VanillaCreatedCircuitCell, None)
- # Just forward a VanillaExtendedCircuitCell back towards the
- # client
- circhandler.adjacent_circuit_handler.send_cell(
- VanillaExtendedCircuitCell(cell.ntor_reply))
- class TelescopingCreatedRelayHandler:
- """Handle a TelescopingCreatedCircuitCell received by a _relay_ that
- recently received a TelescopingExtendCircuitCell from a client, and so
- forwarded a TelescopingCreateCircuitCell to the next hop."""
- def __init__(self, next_snip):
- self.next_snip = next_snip
- def received_cell(self, circhandler, cell):
- logging.debug("Handle a TelescopingCreatedCircuit received by a relay")
- # Remove ourselves from handling a second
- # TelescopingCreatedCircuitCell on this circuit
- circhandler.replace_celltype_handler(TelescopingCreatedCircuitCell, None)
- # Just forward a TelescopingExtendedCircuitCell back towards the
- # client
- circhandler.adjacent_circuit_handler.send_cell(
- TelescopingExtendedCircuitCell(cell.ntor_reply, self.next_snip))
- class SinglePassCreatedRelayHandler:
- """Handle a SinglePassCreatedCircuitCell received by a _relay_ that
- recently received a SinglePassCreateCircuitMsg from this relay."""
- def __init__(self, ntorreply, next_snip, vrf_output, createdkey):
- self.ntorreply = ntorreply
- self.next_snip = next_snip
- self.vrf_output = vrf_output
- self.createdkey = createdkey
- def received_cell(self, circhandler, cell):
- logging.debug("Handle a SinglePassCreatedCircuitCell received by a relay")
- # Remove ourselves from handling a second
- # SinglePassCreatedCircuitCell on this circuit
- circhandler.replace_celltype_handler(SinglePassCreatedCircuitCell, None)
- logging.debug("Sending a SinglePassCreatedCell after receiving one from %s", self.next_snip.snipdict['addr'])
- # Forward a SinglePassCreatedCircuitCell back towards the client
- circhandler.adjacent_circuit_handler.channel_send_cell(
- SinglePassCreatedCircuitCell(self.ntorreply,
- SinglePassCreatedEnc(self.createdkey, self.next_snip,
- self.vrf_output, cell)))
- class CircuitHandler:
- """A class for managing sending and receiving encrypted cells on a
- particular circuit."""
- class NoCryptLayer:
- def encrypt_msg(self, msg): return msg
- def decrypt_msg(self, msg): return msg
- class CryptLayer:
- def __init__(self, enckey, deckey, next_layer):
- self.enckey = enckey
- self.deckey = deckey
- self.next_layer = next_layer
- def encrypt_msg(self, msg):
- return self.next_layer.encrypt_msg(EncryptedCell(self.enckey, msg))
- def decrypt_msg(self, msg):
- return self.next_layer.decrypt_msg(msg).decrypt(self.deckey)
- def __init__(self, channel, circid):
- self.channel = channel
- self.circid = circid
- # The list of relay descriptors that form the circuit so far
- # (client side only)
- self.circuit_descs = []
- # The dispatch table is indexed by type, and the values are
- # objects with received_cell(circhandler, cell) methods.
- self.cell_dispatch_table = dict()
- # The topmost crypt layer. This is an object with
- # encrypt_msg(msg) and decrypt_msg(msg) methods that returns the
- # en/decrypted messages respectively. Messages are encrypted
- # starting with the last layer that was added (the keys for the
- # furthest away relay in the circuit) and are decrypted starting
- # with the first layer that was added (the keys for the guard).
- self.crypt_layer = self.NoCryptLayer()
- # The adjacent CircuitHandler that's connected to this one. If
- # we get a cell on one, we forward it to the other (if it's not
- # meant for us to handle directly)
- self.adjacent_circuit_handler = None
- # The function to call when this circuit closes
- self.closer = lambda: self.channel.circuithandlers.pop(circid)
- def close(self):
- """Close the circuit. Sends a CloseCell on the circuit (and its
- adjacent circuit, if present) and closes both."""
- adjcirchandler = self.adjacent_circuit_handler
- self.adjacent_circuit_handler = None
- logging.debug("Closing circuit. circid: %s", str(self.circid))
- if adjcirchandler is not None:
- adjcirchandler.adjacent_circuit_handler = None
- self.closer()
- self.channel_send_cell(CloseCell())
- if adjcirchandler is not None:
- adjcirchandler.closer()
- adjcirchandler.channel_send_cell(CloseCell())
- def send_cell(self, cell):
- """Send a cell on this circuit, encrypting as needed."""
- self.channel_send_cell(self.crypt_layer.encrypt_msg(cell))
- def channel_send_cell(self, cell):
- """Send a cell on this circuit directly without encrypting it
- first."""
- self.channel.send_msg(CircuitCellMsg(self.circid, cell))
- def received_cell(self, cell):
- """A cell has been received on this circuit. Dispatch it
- according to its type."""
- if isinstance(cell, EncryptedCell):
- cell = self.crypt_layer.decrypt_msg(cell)
- logging.debug("CircuitHandler: %s received cell %s on circuit %d from %s" % (self.channel.channelmgr.myaddr, cell, self.circid, self.channel.peer.channelmgr.myaddr))
- # If it's still encrypted, it's for sure meant for forwarding to
- # our adjacent hop, which had better exist.
- if isinstance(cell, EncryptedCell):
- self.adjacent_circuit_handler.send_cell(cell)
- else:
- # This is a plaintext cell meant for us. Handle it
- # according to the table.
- celltype = type(cell)
- if celltype in self.cell_dispatch_table:
- self.cell_dispatch_table[celltype].received_cell(self, cell)
- elif isinstance(cell, StringCell):
- # Default handler; just print the message in the cell
- logging.debug("CircuitHandler: %s received '%s' on circuit %d from %s" \
- % (self.channel.channelmgr.myaddr, cell,
- self.circid, self.channel.peer.channelmgr.myaddr))
- elif isinstance(cell, CloseCell):
- logging.debug("Received CloseCell on circuit %s", str(self.circid))
- # Forward the CloseCell (without encryption) to the
- # adjacent circuit, if any, and close both this and the
- # adjacent circuit
- adjcirchandler = self.adjacent_circuit_handler
- self.adjacent_circuit_handler = None
- if adjcirchandler is not None:
- adjcirchandler.adjacent_circuit_handler = None
- self.closer()
- if adjcirchandler is not None:
- adjcirchandler.closer()
- adjcirchandler.channel_send_cell(cell)
- else:
- # I don't know how to handle this cell?
- raise ValueError("CircuitHandler: %s received unknown cell type %s on circuit %d from %s" \
- % (self.channel.channelmgr.myaddr, cell,
- self.circid, self.channel.peer.channelmgr.myaddr))
- def replace_celltype_handler(self, celltype, handler):
- """Add an object with a received_cell(circhandler, cell) method
- to the cell dispatch table. It replaces anything that's already
- there. Passing None as the handler removes the dispatcher for
- that cell type."""
- if handler is None:
- del self.cell_dispatch_table[celltype]
- else:
- self.cell_dispatch_table[celltype] = handler
- def add_crypt_layer(self, enckey, deckey):
- """Add a processing layer to this CircuitHandler so that cells
- we send will get encrypted with the first given key, and cells
- we receive will be decrypted with the other given key."""
- current_crypt_layer = self.crypt_layer
- self.crypt_layer = self.CryptLayer(enckey, deckey, current_crypt_layer)
- class Channel(network.Connection):
- """A class representing a channel between a relay and either a
- client or a relay, transporting cells from various circuits."""
- def __init__(self):
- super().__init__()
- # The RelayChannelManager managing this Channel
- self.channelmgr = None
- # The Channel at the other end
- self.peer = None
- # The function to call when the connection closes
- self.closer = lambda: None
- # The next circuit id to use on this channel. The party that
- # opened the channel uses even numbers; the receiving party uses
- # odd numbers.
- self.next_circid = None
- # A map for CircuitHandlers to use for each open circuit on the
- # channel
- self.circuithandlers = dict()
- def closed(self):
- # Close each circuithandler we're managing
- while self.circuithandlers:
- chitems = iter(self.circuithandlers.items())
- circid, circhandler = next(chitems)
- logging.debug('closing circuit %s', circid)
- circhandler.close()
- self.closer()
- self.peer = None
- def close(self):
- peer = self.peer
- self.closed()
- if peer is not None and peer is not self:
- peer.closed()
- def new_circuit(self):
- """Allocate a new circuit on this channel, returning the new
- circuit's id and the new CircuitHandler."""
- circid = self.next_circid
- self.next_circid += 2
- circuithandler = CircuitHandler(self, circid)
- self.circuithandlers[circid] = circuithandler
- return circid, circuithandler
- def is_circuit_open(self, circid):
- is_open = (circid in self.circuithandlers) and (self.circuithandlers[circid] is not None)
- return is_open
- def new_circuit_with_circid(self, circid):
- """Allocate a new circuit on this channel, with the circuit id
- received from our peer. Return the new CircuitHandler"""
- circuithandler = CircuitHandler(self, circid)
- self.circuithandlers[circid] = circuithandler
- return circuithandler
- def send_cell(self, circid, cell):
- """Send the given message on the given circuit, encrypting or
- decrypting as needed."""
- self.circuithandlers[circid].send_cell(cell)
- def send_raw_cell(self, circid, cell):
- """Send the given message, tagged for the given circuit id. No
- encryption or decryption is done."""
- self.send_msg(CircuitCellMsg(self.circid, self.cell))
- def send_msg(self, msg):
- """Send the given NetMsg on the channel."""
- self.channelmgr.perfstats.bytes_sent += msg.size()
- self.peer.received(self.channelmgr.myaddr, msg)
- def received(self, peeraddr, msg):
- """Callback when a message is received from the network."""
- logging.debug('Channel: %s received %s from %s' % (self.channelmgr.myaddr, msg, peeraddr))
- self.channelmgr.perfstats.bytes_received += msg.size()
- if isinstance(msg, CircuitCellMsg):
- circid, cell = msg.circid, msg.cell
- self.circuithandlers[circid].received_cell(cell)
- else:
- self.channelmgr.received_msg(msg, peeraddr, self)
- class ChannelManager:
- """The class that manages the channels to other relays and clients.
- Relays and clients both use subclasses of this class to both create
- on-demand channels to relays, to gracefully handle the closing of
- channels, and to handle commands received over the channels."""
- def __init__(self, myaddr, dirauthaddrs, perfstats):
- # A dictionary of Channels to other hosts, indexed by NetAddr
- self.channels = dict()
- self.myaddr = myaddr
- self.dirauthaddrs = dirauthaddrs
- self.consensus = None
- self.relaypicker = None
- self.perfstats = perfstats
- def terminate(self):
- """Close all connections we're managing."""
- while self.channels:
- channelitems = iter(self.channels.items())
- addr, channel = next(channelitems)
- logging.debug('closing channel %s %s', addr, channel)
- channel.close()
- def add_channel(self, channel, peeraddr):
- """Add the given channel to the list of channels we are
- managing. If we are already managing a channel to the same
- peer, close it first."""
- if peeraddr in self.channels:
- self.channels[peeraddr].close()
- channel.channelmgr = self
- self.channels[peeraddr] = channel
- channel.closer = lambda: self.channels.pop(peeraddr)
- def get_channel_to(self, addr):
- """Get the Channel connected to the given NetAddr, creating one
- if none exists right now."""
- if addr in self.channels:
- return self.channels[addr]
- # Create the new channel
- logging.debug('getting channel from %s to %s',self.myaddr,addr)
- newchannel = network.thenetwork.connect(self.myaddr, addr,
- self.perfstats)
- logging.debug('got channel from %s to %s',self.myaddr,addr)
- self.channels[addr] = newchannel
- newchannel.closer = lambda: self.channels.pop(addr)
- newchannel.channelmgr = self
- return newchannel
- def received_msg(self, msg, peeraddr, channel):
- """Callback when a NetMsg not specific to a circuit is
- received."""
- logging.debug("ChannelManager: Node %s received msg %s from %s" % (self.myaddr, msg, peeraddr))
- def received_cell(self, circid, cell, peeraddr, channel):
- """Callback with a circuit-specific cell is received."""
- logging.debug("ChannelManager: Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
- def send_msg(self, msg, peeraddr):
- """Send a message to the peer with the given address."""
- channel = self.get_channel_to(peeraddr)
- channel.send_msg(msg)
- def send_cell(self, circid, cell, peeraddr):
- """Send a cell on the given circuit to the peer with the given
- address."""
- channel = self.get_channel_to(peeraddr)
- channel.send_cell(circid, cell)
- class RelayChannelManager(ChannelManager):
- """The subclass of ChannelManager for relays."""
- def __init__(self, myaddr, dirauthaddrs, onionprivkey, idpubkey,
- desc_getter, path_selection_key_getter, perfstats):
- super().__init__(myaddr, dirauthaddrs, perfstats)
- self.onionkey = onionprivkey
- self.idpubkey = idpubkey
- if network.thenetwork.womode != network.WOMode.VANILLA:
- self.endive = None
- self.desc_getter = desc_getter
- if network.thenetwork.womode == network.WOMode.SINGLEPASS:
- self.path_selection_key_getter = path_selection_key_getter
- def get_consensus(self):
- """Download a fresh consensus (and ENDIVE if using Walking
- Onions) from a random dirauth."""
- a = random.choice(self.dirauthaddrs)
- c = network.thenetwork.connect(self, a, self.perfstats)
- if network.thenetwork.womode == network.WOMode.VANILLA:
- if self.consensus is not None and \
- len(self.consensus.consdict['relays']) > 0:
- self.consensus = c.getconsensusdiff()
- else:
- self.consensus = c.getconsensus()
- self.relaypicker = dirauth.Consensus.verify(self.consensus,
- network.thenetwork.dirauthkeys(), self.perfstats)
- else:
- self.consensus = c.getconsensus()
- if self.endive is not None and \
- len(self.endive.enddict['snips']) > 0:
- self.endive = c.getendivediff()
- else:
- self.endive = c.getendive()
- self.relaypicker = dirauth.ENDIVE.verify(self.endive,
- self.consensus, network.thenetwork.dirauthkeys(),
- self.perfstats)
- c.close()
- def received_msg(self, msg, peeraddr, channel):
- """Callback when a NetMsg not specific to a circuit is
- received."""
- logging.debug("RelayChannelManager: Node %s received msg %s from %s" % (self.myaddr, msg, peeraddr))
- if isinstance(msg, RelayRandomHopMsg):
- if msg.ttl > 0:
- # Pick a random next hop from the consensus
- nexthop = self.relaypicker.pick_weighted_relay()
- if network.thenetwork.womode == network.WOMode.VANILLA:
- nextaddr = nexthop.descdict["addr"]
- else:
- nextaddr = nexthop.snipdict["addr"]
- self.send_msg(RelayRandomHopMsg(msg.ttl-1), nextaddr)
- elif isinstance(msg, RelayGetConsensusMsg):
- self.send_msg(RelayConsensusMsg(self.consensus), peeraddr)
- elif isinstance(msg, RelayGetConsensusDiffMsg):
- self.send_msg(RelayConsensusDiffMsg(self.consensus), peeraddr)
- elif isinstance(msg, RelayGetDescMsg):
- self.send_msg(RelayDescMsg(self.desc_getter()), peeraddr)
- elif isinstance(msg, VanillaCreateCircuitMsg):
- # A new circuit has arrived
- circhandler = channel.new_circuit_with_circid(msg.circid)
- # Create the ntor reply
- reply, secret = NTor.reply(self.onionkey, self.idpubkey,
- msg.ntor_request, self.perfstats)
- # Set up the circuit to use the shared secret
- enckey = nacl.hash.sha256(secret + b'downstream')
- deckey = nacl.hash.sha256(secret + b'upstream')
- circhandler.add_crypt_layer(enckey, deckey)
- # Add a handler for if an Extend Cell arrives (there should
- # be at most one on this circuit).
- circhandler.replace_celltype_handler(
- VanillaExtendCircuitCell, VanillaExtendCircuitHandler())
- # Send the ntor reply
- self.send_msg(CircuitCellMsg(msg.circid,
- VanillaCreatedCircuitCell(reply)), peeraddr)
- elif isinstance(msg, TelescopingCreateCircuitMsg):
- # A new circuit has arrived
- circhandler = channel.new_circuit_with_circid(msg.circid)
- # Create the ntor reply
- reply, secret = NTor.reply(self.onionkey, self.idpubkey,
- msg.ntor_request, self.perfstats)
- # Set up the circuit to use the shared secret
- enckey = nacl.hash.sha256(secret + b'downstream')
- deckey = nacl.hash.sha256(secret + b'upstream')
- circhandler.add_crypt_layer(enckey, deckey)
- # Add a handler for if an Extend Cell arrives (there should
- # be at most one on this circuit).
- circhandler.replace_celltype_handler(
- TelescopingExtendCircuitCell,
- TelescopingExtendCircuitHandler(self.relaypicker,
- self.idpubkey))
- # Send the ntor reply
- self.send_msg(CircuitCellMsg(msg.circid,
- TelescopingCreatedCircuitCell(reply)), peeraddr)
- elif isinstance(msg, SinglePassCreateCircuitMsg) and msg.ttl == 0:
- # we are the end of the circuit, just establish a shared key and
- # return
- logging.debug("RelayChannelManager: Single-Pass TTL is 0, replying without extending")
- # A new circuit has arrived
- circhandler = channel.new_circuit_with_circid(msg.circid)
- # Create the ntor reply
- reply, secret = NTor.reply(self.onionkey, self.idpubkey,
- msg.ntor_request, self.perfstats)
- # Set up the circuit to use the shared secret
- enckey = nacl.hash.sha256(secret + b'downstream')
- deckey = nacl.hash.sha256(secret + b'upstream')
- circhandler.add_crypt_layer(enckey, deckey)
- # Send the ntor reply, but no need to send the snip for the next
- # relay or vrf proof, as this is the last relay in the circuit.
- self.send_msg(CircuitCellMsg(msg.circid,
- SinglePassCreatedCircuitCell(reply, None)), peeraddr)
- elif isinstance(msg, SinglePassCreateCircuitMsg) and msg.ttl > 0:
- logging.debug("RelayChannelManager: Single-Pass TTL is greater than 0; extending")
- # A new circuit has arrived
- circhandler = channel.new_circuit_with_circid(msg.circid)
- # Create the ntor reply for the circuit-extension key, and derive
- # the client's next blinded key
- (ntorreply, secret), blinded_client_encr_key = \
- NTor.reply(self.onionkey, self.idpubkey,
- msg.ntor_request, self.perfstats, b'circuit')
- # Set up the circuit to use the shared secret established from the
- # circuit extension key
- enckey = nacl.hash.sha256(secret + b'downstream')
- deckey = nacl.hash.sha256(secret + b'upstream')
- createdkey = nacl.hash.sha256(secret + b'created')
- # Here, we will directly extend the circuit ourselves, after
- # determining the next relay using the client's path selection
- # key in conjunction with our own
- pathsel_rand, blinded_client_path_selection_key = \
- Sphinx.server(nacl.public.PublicKey(msg.clipathselkey),
- self.onionkey, b'pathsel', False, self.perfstats)
- pathselkey = self.path_selection_key_getter()
- # Simulate the VRF output for now (but it has the right
- # size, and charges the right number of group operations to
- # the perfstats)
- vrf_output = VRF.get_output(pathselkey, pathsel_rand,
- self.perfstats)
- index = int.from_bytes(vrf_output[0][:4], 'big', signed=False)
- next_hop = self.relaypicker.pick_relay_by_uniform_index(index)
- if next_hop == None:
- logging.debug("Client requested extending the circuit to a relay index that results in None, aborting. my circid: %s", str(circhandler.circid))
- circhandler.close()
- elif next_hop.snipdict["idkey"] == bytes(self.idpubkey):
- logging.debug("Client requested extending the circuit to a relay already in the path; aborting. my circid: %s", str(circhandler.circid))
- circhandler.close()
- return
- # Allocate a new circuit id to the requested next hop
- channelmgr = circhandler.channel.channelmgr
- nexthopchannel = channelmgr.get_channel_to(next_hop.snipdict["addr"])
- newcircid, newcirchandler = nexthopchannel.new_circuit()
- # Connect the existing and new circuits together
- circhandler.adjacent_circuit_handler = newcirchandler
- newcirchandler.adjacent_circuit_handler = circhandler
- # Add a handler for once the next relay replies to say that the
- # circuit has been created
- newcirchandler.replace_celltype_handler(
- SinglePassCreatedCircuitCell,
- SinglePassCreatedRelayHandler(ntorreply, next_hop,
- vrf_output, createdkey))
- # Send the next create message to the next hop
- nexthopchannel.send_msg(SinglePassCreateCircuitMsg(newcircid,
- blinded_client_encr_key, blinded_client_path_selection_key,
- msg.ttl-1))
- # Now set up the crypto
- circhandler.add_crypt_layer(enckey, deckey)
- else:
- return super().received_msg(msg, peeraddr, channel)
- def received_cell(self, circid, cell, peeraddr, channel):
- """Callback with a circuit-specific cell is received."""
- logging.debug("RelayChannelManager: Node %s received cell on circ %d: %s from %s" % (self.myaddr, circid, cell, peeraddr))
- return super().received_cell(circid, cell, peeraddr, channel)
- class Relay(network.Server):
- """The class representing an onion relay."""
- def __init__(self, dirauthaddrs, bw, flags):
- # Gather performance statistics
- self.perfstats = network.PerfStats(network.EntType.RELAY, bw)
- self.perfstats.is_bootstrapping = True
- # Create the identity and onion keys
- self.idkey = nacl.signing.SigningKey.generate()
- self.onionkey = nacl.public.PrivateKey.generate()
- self.perfstats.keygens += 2
- self.name = self.idkey.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode("ascii")
- # Bind to the network to get a network address
- self.netaddr = network.thenetwork.bind(self)
- self.perfstats.name = "Relay at %s" % self.netaddr
- # Our bandwidth and flags
- self.bw = bw
- self.flags = flags
- # Register for epoch change notification
- network.thenetwork.wantepochticks(self, True, priority=True, end=True)
- network.thenetwork.wantepochticks(self, True, priority=True)
- self.current_desc = None
- self.next_desc = None
- # Create the path selection key for Single-Pass Walking Onions
- if network.thenetwork.womode == network.WOMode.SINGLEPASS:
- self.path_selection_key = nacl.public.PrivateKey.generate()
- self.next_path_selection_key = self.path_selection_key
- self.perfstats.keygens += 1
- else:
- self.path_selection_key = None
- # Create the RelayChannelManager connection manager
- self.channelmgr = RelayChannelManager(self.netaddr, dirauthaddrs,
- self.onionkey, self.idkey.verify_key,
- lambda: self.current_desc, lambda: self.path_selection_key,
- self.perfstats)
- # Initially, we're not a fallback relay
- self.is_fallbackrelay = False
- self.uploaddesc()
- def terminate(self):
- """Stop this relay."""
- if self.is_fallbackrelay:
- # Fallback relays must not (for now) terminate
- raise RelayFallbackTerminationError(self)
- # Stop listening for epoch ticks
- network.thenetwork.wantepochticks(self, False, priority=True, end=True)
- network.thenetwork.wantepochticks(self, False, priority=True)
- # Tell the dirauths we're going away
- self.uploaddesc(False)
- # Close connections to other relays
- self.channelmgr.terminate()
- # Stop listening to our own bound port
- self.close()
- def set_is_fallbackrelay(self, isfallback = True):
- """Set this relay to be a fallback relay (or unset if passed
- False)."""
- self.is_fallbackrelay = isfallback
- def epoch_ending(self, epoch):
- # Download the new consensus, which will have been created
- # already since the dirauths' epoch_ending callbacks happened
- # before the relays'.
- self.channelmgr.get_consensus()
- def newepoch(self, epoch):
- # Rotate the path selection key for Single-Pass Walking Onions
- if network.thenetwork.womode == network.WOMode.SINGLEPASS:
- self.path_selection_key = self.next_path_selection_key
- self.next_path_selection_key = nacl.public.PrivateKey.generate()
- self.perfstats.keygens += 1
- # Upload the descriptor for the *next* epoch (the one after the
- # one that just started)
- self.uploaddesc()
- def uploaddesc(self, upload=True):
- # Upload the descriptor for the epoch to come, or delete a
- # previous upload if upload=False
- descdict = dict();
- descdict["epoch"] = network.thenetwork.getepoch() + 1
- descdict["idkey"] = bytes(self.idkey.verify_key)
- descdict["onionkey"] = bytes(self.onionkey.public_key)
- descdict["addr"] = self.netaddr
- descdict["bw"] = self.bw
- descdict["flags"] = self.flags
- if network.thenetwork.womode == network.WOMode.SINGLEPASS:
- descdict["pathselkey"] = \
- bytes(self.next_path_selection_key.public_key)
- desc = dirauth.RelayDescriptor(descdict)
- desc.sign(self.idkey, self.perfstats)
- dirauth.RelayDescriptor.verify(desc, self.perfstats)
- self.current_desc = self.next_desc
- self.next_desc = desc
- if upload:
- descmsg = dirauth.DirAuthUploadDescMsg(desc)
- else:
- # Note that this relies on signatures being deterministic;
- # otherwise we'd need to save the descriptor we uploaded
- # before so we could tell the airauths to delete the exact
- # one
- descmsg = dirauth.DirAuthDelDescMsg(desc)
- # Upload them
- for a in self.channelmgr.dirauthaddrs:
- c = network.thenetwork.connect(self, a, self.perfstats)
- c.sendmsg(descmsg)
- c.close()
- def connected(self, peer):
- """Callback invoked when someone (client or relay) connects to
- us. Create a pair of linked Channels and return the peer half
- to the peer."""
- # Create the linked pair
- if peer is self.netaddr:
- # A self-loop? We'll allow it.
- peerchannel = Channel()
- peerchannel.peer = peerchannel
- peerchannel.next_circid = 2
- return peerchannel
- peerchannel = Channel()
- ourchannel = Channel()
- peerchannel.peer = ourchannel
- peerchannel.next_circid = 2
- ourchannel.peer = peerchannel
- ourchannel.next_circid = 1
- # Add our channel to the RelayChannelManager
- self.channelmgr.add_channel(ourchannel, peer)
- return peerchannel
- if __name__ == '__main__':
- perfstats = network.PerfStats(network.EntType.NONE)
- # Initialize the (non-cryptographic) random seed
- random.seed(1)
- if len(sys.argv) < 3:
- print("Must pass in network mode and snip auth mode!")
- print("Network options are vanilla, telescoping, or single-pass.")
- print("SNIP auth options are merkle or threshold.")
- sys.exit(0)
- network_mode = network.WOMode.string_to_type(sys.argv[1])
- if network_mode == -1:
- print("Not a valid network mode: " + network_mode)
- sys.exit(0)
- snipauth_mode = network.SNIPAuthMode.string_to_type(sys.argv[2])
- if network_mode == -1:
- print("Not a valid SNIP authentication mode: " + snipauth_mode)
- sys.exit(0)
- if network_mode == network.WOMode.VANILLA:
- network.thenetwork.set_wo_style(network.WOMode.VANILLA,
- network.SNIPAuthMode.NONE)
- elif network_mode == network.WOMode.TELESCOPING:
- if snipauth_mode == network.SNIPAuthMode.MERKLE:
- network.thenetwork.set_wo_style(network.WOMode.TELESCOPING,
- network.SNIPAuthMode.MERKLE)
- else:
- network.thenetwork.set_wo_style(network.WOMode.TELESCOPING,
- network.SNIPAuthMode.THRESHSIG)
- elif network_mode == network.WOMode.SINGLEPASS:
- if snipauth_mode == network.SNIPAuthMode.MERKLE:
- network.thenetwork.set_wo_style(network.WOMode.SINGLEPASS,
- network.SNIPAuthMode.MERKLE)
- else:
- network.thenetwork.set_wo_style(network.WOMode.SINGLEPASS,
- network.SNIPAuthMode.THRESHSIG)
- else:
- sys.exit("Received unsupported network mode, exiting.")
- # Start some dirauths
- numdirauths = 9
- dirauthaddrs = []
- for i in range(numdirauths):
- dira = dirauth.DirAuth(i, numdirauths)
- dirauthaddrs.append(dira.netaddr)
- # Start some relays
- numrelays = 10
- relays = []
- for i in range(numrelays):
- # Relay bandwidths (at least the ones fast enough to get used)
- # in the live Tor network (as of Dec 2019) are well approximated
- # by (200000-(200000-25000)/3*log10(x)) where x is a
- # uniform integer in [1,2500]
- x = random.randint(1,2500)
- bw = int(200000-(200000-25000)/3*math.log10(x))
- relays.append(Relay(dirauthaddrs, bw, 0))
- # The fallback relays are a hardcoded list of about 5% of the
- # relays, used by clients for bootstrapping
- numfallbackrelays = int(numrelays * 0.05) + 1
- fallbackrelays = random.sample(relays, numfallbackrelays)
- for r in fallbackrelays:
- r.set_is_fallbackrelay()
- network.thenetwork.setfallbackrelays(fallbackrelays)
- # Tick the epoch
- network.thenetwork.nextepoch()
- print(dirauth.DirAuth.consensus)
- print(dirauth.DirAuth.endive)
- if network.thenetwork.womode == network.WOMode.VANILLA:
- relaypicker = dirauth.Consensus.verify(dirauth.DirAuth.consensus,
- network.thenetwork.dirauthkeys(), perfstats)
- else:
- relaypicker = dirauth.ENDIVE.verify(dirauth.DirAuth.endive,
- dirauth.DirAuth.consensus,
- network.thenetwork.dirauthkeys(), perfstats)
- if network.thenetwork.snipauthmode == \
- network.SNIPAuthMode.THRESHSIG:
- for s in dirauth.DirAuth.endive.enddict['snips']:
- dirauth.SNIP.verify(s, dirauth.DirAuth.consensus,
- network.thenetwork.dirauthkeys()[0], perfstats)
- print('ticked; epoch=', network.thenetwork.getepoch())
- relays[3].channelmgr.send_msg(RelayRandomHopMsg(30), relays[5].netaddr)
- # See what channels exist and do a consistency check
- for r in relays:
- print("%s: %s" % (r.netaddr, [ str(k) for k in r.channelmgr.channels.keys()]))
- raddr = r.netaddr
- for ad, ch in r.channelmgr.channels.items():
- if ch.peer.channelmgr.myaddr != ad:
- print('address mismatch:', raddr, ad, ch.peer.channelmgr.myaddr)
- if ch.peer.channelmgr.channels[raddr].peer is not ch:
- print('asymmetry:', raddr, ad, ch, ch.peer.channelmgr.channels[raddr].peer)
- # Stop some relays
- relays[3].terminate()
- del relays[3]
- relays[5].terminate()
- del relays[5]
- relays[7].terminate()
- del relays[7]
- # Tick the epoch
- network.thenetwork.nextepoch()
- print(dirauth.DirAuth.consensus)
- print(dirauth.DirAuth.endive)
- # See what channels exist and do a consistency check
- for r in relays:
- print("%s: %s" % (r.netaddr, [ str(k) for k in r.channelmgr.channels.keys()]))
- raddr = r.netaddr
- for ad, ch in r.channelmgr.channels.items():
- if ch.peer.channelmgr.myaddr != ad:
- print('address mismatch:', raddr, ad, ch.peer.channelmgr.myaddr)
- if ch.peer.channelmgr.channels[raddr].peer is not ch:
- print('asymmetry:', raddr, ad, ch, ch.peer.channelmgr.channels[raddr].peer)
- channel = relays[3].channelmgr.get_channel_to(relays[5].netaddr)
- circid, circhandler = channel.new_circuit()
- peerchannel = relays[5].channelmgr.get_channel_to(relays[3].netaddr)
- peerchannel.new_circuit_with_circid(circid)
- relays[3].channelmgr.send_cell(circid, StringCell("test"), relays[5].netaddr)
- idpubkey = relays[1].idkey.verify_key
- onionpubkey = relays[1].onionkey.public_key
- nt = NTor(perfstats)
- req = nt.request()
- R, S = NTor.reply(relays[1].onionkey, idpubkey, req, perfstats)
- S2 = nt.verify(R, onionpubkey, idpubkey)
- print(S == S2)
- # Test the Sphinx class: DH version (for the path selection keys)
- server1_key = nacl.public.PrivateKey.generate()
- server2_key = nacl.public.PrivateKey.generate()
- server3_key = nacl.public.PrivateKey.generate()
- client_key = nacl.public.PrivateKey.generate()
- # Check that normal DH is working
- ckey = nacl.public.Box(client_key, server1_key.public_key).shared_key()
- skey = nacl.public.Box(server1_key, client_key.public_key).shared_key()
- assert(ckey == skey)
- # Transform the client pubkey with Sphinx as it passes through the
- # servers and record the resulting shared secrets
- blinded_client_pubkey = client_key.public_key
- s1secret, blinded_client_pubkey = Sphinx.server(blinded_client_pubkey,
- server1_key, b'circuit', False, perfstats)
- s2secret, blinded_client_pubkey = Sphinx.server(blinded_client_pubkey,
- server2_key, b'circuit', False, perfstats)
- s3secret, _ = Sphinx.server(blinded_client_pubkey,
- server3_key, b'circuit', True, perfstats)
- # Hopefully matching keys on the client side
- blinding_keys = []
- c1secret, blind_key = Sphinx.client(client_key, blinding_keys,
- server1_key.public_key, b'circuit', False, perfstats)
- blinding_keys.append(blind_key)
- c2secret, blind_key = Sphinx.client(client_key, blinding_keys,
- server2_key.public_key, b'circuit', False, perfstats)
- blinding_keys.append(blind_key)
- c3secret, _ = Sphinx.client(client_key, blinding_keys,
- server3_key.public_key, b'circuit', True, perfstats)
- assert(s1secret == c1secret)
- assert(s2secret == c2secret)
- assert(s3secret == c3secret)
- print('Sphinx DH test successful')
- # End test of Sphinx (DH version)
- # Test the Sphinx class: NTor version (for the path selection keys)
- server1_idkey = nacl.signing.SigningKey.generate().verify_key
- server2_idkey = nacl.signing.SigningKey.generate().verify_key
- server3_idkey = nacl.signing.SigningKey.generate().verify_key
- server1_onionkey = nacl.public.PrivateKey.generate()
- server2_onionkey = nacl.public.PrivateKey.generate()
- server3_onionkey = nacl.public.PrivateKey.generate()
- client_ntor = NTor(perfstats)
- # Client's initial message
- client_pubkey = client_ntor.request()
- # Transform the client pubkey with Sphinx as it passes through the
- # servers and record the resulting shared secrets
- blinded_client_pubkey = client_pubkey
- (s1reply, s1secret), blinded_client_pubkey = NTor.reply(
- server1_onionkey, server1_idkey, blinded_client_pubkey,
- perfstats, b'data')
- (s2reply, s2secret), blinded_client_pubkey = NTor.reply(
- server2_onionkey, server2_idkey, blinded_client_pubkey,
- perfstats, b'data')
- (s3reply, s3secret) = NTor.reply(
- server3_onionkey, server3_idkey, blinded_client_pubkey,
- perfstats)
- # Hopefully matching keys on the client side
- c1secret = client_ntor.verify(s1reply, server1_onionkey.public_key,
- server1_idkey, b'data')
- c2secret = client_ntor.verify(s2reply, server2_onionkey.public_key,
- server2_idkey, b'data')
- c3secret = client_ntor.verify(s3reply, server3_onionkey.public_key,
- server3_idkey)
- assert(s1secret == c1secret)
- assert(s2secret == c2secret)
- assert(s3secret == c3secret)
- print('Sphinx NTor test successful')
- # End test of Sphinx (NTor version)
|