tests: Framework for EAP peer protocol tests
authorJouni Malinen <j@w1.fi>
Sun, 8 Jun 2014 20:10:02 +0000 (23:10 +0300)
committerJouni Malinen <j@w1.fi>
Sun, 8 Jun 2014 21:30:44 +0000 (00:30 +0300)
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 <j@w1.fi>
tests/hwsim/dictionary.radius
tests/hwsim/test_eap_proto.py [new file with mode: 0644]

index da31f89..26db836 100644 (file)
@@ -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 (file)
index 0000000..613f44f
--- /dev/null
@@ -0,0 +1,331 @@
+# EAP protocol tests
+# Copyright (c) 2014, Jouni Malinen <j@w1.fi>
+#
+# 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)