1# This is a simple KCM test server, used to exercise the KCM ccache 2# client code. It will generally throw an uncaught exception if the 3# client sends anything unexpected, so is unsuitable for production. 4# (It also imposes no namespace or access constraints, and blocks 5# while reading requests and writing responses.) 6 7# This code knows nothing about how to marshal and unmarshal principal 8# names and credentials as is required in the KCM protocol; instead, 9# it just remembers the marshalled forms and replays them to the 10# client when asked. This works because marshalled creds and 11# principal names are always the last part of marshalled request 12# arguments, and because we don't need to implement remove_cred (which 13# would need to know how to match a cred tag against previously stored 14# credentials). 15 16# The following code is useful for debugging if anything appears to be 17# going wrong in the server, since daemon output is generally not 18# visible in Python test scripts. 19# 20# import sys, traceback 21# def ehook(etype, value, tb): 22# with open('/tmp/exception', 'w') as f: 23# traceback.print_exception(etype, value, tb, file=f) 24# sys.excepthook = ehook 25 26import optparse 27import select 28import socket 29import struct 30import sys 31 32caches = {} 33cache_uuidmap = {} 34defname = b'default' 35next_unique = 1 36next_uuid = 1 37 38class KCMOpcodes(object): 39 GEN_NEW = 3 40 INITIALIZE = 4 41 DESTROY = 5 42 STORE = 6 43 RETRIEVE = 7 44 GET_PRINCIPAL = 8 45 GET_CRED_UUID_LIST = 9 46 GET_CRED_BY_UUID = 10 47 REMOVE_CRED = 11 48 GET_CACHE_UUID_LIST = 18 49 GET_CACHE_BY_UUID = 19 50 GET_DEFAULT_CACHE = 20 51 SET_DEFAULT_CACHE = 21 52 GET_KDC_OFFSET = 22 53 SET_KDC_OFFSET = 23 54 GET_CRED_LIST = 13001 55 REPLACE = 13002 56 57 58class KRB5Errors(object): 59 KRB5_CC_NOTFOUND = -1765328243 60 KRB5_CC_END = -1765328242 61 KRB5_CC_NOSUPP = -1765328137 62 KRB5_FCC_NOFILE = -1765328189 63 KRB5_FCC_INTERNAL = -1765328188 64 65 66def make_uuid(): 67 global next_uuid 68 uuid = bytes(12) + struct.pack('>L', next_uuid) 69 next_uuid = next_uuid + 1 70 return uuid 71 72 73class Cache(object): 74 def __init__(self, name): 75 self.name = name 76 self.princ = None 77 self.uuid = make_uuid() 78 self.cred_uuids = [] 79 self.creds = {} 80 self.time_offset = 0 81 82 83def get_cache(name): 84 if name in caches: 85 return caches[name] 86 cache = Cache(name) 87 caches[name] = cache 88 cache_uuidmap[cache.uuid] = cache 89 return cache 90 91 92def unmarshal_name(argbytes): 93 offset = argbytes.find(b'\0') 94 return argbytes[0:offset], argbytes[offset+1:] 95 96 97# Find the bounds of a marshalled principal, returning it and the 98# remainder of argbytes. 99def extract_princ(argbytes): 100 ncomps, rlen = struct.unpack('>LL', argbytes[4:12]) 101 pos = 12 + rlen 102 for i in range(ncomps): 103 clen, = struct.unpack('>L', argbytes[pos:pos+4]) 104 pos += 4 + clen 105 return argbytes[0:pos], argbytes[pos:] 106 107 108# Return true if the marshalled principals p1 and p2 name the same 109# principal. 110def princ_eq(p1, p2): 111 # Ignore the name-types at bytes 0..3. The remaining bytes should 112 # be identical if the principals are the same. 113 return p1[4:] == p2[4:] 114 115 116def op_gen_new(argbytes): 117 # Does not actually check for uniqueness. 118 global next_unique 119 name = b'unique' + str(next_unique).encode('ascii') 120 next_unique += 1 121 return 0, name + b'\0' 122 123 124def op_initialize(argbytes): 125 name, princ = unmarshal_name(argbytes) 126 cache = get_cache(name) 127 cache.princ = princ 128 cache.cred_uuids = [] 129 cache.creds = {} 130 cache.time_offset = 0 131 return 0, b'' 132 133 134def op_destroy(argbytes): 135 name, rest = unmarshal_name(argbytes) 136 cache = get_cache(name) 137 del cache_uuidmap[cache.uuid] 138 del caches[name] 139 return 0, b'' 140 141 142def op_store(argbytes): 143 name, cred = unmarshal_name(argbytes) 144 cache = get_cache(name) 145 uuid = make_uuid() 146 cache.creds[uuid] = cred 147 cache.cred_uuids.append(uuid) 148 return 0, b'' 149 150 151def op_retrieve(argbytes): 152 name, rest = unmarshal_name(argbytes) 153 # Ignore the flags at rest[0:4] and the header at rest[4:8]. 154 # Assume there are client and server creds in the tag and match 155 # only against them. 156 cprinc, rest = extract_princ(rest[8:]) 157 sprinc, rest = extract_princ(rest) 158 cache = get_cache(name) 159 for cred in (cache.creds[u] for u in cache.cred_uuids): 160 cred_cprinc, rest = extract_princ(cred) 161 cred_sprinc, rest = extract_princ(rest) 162 if princ_eq(cred_cprinc, cprinc) and princ_eq(cred_sprinc, sprinc): 163 return 0, cred 164 return KRB5Errors.KRB5_CC_NOTFOUND, b'' 165 166 167def op_get_principal(argbytes): 168 name, rest = unmarshal_name(argbytes) 169 cache = get_cache(name) 170 if cache.princ is None: 171 return KRB5Errors.KRB5_FCC_NOFILE, b'' 172 return 0, cache.princ + b'\0' 173 174 175def op_get_cred_uuid_list(argbytes): 176 name, rest = unmarshal_name(argbytes) 177 cache = get_cache(name) 178 return 0, b''.join(cache.cred_uuids) 179 180 181def op_get_cred_by_uuid(argbytes): 182 name, uuid = unmarshal_name(argbytes) 183 cache = get_cache(name) 184 if uuid not in cache.creds: 185 return KRB5Errors.KRB5_CC_END, b'' 186 return 0, cache.creds[uuid] 187 188 189def op_remove_cred(argbytes): 190 return KRB5Errors.KRB5_CC_NOSUPP, b'' 191 192 193def op_get_cache_uuid_list(argbytes): 194 return 0, b''.join(cache_uuidmap.keys()) 195 196 197def op_get_cache_by_uuid(argbytes): 198 uuid = argbytes 199 if uuid not in cache_uuidmap: 200 return KRB5Errors.KRB5_CC_END, b'' 201 return 0, cache_uuidmap[uuid].name + b'\0' 202 203 204def op_get_default_cache(argbytes): 205 return 0, defname + b'\0' 206 207 208def op_set_default_cache(argbytes): 209 global defname 210 defname, rest = unmarshal_name(argbytes) 211 return 0, b'' 212 213 214def op_get_kdc_offset(argbytes): 215 name, rest = unmarshal_name(argbytes) 216 cache = get_cache(name) 217 return 0, struct.pack('>l', cache.time_offset) 218 219 220def op_set_kdc_offset(argbytes): 221 name, obytes = unmarshal_name(argbytes) 222 cache = get_cache(name) 223 cache.time_offset, = struct.unpack('>l', obytes) 224 return 0, b'' 225 226 227def op_get_cred_list(argbytes): 228 name, rest = unmarshal_name(argbytes) 229 cache = get_cache(name) 230 creds = [cache.creds[u] for u in cache.cred_uuids] 231 return 0, (struct.pack('>L', len(creds)) + 232 b''.join(struct.pack('>L', len(c)) + c for c in creds)) 233 234 235def op_replace(argbytes): 236 name, rest = unmarshal_name(argbytes) 237 offset, = struct.unpack('>L', rest[0:4]) 238 princ, rest = extract_princ(rest[4:]) 239 ncreds, = struct.unpack('>L', rest[0:4]) 240 rest = rest[4:] 241 creds = [] 242 for i in range(ncreds): 243 len, = struct.unpack('>L', rest[0:4]) 244 creds.append(rest[4:4+len]) 245 rest = rest[4+len:] 246 247 cache = get_cache(name) 248 cache.princ = princ 249 cache.cred_uuids = [] 250 cache.creds = {} 251 cache.time_offset = offset 252 for i in range(ncreds): 253 uuid = make_uuid() 254 cache.creds[uuid] = creds[i] 255 cache.cred_uuids.append(uuid) 256 257 return 0, b'' 258 259 260ophandlers = { 261 KCMOpcodes.GEN_NEW : op_gen_new, 262 KCMOpcodes.INITIALIZE : op_initialize, 263 KCMOpcodes.DESTROY : op_destroy, 264 KCMOpcodes.STORE : op_store, 265 KCMOpcodes.RETRIEVE : op_retrieve, 266 KCMOpcodes.GET_PRINCIPAL : op_get_principal, 267 KCMOpcodes.GET_CRED_UUID_LIST : op_get_cred_uuid_list, 268 KCMOpcodes.GET_CRED_BY_UUID : op_get_cred_by_uuid, 269 KCMOpcodes.REMOVE_CRED : op_remove_cred, 270 KCMOpcodes.GET_CACHE_UUID_LIST : op_get_cache_uuid_list, 271 KCMOpcodes.GET_CACHE_BY_UUID : op_get_cache_by_uuid, 272 KCMOpcodes.GET_DEFAULT_CACHE : op_get_default_cache, 273 KCMOpcodes.SET_DEFAULT_CACHE : op_set_default_cache, 274 KCMOpcodes.GET_KDC_OFFSET : op_get_kdc_offset, 275 KCMOpcodes.SET_KDC_OFFSET : op_set_kdc_offset, 276 KCMOpcodes.GET_CRED_LIST : op_get_cred_list, 277 KCMOpcodes.REPLACE : op_replace 278} 279 280# Read and respond to a request from the socket s. 281def service_request(s): 282 lenbytes = b'' 283 while len(lenbytes) < 4: 284 lenbytes += s.recv(4 - len(lenbytes)) 285 if lenbytes == b'': 286 return False 287 288 reqlen, = struct.unpack('>L', lenbytes) 289 req = b'' 290 while len(req) < reqlen: 291 req += s.recv(reqlen - len(req)) 292 293 majver, minver, op = struct.unpack('>BBH', req[:4]) 294 argbytes = req[4:] 295 296 if op in ophandlers: 297 code, payload = ophandlers[op](argbytes) 298 else: 299 code, payload = KRB5Errors.KRB5_FCC_INTERNAL, b'' 300 301 # The KCM response is the code (4 bytes) and the response payload. 302 # The Heimdal IPC response is the length of the KCM response (4 303 # bytes), a status code which is essentially always 0 (4 bytes), 304 # and the KCM response. 305 kcm_response = struct.pack('>l', code) + payload 306 hipc_response = struct.pack('>LL', len(kcm_response), 0) + kcm_response 307 s.sendall(hipc_response) 308 return True 309 310parser = optparse.OptionParser() 311parser.add_option('-f', '--fallback', action='store_true', dest='fallback', 312 default=False, 313 help='Do not support RETRIEVE/GET_CRED_LIST/REPLACE') 314(options, args) = parser.parse_args() 315if options.fallback: 316 del ophandlers[KCMOpcodes.RETRIEVE] 317 del ophandlers[KCMOpcodes.GET_CRED_LIST] 318 del ophandlers[KCMOpcodes.REPLACE] 319 320server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 321server.bind(args[0]) 322server.listen(5) 323select_input = [server,] 324sys.stderr.write('starting...\n') 325sys.stderr.flush() 326 327while True: 328 iready, oready, xready = select.select(select_input, [], []) 329 for s in iready: 330 if s == server: 331 client, addr = server.accept() 332 select_input.append(client) 333 else: 334 if not service_request(s): 335 select_input.remove(s) 336 s.close() 337