dirauth.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. #!/usr/bin/env python3
  2. import nacl.encoding
  3. import nacl.signing
  4. import network
  5. # A relay descriptor is a dict containing:
  6. # epoch: epoch id
  7. # idkey: a public identity key
  8. # onionkey: a public onion key
  9. # addr: a network address
  10. # bw: bandwidth
  11. # flags: relay flags
  12. # vrfkey: a VRF public key (Single-Pass Walking Onions only)
  13. # sig: a signature over the above by the idkey
  14. class RelayDescriptor:
  15. def __init__(self, descdict):
  16. self.descdict = descdict
  17. def __str__(self, withsig = True):
  18. res = "RelayDesc [\n"
  19. for k in ["epoch", "idkey", "onionkey", "addr", "bw", "flags",
  20. "vrfkey", "sig"]:
  21. if k in self.descdict:
  22. if k == "idkey" or k == "onionkey":
  23. res += " " + k + ": " + self.descdict[k].encode(encoder=nacl.encoding.HexEncoder).decode("ascii") + "\n"
  24. elif k == "sig":
  25. if withsig:
  26. res += " " + k + ": " + nacl.encoding.HexEncoder.encode(self.descdict[k]).decode("ascii") + "\n"
  27. else:
  28. res += " " + k + ": " + str(self.descdict[k]) + "\n"
  29. res += "]\n"
  30. return res
  31. def sign(self, signingkey):
  32. serialized = self.__str__(False)
  33. signed = signingkey.sign(serialized.encode("ascii"))
  34. self.descdict["sig"] = signed.signature
  35. def verify(self):
  36. serialized = self.__str__(False)
  37. self.descdict["idkey"].verify(serialized.encode("ascii"), self.descdict["sig"])
  38. # A consensus is a dict containing:
  39. # epoch: epoch id
  40. # numrelays: total number of relays
  41. # totbw: total bandwidth of relays
  42. # relays: list of relay descriptors (Vanilla Onion Routing only)
  43. # sigs: list of signatures from the dirauths
  44. class Consensus:
  45. def __init__(self, epoch, relays):
  46. relays = [ d for d in relays if d.descdict['epoch'] == epoch ]
  47. self.consdict = dict()
  48. self.consdict['epoch'] = epoch
  49. self.consdict['numrelays'] = len(relays)
  50. self.consdict['totbw'] = sum([ d.descdict['bw'] for d in relays ])
  51. self.consdict['relays'] = relays
  52. def __str__(self, withsigs = True):
  53. res = "Consensus [\n"
  54. for k in ["epoch", "numrelays", "totbw"]:
  55. if k in self.consdict:
  56. res += " " + k + ": " + str(self.consdict[k]) + "\n"
  57. for r in self.consdict['relays']:
  58. res += str(r)
  59. if withsigs and ('sigs' in self.consdict):
  60. for s in self.consdict['sigs']:
  61. res += " sig: " + nacl.encoding.HexEncoder.encode(s).decode("ascii") + "\n"
  62. res += "]\n"
  63. return res
  64. def sign(self, signingkey, index):
  65. """Use the given signing key to sign the consensus, storing the
  66. result in the sigs list at the given index."""
  67. serialized = self.__str__(False)
  68. signed = signingkey.sign(serialized.encode("ascii"))
  69. if 'sigs' not in self.consdict:
  70. self.consdict['sigs'] = []
  71. if index >= len(self.consdict['sigs']):
  72. self.consdict['sigs'].extend([None] * (index+1-len(self.consdict['sigs'])))
  73. self.consdict['sigs'][index] = signed.signature
  74. def verify(self, verifkeylist):
  75. """Use the given list of verification keys to check the
  76. signatures on the consensus."""
  77. serialized = self.__str__(False)
  78. for i, vk in enumerate(verifkeylist):
  79. vk.verify(serialized.encode("ascii"), self.consdict['sigs'][i])
  80. class DirAuthNetMsg(network.NetMsg):
  81. """The subclass of NetMsg for messages to and from directory
  82. authorities."""
  83. class DirAuthUploadDescMsg(DirAuthNetMsg):
  84. """The subclass of DirAuthNetMsg for uploading a relay
  85. descriptor."""
  86. def __init__(self, desc):
  87. self.desc = desc
  88. class DirAuthGetConsensusMsg(DirAuthNetMsg):
  89. """The subclass of DirAuthNetMsg for fetching the consensus."""
  90. class DirAuthConsensusMsg(DirAuthNetMsg):
  91. """The subclass of DirAuthNetMsg for returning the consensus."""
  92. def __init__(self, consensus):
  93. self.consensus = consensus
  94. class DirAuthGetENDIVEMsg(DirAuthNetMsg):
  95. """The subclass of DirAuthNetMsg for fetching the ENDIVE."""
  96. class DirAuthENDIVEMsg(DirAuthNetMsg):
  97. """The subclass of DirAuthNetMsg for returning the ENDIVE."""
  98. def __init__(self, endive):
  99. self.endive = endive
  100. class DirAuthConnection(network.ClientConnection):
  101. """The subclass of Connection for connections to directory
  102. authorities."""
  103. def __init__(self, peer = None):
  104. super().__init__(peer)
  105. def uploaddesc(self, desc):
  106. """Upload our RelayDescriptor to the DirAuth."""
  107. self.sendmsg(DirAuthUploadDescMeg(desc))
  108. def getconsensus(self):
  109. self.consensus = None
  110. self.sendmsg(DirAuthGetConsensusMsg())
  111. return self.consensus
  112. def getENDIVE(self):
  113. self.endive = None
  114. self.sendmsg(DirAuthGetENDIVEMsg())
  115. return self.endive
  116. def receivedfromserver(self, msg):
  117. if isinstance(msg, DirAuthConsensusMsg):
  118. self.consensus = msg.consensus
  119. elif isinstance(msg, DirAuthENDIVEMsg):
  120. self.endive = msg.endive
  121. else:
  122. raise TypeError('Not a server-originating DirAuthNetMsg', msg)
  123. class DirAuth(network.Server):
  124. """The class representing directory authorities."""
  125. # We simulate the act of computing the consensus by keeping a
  126. # class-static dict that's accessible to all of the dirauths
  127. # This dict is indexed by epoch, and the value is itself a dict
  128. # indexed by the stringified descriptor, with value a pair of (the
  129. # number of dirauths that saw that descriptor, the descriptor
  130. # itself).
  131. uploadeddescs = dict()
  132. consensus = None
  133. endive = None
  134. def __init__(self, me, tot):
  135. """Create a new directory authority. me is the index of which
  136. dirauth this one is (starting from 0), and tot is the total
  137. number of dirauths."""
  138. self.me = me
  139. self.tot = tot
  140. self.name = "Dirauth %d of %d" % (me+1, tot)
  141. # Create the dirauth signature keypair
  142. self.sigkey = nacl.signing.SigningKey.generate()
  143. network.thenetwork.setdirauthkey(me, self.sigkey.verify_key)
  144. network.thenetwork.wantepochticks(self, True, True)
  145. def connected(self, client):
  146. """Callback invoked when a client connects to us. This callback
  147. creates the DirAuthConnection that will be passed to the
  148. client."""
  149. # We don't actually need to keep per-connection state at
  150. # dirauths, even in long-lived connections, so this is
  151. # particularly simple.
  152. return DirAuthConnection(self)
  153. def generate_consensus(self, epoch):
  154. """Generate the consensus (and ENDIVE, if using Walking Onions)
  155. for the given epoch, which should be the one after the one
  156. that's currently about to end."""
  157. threshold = int(self.tot/2)+1
  158. consensusdescs = []
  159. for numseen, desc in DirAuth.uploadeddescs[epoch].values():
  160. if numseen >= threshold:
  161. consensusdescs.append(desc)
  162. DirAuth.consensus = Consensus(epoch, consensusdescs)
  163. def epoch_ending(self, epoch):
  164. # Only dirauth 0 actually needs to generate the consensus
  165. # because of the shared class-static state, but everyone has to
  166. # sign it. Note that this code relies on dirauth 0's
  167. # epoch_ending callback being called before any of the other
  168. # dirauths'.
  169. if self.me == 0:
  170. self.generate_consensus(epoch+1)
  171. del DirAuth.uploadeddescs[epoch+1]
  172. DirAuth.consensus.sign(self.sigkey, self.me)
  173. print(DirAuth.consensus)
  174. def received(self, client, msg):
  175. if isinstance(msg, DirAuthUploadDescMsg):
  176. # Check the uploaded descriptor for sanity
  177. epoch = msg.desc.descdict['epoch']
  178. if epoch != network.thenetwork.getepoch() + 1:
  179. return
  180. # Store it in the class-static dict
  181. if epoch not in DirAuth.uploadeddescs:
  182. DirAuth.uploadeddescs[epoch] = dict()
  183. descstr = str(msg.desc)
  184. if descstr not in DirAuth.uploadeddescs[epoch]:
  185. DirAuth.uploadeddescs[epoch][descstr] = (1, msg.desc)
  186. else:
  187. DirAuth.uploadeddescs[epoch][descstr] = \
  188. (DirAuth.uploadeddescs[epoch][descstr][0]+1,
  189. DirAuth.uploadeddescs[epoch][descstr][1])
  190. elif isinstance(msg, DirAuthGetConsensusMsg):
  191. client.sendmsg(DirAuthConsensusMsg(DirAuth.consensus))
  192. elif isinstance(msg, DirAuthGetENDIVEMsg):
  193. client.sendmsg(DirAuthENDIVEMsg(DirAuth.endive))
  194. else:
  195. raise TypeError('Not a client-originating DirAuthNetMsg', msg)
  196. def closed(self):
  197. pass
  198. if __name__ == '__main__':
  199. # Start some dirauths
  200. numdirauths = 9
  201. dirauthaddrs = []
  202. for i in range(numdirauths):
  203. dirauth = DirAuth(i, numdirauths)
  204. dirauthaddrs.append(network.thenetwork.bind(dirauth))
  205. for a in dirauthaddrs:
  206. print(a,end=' ')
  207. print()
  208. network.thenetwork.nextepoch()