From d81731e681a612fbf24f570e61022418efd22f95 Mon Sep 17 00:00:00 2001 From: Jouni Malinen Date: Sun, 8 Jun 2014 23:10:02 +0300 Subject: [PATCH] tests: Framework for EAP peer protocol tests This introduces a minimal RADIUS authentication server using pyrad to allow simple EAP handler functions to be used for writing protocol tests for EAP peer methods. This initial commit includes test cases for EAP-SAKE. Signed-off-by: Jouni Malinen --- tests/hwsim/dictionary.radius | 1 + tests/hwsim/test_eap_proto.py | 331 ++++++++++++++++++++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 tests/hwsim/test_eap_proto.py diff --git a/tests/hwsim/dictionary.radius b/tests/hwsim/dictionary.radius index da31f89e4..26db83644 100644 --- a/tests/hwsim/dictionary.radius +++ b/tests/hwsim/dictionary.radius @@ -5,6 +5,7 @@ ATTRIBUTE Calling-Station-Id 31 string ATTRIBUTE NAS-Identifier 32 string ATTRIBUTE Acct-Session-Id 44 string ATTRIBUTE Event-Timestamp 55 date +ATTRIBUTE EAP-Message 79 string ATTRIBUTE Message-Authenticator 80 octets ATTRIBUTE Chargeable-User-Identity 89 string ATTRIBUTE Error-Cause 101 integer diff --git a/tests/hwsim/test_eap_proto.py b/tests/hwsim/test_eap_proto.py new file mode 100644 index 000000000..613f44f5c --- /dev/null +++ b/tests/hwsim/test_eap_proto.py @@ -0,0 +1,331 @@ +# EAP protocol tests +# Copyright (c) 2014, Jouni Malinen +# +# This software may be distributed under the terms of the BSD license. +# See README for more details. + +import hmac +import logging +logger = logging.getLogger() +import select +import struct +import threading +import time + +import hostapd + +EAP_CODE_REQUEST = 1 +EAP_CODE_RESPONSE = 2 +EAP_CODE_SUCCESS = 3 +EAP_CODE_FAILURE = 4 + +EAP_TYPE_IDENTITY = 1 +EAP_TYPE_NOTIFICATION = 2 +EAP_TYPE_NAK = 3 +EAP_TYPE_MD5 = 4 +EAP_TYPE_OTP = 5 +EAP_TYPE_GTC = 6 +EAP_TYPE_TLS = 13 +EAP_TYPE_LEAP = 17 +EAP_TYPE_SIM = 18 +EAP_TYPE_TTLS = 21 +EAP_TYPE_AKA = 23 +EAP_TYPE_PEAP = 25 +EAP_TYPE_MSCHAPV2 = 26 +EAP_TYPE_TLV = 33 +EAP_TYPE_TNC = 38 +EAP_TYPE_FAST = 43 +EAP_TYPE_PAX = 46 +EAP_TYPE_PSK = 47 +EAP_TYPE_SAKE = 48 +EAP_TYPE_IKEV2 = 49 +EAP_TYPE_AKA_PRIME = 50 +EAP_TYPE_GPSK = 51 +EAP_TYPE_PWD = 52 +EAP_TYPE_EKE = 53 + +def run_pyrad_server(srv, t_stop, eap_handler): + srv.RunWithStop(t_stop, eap_handler) + +def start_radius_server(eap_handler): + try: + import pyrad.server + import pyrad.packet + import pyrad.dictionary + except ImportError: + return None + + class TestServer(pyrad.server.Server): + def _HandleAuthPacket(self, pkt): + pyrad.server.Server._HandleAuthPacket(self, pkt) + if len(pkt[79]) > 1: + logger.info("Multiple EAP-Message attributes") + # TODO: reassemble + eap = pkt[79][0] + eap_req = self.eap_handler(self.ctx, eap) + reply = self.CreateReplyPacket(pkt) + if eap_req: + if len(eap_req) > 253: + logger.info("Need to fragment EAP-Message") + # TODO: fragment + reply.AddAttribute("EAP-Message", eap_req) + logger.info("No EAP request available") + reply.code = pyrad.packet.AccessChallenge + + hmac_obj = hmac.new(reply.secret) + hmac_obj.update(struct.pack("B", reply.code)) + hmac_obj.update(struct.pack("B", reply.id)) + + # reply attributes + reply.AddAttribute("Message-Authenticator", + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00") + attrs = reply._PktEncodeAttributes() + + # Length + flen = 4 + 16 + len(attrs) + hmac_obj.update(struct.pack(">H", flen)) + hmac_obj.update(pkt.authenticator) + hmac_obj.update(attrs) + del reply[80] + reply.AddAttribute("Message-Authenticator", hmac_obj.digest()) + + self.SendReplyPacket(pkt.fd, reply) + + def RunWithStop(self, t_stop, eap_handler): + self._poll = select.poll() + self._fdmap = {} + self._PrepareSockets() + self.t_stop = t_stop + self.eap_handler = eap_handler + self.ctx = {} + + while not t_stop.is_set(): + for (fd, event) in self._poll.poll(1000): + if event == select.POLLIN: + try: + fdo = self._fdmap[fd] + self._ProcessInput(fdo) + except pyrad.server.ServerPacketError as err: + logger.info("pyrad server dropping packet: " + str(err)) + except pyrad.packet.PacketError as err: + logger.info("pyrad server received invalid packet: " + str(err)) + else: + logger.error("Unexpected event in pyrad server main loop") + + srv = TestServer(dict=pyrad.dictionary.Dictionary("dictionary.radius"), + authport=18138, acctport=18139) + srv.hosts["127.0.0.1"] = pyrad.server.RemoteHost("127.0.0.1", + "radius", + "localhost") + srv.BindToAddress("") + t_stop = threading.Event() + t = threading.Thread(target=run_pyrad_server, args=(srv, t_stop, eap_handler)) + t.start() + + return { 'srv': srv, 'stop': t_stop, 'thread': t } + +def stop_radius_server(srv): + srv['stop'].set() + srv['thread'].join() + +def start_ap(ifname): + params = hostapd.wpa2_eap_params(ssid="eap-test") + params['auth_server_port'] = "18138" + hapd = hostapd.add_ap(ifname, params) + return hapd + +EAP_SAKE_VERSION = 2 + +EAP_SAKE_SUBTYPE_CHALLENGE = 1 +EAP_SAKE_SUBTYPE_CONFIRM = 2 +EAP_SAKE_SUBTYPE_AUTH_REJECT = 3 +EAP_SAKE_SUBTYPE_IDENTITY = 4 + +EAP_SAKE_AT_RAND_S = 1 +EAP_SAKE_AT_RAND_P = 2 +EAP_SAKE_AT_MIC_S = 3 +EAP_SAKE_AT_MIC_P = 4 +EAP_SAKE_AT_SERVERID = 5 +EAP_SAKE_AT_PEERID = 6 +EAP_SAKE_AT_SPI_S = 7 +EAP_SAKE_AT_SPI_P = 8 +EAP_SAKE_AT_ANY_ID_REQ = 9 +EAP_SAKE_AT_PERM_ID_REQ = 10 +EAP_SAKE_AT_ENCR_DATA = 128 +EAP_SAKE_AT_IV = 129 +EAP_SAKE_AT_PADDING = 130 +EAP_SAKE_AT_NEXT_TMPID = 131 +EAP_SAKE_AT_MSK_LIFE = 132 + +def test_eap_proto_sake(dev, apdev): + """EAP-SAKE protocol tests""" + def sake_challenge(ctx): + logger.info("Test: Challenge subtype") + return struct.pack(">BBHBBBBBBLLLL", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 18, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CHALLENGE, + EAP_SAKE_AT_RAND_S, 18, 0, 0, 0, 0) + + def sake_handler(ctx, req): + logger.info("sake_handler - RX " + req.encode("hex")) + if 'num' not in ctx: + ctx['num'] = 0 + ctx['num'] = ctx['num'] + 1 + if 'id' not in ctx: + ctx['id'] = 1 + ctx['id'] = (ctx['id'] + 1) % 256 + + if ctx['num'] == 1: + logger.info("Test: Missing payload") + return struct.pack(">BBHB", EAP_CODE_REQUEST, ctx['id'], 4 + 1, + EAP_TYPE_SAKE) + + if ctx['num'] == 2: + logger.info("Test: Identity subtype without any attributes") + return struct.pack(">BBHBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_IDENTITY) + + if ctx['num'] == 3: + logger.info("Test: Identity subtype") + return struct.pack(">BBHBBBBBBH", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 4, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_IDENTITY, + EAP_SAKE_AT_ANY_ID_REQ, 4, 0) + if ctx['num'] == 4: + logger.info("Test: Identity subtype (different session id)") + return struct.pack(">BBHBBBBBBH", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 4, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 1, EAP_SAKE_SUBTYPE_IDENTITY, + EAP_SAKE_AT_PERM_ID_REQ, 4, 0) + + if ctx['num'] == 5: + logger.info("Test: Identity subtype with too short attribute") + return struct.pack(">BBHBBBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 2, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_IDENTITY, + EAP_SAKE_AT_ANY_ID_REQ, 2) + + if ctx['num'] == 6: + logger.info("Test: Identity subtype with truncated attribute") + return struct.pack(">BBHBBBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 2, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_IDENTITY, + EAP_SAKE_AT_ANY_ID_REQ, 4) + + if ctx['num'] == 7: + logger.info("Test: Unknown subtype") + return struct.pack(">BBHBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, 123) + + if ctx['num'] == 8: + logger.info("Test: Challenge subtype without any attributes") + return struct.pack(">BBHBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CHALLENGE) + + if ctx['num'] == 9: + logger.info("Test: Challenge subtype with too short AT_RAND_S") + return struct.pack(">BBHBBBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 2, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CHALLENGE, + EAP_SAKE_AT_RAND_S, 2) + + if ctx['num'] == 10: + return sake_challenge(ctx) + if ctx['num'] == 11: + logger.info("Test: Unexpected Identity subtype") + return struct.pack(">BBHBBBBBBH", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 4, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_IDENTITY, + EAP_SAKE_AT_ANY_ID_REQ, 4, 0) + + if ctx['num'] == 12: + return sake_challenge(ctx) + if ctx['num'] == 13: + logger.info("Test: Unexpected Challenge subtype") + return struct.pack(">BBHBBBBBBLLLL", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 18, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CHALLENGE, + EAP_SAKE_AT_RAND_S, 18, 0, 0, 0, 0) + + if ctx['num'] == 14: + return sake_challenge(ctx) + if ctx['num'] == 15: + logger.info("Test: Confirm subtype without any attributes") + return struct.pack(">BBHBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CONFIRM) + + if ctx['num'] == 16: + return sake_challenge(ctx) + if ctx['num'] == 17: + logger.info("Test: Confirm subtype with too short AT_MIC_S") + return struct.pack(">BBHBBBBBB", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 2, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CONFIRM, + EAP_SAKE_AT_MIC_S, 2) + + if ctx['num'] == 18: + logger.info("Test: Unexpected Confirm subtype") + return struct.pack(">BBHBBBBBBLLLL", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 18, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CONFIRM, + EAP_SAKE_AT_MIC_S, 18, 0, 0, 0, 0) + + if ctx['num'] == 19: + return sake_challenge(ctx) + if ctx['num'] == 20: + logger.info("Test: Confirm subtype with incorrect AT_MIC_S") + return struct.pack(">BBHBBBBBBLLLL", EAP_CODE_REQUEST, ctx['id'], + 4 + 1 + 3 + 18, + EAP_TYPE_SAKE, + EAP_SAKE_VERSION, 0, EAP_SAKE_SUBTYPE_CONFIRM, + EAP_SAKE_AT_MIC_S, 18, 0, 0, 0, 0) + + return sake_challenge(ctx) + + srv = start_radius_server(sake_handler) + if srv is None: + return "skip" + + try: + hapd = start_ap(apdev[0]['ifname']) + + for i in range(0, 14): + dev[0].connect("eap-test", key_mgmt="WPA-EAP", scan_freq="2412", + eap="SAKE", identity="sake user", + password_hex="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + wait_connect=False) + ev = dev[0].wait_event(["CTRL-EVENT-EAP-PROPOSED-METHOD"], timeout=15) + if ev is None: + raise Exception("Timeout on EAP start") + time.sleep(0.1) + dev[0].request("REMOVE_NETWORK all") + + logger.info("Too short password") + dev[0].connect("eap-test", key_mgmt="WPA-EAP", scan_freq="2412", + eap="SAKE", identity="sake user", + password_hex="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcd", + wait_connect=False) + ev = dev[0].wait_event(["CTRL-EVENT-EAP-PROPOSED-METHOD"], timeout=15) + if ev is None: + raise Exception("Timeout on EAP start") + time.sleep(0.1) + finally: + stop_radius_server(srv)