1# Author: Nathaniel McCallum <npmccallum@redhat.com> 2# 3# Copyright (c) 2013 Red Hat, Inc. 4# 5# Permission is hereby granted, free of charge, to any person obtaining a copy 6# of this software and associated documentation files (the "Software"), to deal 7# in the Software without restriction, including without limitation the rights 8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9# copies of the Software, and to permit persons to whom the Software is 10# furnished to do so, subject to the following conditions: 11# 12# The above copyright notice and this permission notice shall be included in 13# all copies or substantial portions of the Software. 14# 15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21# THE SOFTWARE. 22 23 24# 25# This script tests OTP, both UDP and Unix Sockets, with a variety of 26# configuration. It requires pyrad to run, but exits gracefully if not found. 27# It also deliberately shuts down the test daemons between tests in order to 28# test how OTP handles the case of short daemon restarts. 29# 30 31from k5test import * 32from queue import Empty 33import io 34import struct 35 36try: 37 from pyrad import packet, dictionary 38except ImportError: 39 skip_rest('OTP tests', 'Python pyrad module not found') 40try: 41 from multiprocessing import Process, Queue 42except ImportError: 43 skip_rest('OTP tests', 'Python version 2.6 required') 44 45# We could use a dictionary file, but since we need so few attributes, 46# we'll just include them here. 47radius_attributes = ''' 48ATTRIBUTE User-Name 1 string 49ATTRIBUTE User-Password 2 octets 50ATTRIBUTE Service-Type 6 integer 51ATTRIBUTE NAS-Identifier 32 string 52''' 53 54class RadiusDaemon(Process): 55 MAX_PACKET_SIZE = 4096 56 DICTIONARY = dictionary.Dictionary(io.StringIO(radius_attributes)) 57 58 def listen(self, addr): 59 raise NotImplementedError() 60 61 def recvRequest(self, data): 62 raise NotImplementedError() 63 64 def run(self): 65 addr = self._args[0] 66 secrfile = self._args[1] 67 pswd = self._args[2] 68 outq = self._args[3] 69 70 if secrfile: 71 with open(secrfile, 'rb') as file: 72 secr = file.read().strip() 73 else: 74 secr = b'' 75 76 data = self.listen(addr) 77 outq.put("started") 78 (buf, sock, addr) = self.recvRequest(data) 79 pkt = packet.AuthPacket(secret=secr, 80 dict=RadiusDaemon.DICTIONARY, 81 packet=buf) 82 83 usernm = [] 84 passwd = [] 85 for key in pkt.keys(): 86 if key == 'User-Password': 87 passwd = list(map(pkt.PwDecrypt, pkt[key])) 88 elif key == 'User-Name': 89 usernm = pkt[key] 90 91 reply = pkt.CreateReply() 92 replyq = {'user': usernm, 'pass': passwd} 93 if passwd == [pswd]: 94 reply.code = packet.AccessAccept 95 replyq['reply'] = True 96 else: 97 reply.code = packet.AccessReject 98 replyq['reply'] = False 99 100 outq.put(replyq) 101 if addr is None: 102 sock.send(reply.ReplyPacket()) 103 else: 104 sock.sendto(reply.ReplyPacket(), addr) 105 sock.close() 106 107class UDPRadiusDaemon(RadiusDaemon): 108 def listen(self, addr): 109 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 110 sock.bind((addr.split(':')[0], int(addr.split(':')[1]))) 111 return sock 112 113 def recvRequest(self, sock): 114 (buf, addr) = sock.recvfrom(RadiusDaemon.MAX_PACKET_SIZE) 115 return (buf, sock, addr) 116 117class UnixRadiusDaemon(RadiusDaemon): 118 def listen(self, addr): 119 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 120 if os.path.exists(addr): 121 os.remove(addr) 122 sock.bind(addr) 123 sock.listen(1) 124 return (sock, addr) 125 126 def recvRequest(self, sock_and_addr): 127 sock, addr = sock_and_addr 128 conn = sock.accept()[0] 129 sock.close() 130 os.remove(addr) 131 132 buf = b'' 133 remain = RadiusDaemon.MAX_PACKET_SIZE 134 while True: 135 buf += conn.recv(remain) 136 remain = RadiusDaemon.MAX_PACKET_SIZE - len(buf) 137 if (len(buf) >= 4): 138 remain = struct.unpack("!BBH", buf[0:4])[2] - len(buf) 139 if (remain <= 0): 140 return (buf, conn, None) 141 142def verify(daemon, queue, reply, usernm, passwd): 143 try: 144 data = queue.get(timeout=1) 145 except Empty: 146 sys.stderr.write("ERROR: Packet not received by daemon!\n") 147 daemon.terminate() 148 sys.exit(1) 149 assert data['reply'] is reply 150 assert data['user'] == [usernm] 151 assert data['pass'] == [passwd] 152 daemon.join() 153 154# Compose a single token configuration. 155def otpconfig_1(toktype, username=None, indicators=None): 156 val = '{"type": "%s"' % toktype 157 if username is not None: 158 val += ', "username": "%s"' % username 159 if indicators is not None: 160 qind = ['"%s"' % s for s in indicators] 161 jsonlist = '[' + ', '.join(qind) + ']' 162 val += ', "indicators":' + jsonlist 163 val += '}' 164 return val 165 166# Compose a token configuration list suitable for the "otp" string 167# attribute. 168def otpconfig(toktype, username=None, indicators=None): 169 return '[' + otpconfig_1(toktype, username, indicators) + ']' 170 171prefix = "/tmp/%d" % os.getpid() 172secret_file = prefix + ".secret" 173socket_file = prefix + ".socket" 174with open(secret_file, "w") as file: 175 file.write("otptest") 176atexit.register(lambda: os.remove(secret_file)) 177 178conf = {'plugins': {'kdcpreauth': {'enable_only': 'otp'}}, 179 'otp': {'udp': {'server': '127.0.0.1:$port9', 180 'secret': secret_file, 181 'strip_realm': 'true', 182 'indicator': ['indotp1', 'indotp2']}, 183 'unix': {'server': socket_file, 184 'strip_realm': 'false'}}} 185 186queue = Queue() 187 188realm = K5Realm(kdc_conf=conf) 189realm.run([kadminl, 'modprinc', '+requires_preauth', realm.user_princ]) 190flags = ['-T', realm.ccache] 191server_addr = '127.0.0.1:' + str(realm.portbase + 9) 192 193## Test UDP fail / custom username 194mark('UDP fail / custom username') 195daemon = UDPRadiusDaemon(args=(server_addr, secret_file, 'accept', queue)) 196daemon.start() 197queue.get() 198realm.run([kadminl, 'setstr', realm.user_princ, 'otp', 199 otpconfig('udp', 'custom')]) 200realm.kinit(realm.user_princ, 'reject', flags=flags, expected_code=1) 201verify(daemon, queue, False, 'custom', 'reject') 202 203## Test UDP success / standard username 204mark('UDP success / standard username') 205daemon = UDPRadiusDaemon(args=(server_addr, secret_file, 'accept', queue)) 206daemon.start() 207queue.get() 208realm.run([kadminl, 'setstr', realm.user_princ, 'otp', otpconfig('udp')]) 209realm.kinit(realm.user_princ, 'accept', flags=flags) 210verify(daemon, queue, True, realm.user_princ.split('@')[0], 'accept') 211realm.extract_keytab(realm.krbtgt_princ, realm.keytab) 212realm.run(['./adata', realm.krbtgt_princ], 213 expected_msg='+97: [indotp1, indotp2]') 214 215# Repeat with an indicators override in the string attribute. 216mark('auth indicator override') 217daemon = UDPRadiusDaemon(args=(server_addr, secret_file, 'accept', queue)) 218daemon.start() 219queue.get() 220oconf = otpconfig('udp', indicators=['indtok1', 'indtok2']) 221realm.run([kadminl, 'setstr', realm.user_princ, 'otp', oconf]) 222realm.kinit(realm.user_princ, 'accept', flags=flags) 223verify(daemon, queue, True, realm.user_princ.split('@')[0], 'accept') 224realm.extract_keytab(realm.krbtgt_princ, realm.keytab) 225realm.run(['./adata', realm.krbtgt_princ], 226 expected_msg='+97: [indtok1, indtok2]') 227 228# Detect upstream pyrad bug 229# https://github.com/wichert/pyrad/pull/18 230try: 231 auth = packet.Packet.CreateAuthenticator() 232 packet.Packet(authenticator=auth, secret=b'').ReplyPacket() 233except AssertionError: 234 skip_rest('OTP UNIX domain socket tests', 'pyrad assertion bug detected') 235 236## Test Unix fail / custom username 237mark('Unix socket fail / custom username') 238daemon = UnixRadiusDaemon(args=(socket_file, None, 'accept', queue)) 239daemon.start() 240queue.get() 241realm.run([kadminl, 'setstr', realm.user_princ, 'otp', 242 otpconfig('unix', 'custom')]) 243realm.kinit(realm.user_princ, 'reject', flags=flags, expected_code=1) 244verify(daemon, queue, False, 'custom', 'reject') 245 246## Test Unix success / standard username 247mark('Unix socket success / standard username') 248daemon = UnixRadiusDaemon(args=(socket_file, None, 'accept', queue)) 249daemon.start() 250queue.get() 251realm.run([kadminl, 'setstr', realm.user_princ, 'otp', otpconfig('unix')]) 252realm.kinit(realm.user_princ, 'accept', flags=flags) 253verify(daemon, queue, True, realm.user_princ, 'accept') 254 255## Regression test for #8708: test with the standard username and two 256## tokens configured, with the first rejecting and the second 257## accepting. With the bug, the KDC incorrectly rejects the request 258## and then performs invalid memory accesses, most likely crashing. 259queue2 = Queue() 260daemon1 = UDPRadiusDaemon(args=(server_addr, secret_file, 'accept1', queue)) 261daemon2 = UnixRadiusDaemon(args=(socket_file, None, 'accept2', queue2)) 262daemon1.start() 263queue.get() 264daemon2.start() 265queue2.get() 266oconf = '[' + otpconfig_1('udp') + ', ' + otpconfig_1('unix') + ']' 267realm.run([kadminl, 'setstr', realm.user_princ, 'otp', oconf]) 268realm.kinit(realm.user_princ, 'accept2', flags=flags) 269verify(daemon1, queue, False, realm.user_princ.split('@')[0], 'accept2') 270verify(daemon2, queue2, True, realm.user_princ, 'accept2') 271 272success('OTP tests') 273