chiark / gitweb /
replace plaintext secret transmission with time-limited hmac-based bearer tokens
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Tue, 25 Apr 2017 14:33:19 +0000 (15:33 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Tue, 25 Apr 2017 14:33:19 +0000 (15:33 +0100)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
PROTOCOL
README.config
hippotat
hippotatd
hippotatlib/__init__.py

index 4e07db163296421c402714f8858a0059a1e5725a..e18cf0e1554da169e6ffdcd8994ba3a727f8c17c 100644 (file)
--- a/PROTOCOL
+++ b/PROTOCOL
@@ -8,7 +8,9 @@ from the queue and returns them as the POST response body payload
 Each incoming request contains up to max_batch_up bytes of payload.
 It's a multipart/form-data.
 
-Authentication: for now, plaintext secret
+Authentication: clock-based lifetime-limited bearer tokens.
+
+Encryption and integrity checking: none.  Use a real VPN over this!
 
 Routing assistance: none in hippotat; can be requested on client
  from userv-ipif via `vroutes' parameter.  Use with secnet polypath
@@ -17,12 +19,16 @@ Routing assistance: none in hippotat; can be requested on client
 Client form parameters (multipart/form-data):
  m             metadata, newline-separated list (text file) of
                        client ip address (textual)
-                       password
+                       token
                        target_requests_outstanding
                        http_timeout
  d              data (SLIP format, with SLIP_ESC and `-' swapped)
 
 
+Authentication token is:
+        <time_t in hex with no leading 0s> <hmac in base64>
+(separated by a single space).  The hmac is
+        HMAC(secret, <time_t in hex>)
 
 
 Possible future nonce-based authentication:
index a9d36d72a85907c2f808f67fb8443bf17753e215..842573cd6a153ca9d9f1e1f7041a2c066e8ea468 100644 (file)
@@ -122,6 +122,16 @@ Ordinary settings, used by both, not client-specific:
      Virtual interface name on the client.  [hippo%d]
      Any %d is interpolated (by the kernel).
 
+Ordinary settings, used by server only:
+
+  max_clock_skew
+     Permissible clock skew between client and server.
+     hippotat will not work if clock skew is more than this.
+     Conversely: when moving client from one public network to
+     another, the first network can deny service to the client for
+     this period after the client leaves the first network.
+     [300s]
+
 Ordinary settings, used by client only:
 
   http_timeout_grace
index 56e3e458daaa77a5ff7f5cedfd8ad1588792b6fa..a6ec7aec06405b8315d1120d50e70be83776eeb2 100755 (executable)
--- a/hippotat
+++ b/hippotat
@@ -185,13 +185,15 @@ class Client():
 
       d = mime_translate(d)
 
+      token = authtoken_make(cl.c.secret)
+
       crlf = b'\r\n'
       lf   =   b'\n'
       mime = (b'--b'                                        + crlf +
               b'Content-Type: text/plain; charset="utf-8"'  + crlf +
               b'Content-Disposition: form-data; name="m"'   + crlf + crlf +
               str(cl.c.client)            .encode('ascii')  + crlf +
-              cl.c.secret                                   + crlf +
+              token                                         + crlf +
               str(cl.c.target_requests_outstanding)
                                           .encode('ascii')  + crlf +
               str(cl.c.http_timeout)      .encode('ascii')  + crlf +
index bbc866618e37011cdebc101ec620878bd9ea1b67..05c51bcb58a1c57a682221ad9f5873364a19e5d4 100755 (executable)
--- a/hippotatd
+++ b/hippotatd
@@ -174,7 +174,7 @@ def process_request(request, desca):
   # find client, update config, etc.
   metadata = request.args[b'm'][0]
   metadata = metadata.split(b'\r\n')
-  (ci_s, pw, tro, cto) = metadata[0:4]
+  (ci_s, token, tro, cto) = metadata[0:4]
   desca['m[0,2:3]'] = [ci_s, tro, cto]
   ci_s = ci_s.decode('utf-8')
   tro = int(tro); desca['tro']= tro
@@ -182,7 +182,7 @@ def process_request(request, desca):
   ci = ipaddr(ci_s)
   desca['ci'] = ci
   cl = clients[ci]
-  if pw != cl.cc.secret: raise ValueError('bad secret')
+  authtoken_check(cl.cc.secret, token, cl.cc.max_clock_skew)
   desca['pwok']=True
 
   if tro != cl.cc.target_requests_outstanding:
@@ -311,6 +311,7 @@ def process_cfg(_opts, putative_servers, putative_clients):
     if not sections: continue
     cfg_process_client_limited(cc,c.server,sections, 'max_batch_down')
     cfg_process_client_limited(cc,c.server,sections, 'max_queue_time')
+    cc.max_clock_skew = cfg_search(cfg.getint, 'max_clock_skew', sections)
     Client(ci, cc)
 
   try:
index 282266c009651b004ec940f7b79aac2b84f5c3c7..2fa300831e6bc186edf165c5b12eef7ed2d4e3f7 100644 (file)
@@ -50,6 +50,9 @@ from functools import partial
 
 import collections
 import time
+import hmac
+import hashlib
+import base64
 import codecs
 import traceback
 
@@ -139,6 +142,7 @@ port = 80
 vroutes = ''
 ifname_client = hippo%%d
 ifname_server = shippo%%d
+max_clock_skew = 300
 
 #[server] or [<client>] overrides
 ipif = userv root ipif %(local)s,%(peer)s,%(mtu)s,slip,%(ifname)s %(rnets)s
@@ -367,6 +371,34 @@ def crash_on_critical(event):
   if event.get('log_level') >= LogLevel.critical:
     crash(twisted.logger.formatEvent(event))
 
+#---------- authentication tokens ----------
+
+_authtoken_digest = hashlib.sha256
+
+def _authtoken_time():
+  return int(time.time())
+
+def _authtoken_hmac(secret, hextime):
+  return hmac.new(secret, hextime, _authtoken_digest).digest()
+
+def authtoken_make(secret):
+  hextime = ('%x' % _authtoken_time()).encode('ascii')
+  mac = _authtoken_hmac(secret, hextime)
+  return hextime + b' ' + base64.b64encode(mac)
+
+def authtoken_check(secret, token, maxskew):
+  (hextime, theirmac64) = token.split(b' ')
+  now = _authtoken_time()
+  then = int(hextime, 16)
+  skew = then - now;
+  if (abs(skew) > maxskew):
+    raise ValueError('too much clock skew (client %ds ahead)' % skew)
+  theirmac = base64.b64decode(theirmac64)
+  ourmac = _authtoken_hmac(secret, hextime)
+  if not hmac.compare_digest(theirmac, ourmac):
+    raise ValueError('invalid token (wrong secret?)')
+  pass
+
 #---------- config processing ----------
 
 def _cfg_process_putatives():