From ef04103340f9c2f2666ea3a12bf6420758f2da6d Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Tue, 25 Apr 2017 15:33:19 +0100 Subject: [PATCH] replace plaintext secret transmission with time-limited hmac-based bearer tokens Signed-off-by: Ian Jackson --- PROTOCOL | 10 ++++++++-- README.config | 10 ++++++++++ hippotat | 4 +++- hippotatd | 5 +++-- hippotatlib/__init__.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/PROTOCOL b/PROTOCOL index 4e07db1..e18cf0e 100644 --- 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: + +(separated by a single space). The hmac is + HMAC(secret, ) Possible future nonce-based authentication: diff --git a/README.config b/README.config index a9d36d7..842573c 100644 --- a/README.config +++ b/README.config @@ -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 diff --git a/hippotat b/hippotat index 56e3e45..a6ec7ae 100755 --- 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 + diff --git a/hippotatd b/hippotatd index bbc8666..05c51bc 100755 --- 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: diff --git a/hippotatlib/__init__.py b/hippotatlib/__init__.py index 282266c..2fa3008 100644 --- a/hippotatlib/__init__.py +++ b/hippotatlib/__init__.py @@ -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 [] 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(): -- 2.11.0