From: Daniel Lenski Date: Sun, 21 Feb 2021 23:02:44 +0000 (-0800) Subject: add auth-f5 tests X-Git-Tag: v8.20~325^2~11 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=cda045a0a6adcfaecf70b555133c59704d3cd55b;p=users%2Fdwmw2%2Fopenconnect.git add auth-f5 tests This tests OpenConnect's ability to authenticate with a (fake) F5 server, using username+password (the only option that OpenConnect currently supports). The fake F5 authentication server requires python3 and Flask. Signed-off-by: Daniel Lenski --- diff --git a/tests/Makefile.am b/tests/Makefile.am index 50d49a72..6b16a7a2 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -46,7 +46,7 @@ EXTRA_DIST = certs/ca.pem certs/ca-key.pem certs/user-cert.pem $(USER_KEYS) $(US configs/user-cert.prm configs/server-cert.prm \ softhsm2.conf.in softhsm ns.sh configs/test-dtls-psk.config \ scripts/vpnc-script scripts/vpnc-script-detect-disconnect \ - suppressions.lsan fake-fortinet-server.py + suppressions.lsan fake-fortinet-server.py fake-f5-server.py dist_check_SCRIPTS = autocompletion @@ -63,6 +63,7 @@ dist_check_SCRIPTS += auth-username-pass auth-certificate auth-nonascii cert-fin if HAVE_PYTHON36_FLASK dist_check_SCRIPTS += auth-fortinet +dist_check_SCRIPTS += auth-f5 endif if TEST_PKCS11 diff --git a/tests/auth-f5 b/tests/auth-f5 new file mode 100755 index 00000000..8f76b798 --- /dev/null +++ b/tests/auth-f5 @@ -0,0 +1,46 @@ +#!/bin/sh +# +# Copyright © 2021 Daniel Lenski +# +# This file is part of openconnect. +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# as published by the Free Software Foundation; either version 2.1 of +# the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see + +# This test uses LD_PRELOAD +PRELOAD=1 +srcdir=${srcdir:-.} +top_builddir=${top_builddir:-..} + +. `dirname $0`/common.sh + +FINGERPRINT="--servercert=d66b507ae074d03b02eafca40d35f87dd81049d3" +CERT=$certdir/server-cert.pem +KEY=$certdir/server-key.pem + +echo "Testing F5 auth against fake server ... " + +OCSERV=./fake-f5-server.py +launch_simple_sr_server $ADDRESS 443 $CERT $KEY > /dev/null 2>&1 +PID=$! +wait_server $PID 1 + +echo -n "Authenticating with username/password... " +( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=f5 -q $ADDRESS:443 -u test $FINGERPRINT --cookieonly >/dev/null 2>&1) || + fail $PID "Could not receive cookie from fake F5 server" + +echo ok + +cleanup + +exit 0 diff --git a/tests/fake-f5-server.py b/tests/fake-f5-server.py new file mode 100755 index 00000000..abb5c02a --- /dev/null +++ b/tests/fake-f5-server.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Copyright © 2021 Daniel Lenski +# +# This file is part of openconnect. +# +# This is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# as published by the Free Software Foundation; either version 2.1 of +# the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see + +######################################## +# This program emulates the authentication-phase behavior of a F5 +# server enough to test OpenConnect's authentication behavior against it. +# Specifically, it emulates the following requests: +# +# GET / +# GET /my.policy +# POST /my.policy +# +# It does not actually validate the credentials in any way, but attempts to +# verify their consistency from one request to the next, by saving their +# values via a (cookie-based) session. +######################################## + +import sys +import ssl +import random +import base64 +import time +from json import dumps +from functools import wraps +from flask import Flask, request, abort, redirect, url_for, make_response, session + +host, port, *cert_and_maybe_keyfile = sys.argv[1:] + +context = ssl.SSLContext() +context.load_cert_chain(*cert_and_maybe_keyfile) + +app = Flask(__name__) +app.config.update(SECRET_KEY=b'fake', DEBUG=True, HOST=host, PORT=int(port), SESSION_COOKIE_NAME='fake') + +######################################## + +def cookify(jsonable): + return base64.urlsafe_b64encode(dumps(jsonable).encode()) + +def require_MRHSession(fn): + @wraps(fn) + def wrapped(*args, **kwargs): + if not request.cookies.get('MRHSession'): + session.clear() + return redirect(url_for('get_policy')) + return fn(*args, **kwargs) + return wrapped + +def check_form_against_session(*fields, use_query=False): + def inner(fn): + @wraps(fn) + def wrapped(*args, **kwargs): + source = request.args if use_query else request.form + source_name = 'args' if use_query else 'form' + for f in fields: + assert session.get(f) == source.get(f), \ + f'at step {session.get("step")}: {source_name} {f!r} {source.get(f)!r} != session {f!r} {session.get(f)!r}' + return fn(*args, **kwargs) + return wrapped + return inner + +######################################## + +# Respond to initial 'GET /' with a redirect to '/my.policy' +@app.route('/') +def root(): + session.update(step='initial-GET') + # print(session) + return redirect(url_for('get_policy')) + + +# Respond to 'GET /my.policy with a placeholder stub (since OpenConnect doesn't even try to parse the form) +@app.route('/my.policy') +def get_policy(): + session.update(step='GET-login-form') + return 'login page' + + +# Respond to 'POST /my.policy with an empty response containing MRHSession and F5_ST +# cookies (OpenConnect uses the combination of the two to detect successful authentication) +@app.route('/my.policy', methods=['POST']) +def post_policy(): + session.update(step='POST-login', username=request.form.get('username'), credential=request.form.get('password')) + # print(session) + + resp = make_response('') + resp.set_cookie('MRHSession', cookify(dict(session))) + resp.set_cookie('F5_ST', '1z1z1z%dz%d' % (time.time(), 3600)) + return resp + + +# Respond to 'GET /remote/logout' by clearing session and MRHSession +@app.route('/remote/logout') +@require_MRHSession +def logout(): + assert request.args == {'hangup_error': '1'} + session.clear() + resp = make_response('successful logout') + resp.set_cookie('MRHSession', '') + return resp + + +app.run(host=app.config['HOST'], port=app.config['PORT'], debug=app.config['DEBUG'], + ssl_context=context, use_debugger=False)