Browse Source

Implement ntor

It's not byte-for-byte compatible with the real tor handshake, but
it correctly does key agreement and one-way authentication, and the
right number of public-key operations.
Ian Goldberg 5 years ago
parent
commit
f32dee62a6
1 changed files with 64 additions and 0 deletions
  1. 64 0
      relay.py

+ 64 - 0
relay.py

@@ -6,6 +6,7 @@ import math
 import nacl.utils
 import nacl.signing
 import nacl.public
+import nacl.hash
 
 import network
 import dirauth
@@ -70,6 +71,61 @@ class RelayFallbackTerminationError(Exception):
     relay."""
 
 
+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
+
+    def request(self):
+        """Create the ntor request message: X = g^x."""
+        self.client_ephem_key = nacl.public.PrivateKey.generate()
+        self.perfstats.keygens += 1
+        return self.client_ephem_key.public_key
+
+    @staticmethod
+    def reply(onion_privkey, idpubkey, client_pubkey, perfstats):
+        """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)."""
+        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.encode(encoder=nacl.encoding.RawEncoder) + \
+                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)
+        return ((server_ephem_key.public_key, onion_privkey.public_key, A), \
+                S)
+
+    def verify(self, reply, onion_pubkey, idpubkey):
+        """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 .  Returns the shared secret on
+        success, or raises ValueError on failure."""
+        server_ephem_pubkey, server_onion_pubkey, authtag = reply
+        if onion_pubkey != server_onion_pubkey:
+            raise ValueError("NTor onion pubkey mismatch")
+        xykey = nacl.public.Box(self.client_ephem_key, server_ephem_pubkey).shared_key()
+        xbkey = nacl.public.Box(self.client_ephem_key, onion_pubkey).shared_key()
+        self.perfstats.dhs += 2
+        M = xykey + xbkey + \
+                idpubkey.encode(encoder=nacl.encoding.RawEncoder) + \
+                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")
+        return S
+
 class CircuitHandler:
     """A class for managing sending and receiving encrypted cells on a
     particular circuit."""
@@ -463,3 +519,11 @@ if __name__ == '__main__':
     peerchannel = relays[5].cellhandler.get_channel_to(relays[3].netaddr)
     peerchannel.new_circuit_with_circid(circid)
     relays[3].cellhandler.send_cell(circid, network.StringNetMsg("test"), relays[5].netaddr)
+
+    idpubkey = dirauth.DirAuth.consensus.consdict["relays"][1].descdict["idkey"]
+    onionpubkey = dirauth.DirAuth.consensus.consdict["relays"][1].descdict["onionkey"]
+    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)