|
@@ -8,6 +8,7 @@ import nacl.utils
|
|
|
import nacl.signing
|
|
|
import nacl.public
|
|
|
import nacl.hash
|
|
|
+import nacl.bindings
|
|
|
|
|
|
import network
|
|
|
import dirauth
|
|
@@ -199,6 +200,93 @@ class RelayFallbackTerminationError(Exception):
|
|
|
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."""
|
|
|
+ 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
|
|
@@ -207,6 +295,9 @@ class NTor:
|
|
|
|
|
|
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."""
|
|
@@ -215,10 +306,13 @@ class NTor:
|
|
|
return self.client_ephem_key.public_key
|
|
|
|
|
|
@staticmethod
|
|
|
- def reply(onion_privkey, idpubkey, client_pubkey, perfstats):
|
|
|
+ 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)."""
|
|
|
+ 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."""
|
|
|
server_ephem_key = nacl.public.PrivateKey.generate()
|
|
|
perfstats.keygens += 1
|
|
|
xykey = nacl.public.Box(server_ephem_key, client_pubkey).shared_key()
|
|
@@ -230,18 +324,43 @@ class NTor:
|
|
|
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):
|
|
|
+ if sphinx_domainsep is not None:
|
|
|
+ blindkey = Sphinx.makeblindkey(S, sphinx_domainsep, perfstats)
|
|
|
+ blinded_client_pubkey = Sphinx.reblindpubkey(blindkey,
|
|
|
+ client_pubkey, perfstats)
|
|
|
+ return ((server_ephem_key.public_key, onion_privkey.public_key, A),
|
|
|
+ S), blinded_client_pubkey
|
|
|
+ else:
|
|
|
+ return ((server_ephem_key.public_key, 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 . Returns the shared secret on
|
|
|
- success, or raises ValueError on failure."""
|
|
|
+ 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 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()
|
|
|
+ # 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.encode(encoder=nacl.encoding.RawEncoder) + \
|
|
@@ -251,6 +370,11 @@ class NTor:
|
|
|
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
|
|
|
|
|
|
|
|
@@ -970,3 +1094,84 @@ if __name__ == '__main__':
|
|
|
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)
|