xref: /illumos-gate/usr/src/lib/krb5/kadm5/srv/chgpwd.c (revision 7ab4e62e3b5c454f248a38bec0d489e8f5543324)
1 /*
2  * Copyright 2010 Sun Microsystems, Inc.  All rights reserved.
3  * Use is subject to license terms.
4  */
5 
6 /*
7  * lib/krb5/kadm5/srv/chgpwd.c
8  *
9  * Copyright 1998 by the Massachusetts Institute of Technology.
10  * All Rights Reserved.
11  *
12  * Export of this software from the United States of America may
13  *   require a specific license from the United States Government.
14  *   It is the responsibility of any person or organization contemplating
15  *   export to obtain such a license before exporting.
16  *
17  * WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
18  * distribute this software and its documentation for any purpose and
19  * without fee is hereby granted, provided that the above copyright
20  * notice appear in all copies and that both that copyright notice and
21  * this permission notice appear in supporting documentation, and that
22  * the name of M.I.T. not be used in advertising or publicity pertaining
23  * to distribution of the software without specific, written prior
24  * permission.  Furthermore if you modify this software you must label
25  * your software as modified software and not distribute it in such a
26  * fashion that it might be confused with the original M.I.T. software.
27  * M.I.T. makes no representations about the suitability of
28  * this software for any purpose.  It is provided "as is" without express
29  * or implied warranty.
30  *
31  */
32 
33 /*
34  * chgpwd.c - Handles changepw requests issued from non-Solaris krb5 clients.
35  */
36 
37 #include <libintl.h>
38 #include <locale.h>
39 #include <kadm5/admin.h>
40 #include <syslog.h>
41 #include <krb5/adm_proto.h>
42 
43 #define	MAXAPREQ 1500
44 
45 static krb5_error_code
46 process_chpw_request(krb5_context context, void *server_handle,
47 			char *realm, int s, krb5_keytab keytab,
48 			struct sockaddr_in *sin, krb5_data *req,
49 			krb5_data *rep)
50 {
51 	krb5_error_code ret;
52 	char *ptr;
53 	int plen, vno;
54 	krb5_address local_kaddr, remote_kaddr;
55 	int allocated_mem = 0;
56 	krb5_data ap_req, ap_rep;
57 	krb5_auth_context auth_context;
58 	krb5_principal changepw;
59 	krb5_ticket *ticket;
60 	krb5_data cipher, clear;
61 	struct sockaddr local_addr, remote_addr;
62 	int addrlen;
63 	krb5_replay_data replay;
64 	krb5_error krberror;
65 	int numresult;
66 	char strresult[1024];
67 	char *clientstr;
68 	size_t clen;
69 	char *cdots;
70 
71 	ret = 0;
72 	rep->length = 0;
73 
74 	auth_context = NULL;
75 	changepw = NULL;
76 	ap_rep.length = 0;
77 	ap_rep.data = NULL;
78 	ticket = NULL;
79 	clear.length = 0;
80 	clear.data = NULL;
81 	cipher.length = 0;
82 	cipher.data = NULL;
83 
84 	if (req->length < 4) {
85 		/*
86 		 * either this, or the server is printing bad messages,
87 		 * or the caller passed in garbage
88 		 */
89 		ret = KRB5KRB_AP_ERR_MODIFIED;
90 		numresult = KRB5_KPASSWD_MALFORMED;
91 		(void) strlcpy(strresult, "Request was truncated",
92 				sizeof (strresult));
93 		goto chpwfail;
94 	}
95 
96 	ptr = req->data;
97 
98 	/*
99 	 * Verify length
100 	 */
101 	plen = (*ptr++ & 0xff);
102 	plen = (plen<<8) | (*ptr++ & 0xff);
103 
104 	if (plen != req->length)
105 		return (KRB5KRB_AP_ERR_MODIFIED);
106 
107 	/*
108 	 * Verify version number
109 	 */
110 	vno = (*ptr++ & 0xff);
111 	vno = (vno<<8) | (*ptr++ & 0xff);
112 
113 	if (vno != 1) {
114 		ret = KRB5KDC_ERR_BAD_PVNO;
115 		numresult = KRB5_KPASSWD_MALFORMED;
116 		(void) snprintf(strresult, sizeof (strresult),
117 		    "Request contained unknown protocol version number %d",
118 		    vno);
119 		goto chpwfail;
120 	}
121 
122 	/*
123 	 * Read, check ap-req length
124 	 */
125 	ap_req.length = (*ptr++ & 0xff);
126 	ap_req.length = (ap_req.length<<8) | (*ptr++ & 0xff);
127 
128 	if (ptr + ap_req.length >= req->data + req->length) {
129 		ret = KRB5KRB_AP_ERR_MODIFIED;
130 		numresult = KRB5_KPASSWD_MALFORMED;
131 		(void) strlcpy(strresult, "Request was truncated in AP-REQ",
132 					sizeof (strresult));
133 		goto chpwfail;
134 	}
135 
136 	/*
137 	 * Verify ap_req
138 	 */
139 	ap_req.data = ptr;
140 	ptr += ap_req.length;
141 
142 	if (ret = krb5_auth_con_init(context, &auth_context)) {
143 		krb5_klog_syslog(LOG_ERR,
144 				gettext("Change password request failed. "
145 					"Failed initializing auth context: %s"),
146 				error_message(ret));
147 		numresult = KRB5_KPASSWD_HARDERROR;
148 		(void) strlcpy(strresult, "Failed initializing auth context",
149 					sizeof (strresult));
150 		goto chpwfail;
151 	}
152 
153 	if (ret = krb5_auth_con_setflags(context, auth_context,
154 					KRB5_AUTH_CONTEXT_DO_SEQUENCE)) {
155 		krb5_klog_syslog(LOG_ERR,
156 				gettext("Change password request failed. "
157 						"Failed setting auth "
158 					    "context flags: %s"),
159 				error_message(ret));
160 		numresult = KRB5_KPASSWD_HARDERROR;
161 		(void) strlcpy(strresult, "Failed initializing auth context",
162 					sizeof (strresult));
163 		goto chpwfail;
164 	}
165 
166 	if (ret = krb5_build_principal(context, &changepw, strlen(realm), realm,
167 				    "kadmin", "changepw", NULL)) {
168 		krb5_klog_syslog(LOG_ERR,
169 			gettext("Change password request failed "
170 					"Failed to build kadmin/changepw "
171 					"principal: %s"),
172 			error_message(ret));
173 		numresult = KRB5_KPASSWD_HARDERROR;
174 		(void) strlcpy(strresult,
175 				"Failed building kadmin/changepw principal",
176 				sizeof (strresult));
177 		goto chpwfail;
178 	}
179 
180 	ret = krb5_rd_req(context, &auth_context, &ap_req, changepw, keytab,
181 			NULL, &ticket);
182 
183 	if (ret) {
184 		char kt_name[MAX_KEYTAB_NAME_LEN];
185 		if (krb5_kt_get_name(context, keytab,
186 				kt_name, sizeof (kt_name)))
187 			strncpy(kt_name, "default keytab", sizeof (kt_name));
188 
189 		switch (ret) {
190 		case KRB5_KT_NOTFOUND:
191 		krb5_klog_syslog(LOG_ERR,
192 			gettext("Change password request failed because "
193 					"keytab entry \"kadmin/changepw\" "
194 					"is missing from \"%s\""),
195 			kt_name);
196 		break;
197 		case ENOENT:
198 		krb5_klog_syslog(LOG_ERR,
199 			gettext("Change password request failed because "
200 					"keytab file \"%s\" does not exist"),
201 			kt_name);
202 		break;
203 		default:
204 		krb5_klog_syslog(LOG_ERR,
205 			gettext("Change password request failed. "
206 				"Failed to parse Kerberos AP_REQ message: %s"),
207 			error_message(ret));
208 		}
209 
210 		numresult = KRB5_KPASSWD_AUTHERROR;
211 		(void) strlcpy(strresult, "Failed reading application request",
212 					sizeof (strresult));
213 		goto chpwfail;
214 	}
215 
216 	/*
217 	 * Set up address info
218 	 */
219 	addrlen = sizeof (local_addr);
220 
221 	if (getsockname(s, &local_addr, &addrlen) < 0) {
222 		ret = errno;
223 		numresult = KRB5_KPASSWD_HARDERROR;
224 		(void) strlcpy(strresult,
225 				"Failed getting server internet address",
226 				sizeof (strresult));
227 		goto chpwfail;
228 	}
229 
230 	/*
231 	 * Some brain-dead OS's don't return useful information from
232 	 * the getsockname call.  Namely, windows and solaris.
233 	 */
234 	if (((struct sockaddr_in *)&local_addr)->sin_addr.s_addr != 0) {
235 		local_kaddr.addrtype = ADDRTYPE_INET;
236 		local_kaddr.length = sizeof (((struct sockaddr_in *)
237 						&local_addr)->sin_addr);
238 		/* CSTYLED */
239 		local_kaddr.contents = (krb5_octet *) &(((struct sockaddr_in *)&local_addr)->sin_addr);
240 	} else {
241 		krb5_address **addrs;
242 
243 		krb5_os_localaddr(context, &addrs);
244 
245 		local_kaddr.magic = addrs[0]->magic;
246 		local_kaddr.addrtype = addrs[0]->addrtype;
247 		local_kaddr.length = addrs[0]->length;
248 		if ((local_kaddr.contents = malloc(addrs[0]->length)) == 0) {
249 			ret = errno;
250 			numresult = KRB5_KPASSWD_HARDERROR;
251 			(void) strlcpy(strresult,
252 				"Malloc failed for local_kaddr",
253 				sizeof (strresult));
254 			goto chpwfail;
255 		}
256 
257 		(void) memcpy(local_kaddr.contents, addrs[0]->contents,
258 				addrs[0]->length);
259 		allocated_mem++;
260 
261 		krb5_free_addresses(context, addrs);
262 	}
263 
264 	addrlen = sizeof (remote_addr);
265 
266 	if (getpeername(s, &remote_addr, &addrlen) < 0) {
267 		ret = errno;
268 		numresult = KRB5_KPASSWD_HARDERROR;
269 		(void) strlcpy(strresult,
270 				"Failed getting client internet address",
271 				sizeof (strresult));
272 		goto chpwfail;
273 	}
274 
275 	remote_kaddr.addrtype = ADDRTYPE_INET;
276 	remote_kaddr.length = sizeof (((struct sockaddr_in *)
277 					&remote_addr)->sin_addr);
278 	/* CSTYLED */
279 	remote_kaddr.contents = (krb5_octet *) &(((struct sockaddr_in *)&remote_addr)->sin_addr);
280 	remote_kaddr.addrtype = ADDRTYPE_INET;
281 	remote_kaddr.length = sizeof (sin->sin_addr);
282 	remote_kaddr.contents = (krb5_octet *) &sin->sin_addr;
283 
284 	/*
285 	 * mk_priv requires that the local address be set.
286 	 * getsockname is used for this.  rd_priv requires that the
287 	 * remote address be set.  recvfrom is used for this.  If
288 	 * rd_priv is given a local address, and the message has the
289 	 * recipient addr in it, this will be checked.  However, there
290 	 * is simply no way to know ahead of time what address the
291 	 * message will be delivered *to*.  Therefore, it is important
292 	 * that either no recipient address is in the messages when
293 	 * mk_priv is called, or that no local address is passed to
294 	 * rd_priv.  Both is a better idea, and I have done that.  In
295 	 * summary, when mk_priv is called, *only* a local address is
296 	 * specified.  when rd_priv is called, *only* a remote address
297 	 * is specified.  Are we having fun yet?
298 	 */
299 	if (ret = krb5_auth_con_setaddrs(context, auth_context, NULL,
300 					&remote_kaddr)) {
301 		numresult = KRB5_KPASSWD_HARDERROR;
302 		(void) strlcpy(strresult,
303 				"Failed storing client internet address",
304 				sizeof (strresult));
305 		goto chpwfail;
306 	}
307 
308 	/*
309 	 * Verify that this is an AS_REQ ticket
310 	 */
311 	if (!(ticket->enc_part2->flags & TKT_FLG_INITIAL)) {
312 		numresult = KRB5_KPASSWD_AUTHERROR;
313 		(void) strlcpy(strresult,
314 				"Ticket must be derived from a password",
315 				sizeof (strresult));
316 		goto chpwfail;
317 	}
318 
319 	/*
320 	 * Construct the ap-rep
321 	 */
322 	if (ret = krb5_mk_rep(context, auth_context, &ap_rep)) {
323 		numresult = KRB5_KPASSWD_AUTHERROR;
324 		(void) strlcpy(strresult,
325 				"Failed replying to application request",
326 				sizeof (strresult));
327 		goto chpwfail;
328 	}
329 
330 	/*
331 	 * Decrypt the new password
332 	 */
333 	cipher.length = (req->data + req->length) - ptr;
334 	cipher.data = ptr;
335 
336 	if (ret = krb5_rd_priv(context, auth_context, &cipher,
337 				&clear, &replay)) {
338 		numresult = KRB5_KPASSWD_HARDERROR;
339 		(void) strlcpy(strresult, "Failed decrypting request",
340 					sizeof (strresult));
341 		goto chpwfail;
342 	}
343 
344 	ret = krb5_unparse_name(context, ticket->enc_part2->client, &clientstr);
345 	if (ret) {
346 		numresult = KRB5_KPASSWD_HARDERROR;
347 		(void) strcpy(strresult, "Failed decrypting request");
348 		goto chpwfail;
349 	}
350 
351 	/*
352 	 * Change the password
353 	 */
354 	if ((ptr = (char *)malloc(clear.length + 1)) == NULL) {
355 		ret = errno;
356 		numresult = KRB5_KPASSWD_HARDERROR;
357 		(void) strlcpy(strresult, "Malloc failed for ptr",
358 			sizeof (strresult));
359 		krb5_free_unparsed_name(context, clientstr);
360 		goto chpwfail;
361 	}
362 	(void) memcpy(ptr, clear.data, clear.length);
363 	ptr[clear.length] = '\0';
364 
365 	ret = (kadm5_ret_t)kadm5_chpass_principal_util(server_handle,
366 						ticket->enc_part2->client,
367 						ptr, NULL, strresult,
368 						sizeof (strresult));
369 
370 	/*
371 	 * Zap the password
372 	 */
373 	(void) memset(clear.data, 0, clear.length);
374 	(void) memset(ptr, 0, clear.length);
375 	if (clear.data != NULL) {
376 		krb5_xfree(clear.data);
377 		clear.data = NULL;
378 	}
379 	free(ptr);
380 	clear.length = 0;
381 
382 	clen = strlen(clientstr);
383 	trunc_name(&clen, &cdots);
384 	krb5_klog_syslog(LOG_NOTICE, "chpw request from %s for %.*s%s: %s",
385 		inet_ntoa(((struct sockaddr_in *)&remote_addr)->sin_addr),
386 		clen, clientstr, cdots, ret ? error_message(ret) : "success");
387 	krb5_free_unparsed_name(context, clientstr);
388 
389 	if (ret) {
390 		if ((ret != KADM5_PASS_Q_TOOSHORT) &&
391 		    (ret != KADM5_PASS_REUSE) &&
392 		    (ret != KADM5_PASS_Q_CLASS) &&
393 		    (ret != KADM5_PASS_Q_DICT) &&
394 		    (ret != KADM5_PASS_TOOSOON))
395 			numresult = KRB5_KPASSWD_HARDERROR;
396 		else
397 			numresult = KRB5_KPASSWD_SOFTERROR;
398 		/*
399 		 * strresult set by kadb5_chpass_principal_util()
400 		 */
401 		goto chpwfail;
402 	}
403 
404 	/*
405 	 * Success!
406 	 */
407 	numresult = KRB5_KPASSWD_SUCCESS;
408 	(void) strlcpy(strresult, "", sizeof (strresult));
409 
410 chpwfail:
411 
412 	clear.length = 2 + strlen(strresult);
413 	if (clear.data != NULL) {
414 		krb5_xfree(clear.data);
415 		clear.data = NULL;
416 	}
417 	if ((clear.data = (char *)malloc(clear.length)) == NULL) {
418 		ret = errno;
419 		numresult = KRB5_KPASSWD_HARDERROR;
420 		(void) strlcpy(strresult, "Malloc failed for clear.data",
421 			sizeof (strresult));
422 	}
423 
424 	ptr = clear.data;
425 
426 	*ptr++ = (numresult>>8) & 0xff;
427 	*ptr++ = numresult & 0xff;
428 
429 	(void) memcpy(ptr, strresult, strlen(strresult));
430 
431 	cipher.length = 0;
432 
433 	if (ap_rep.length) {
434 		if (ret = krb5_auth_con_setaddrs(context, auth_context,
435 					&local_kaddr, NULL)) {
436 		    numresult = KRB5_KPASSWD_HARDERROR;
437 		    (void) strlcpy(strresult,
438 			"Failed storing client and server internet addresses",
439 			sizeof (strresult));
440 		} else {
441 			if (ret = krb5_mk_priv(context, auth_context, &clear,
442 						&cipher, &replay)) {
443 				numresult = KRB5_KPASSWD_HARDERROR;
444 				(void) strlcpy(strresult,
445 					"Failed encrypting reply",
446 					sizeof (strresult));
447 			}
448 		}
449 	}
450 
451 	/*
452 	 * If no KRB-PRIV was constructed, then we need a KRB-ERROR.
453 	 * If this fails, just bail.  There's nothing else we can do.
454 	 */
455 	if (cipher.length == 0) {
456 		/*
457 		 * Clear out ap_rep now, so that it won't be inserted
458 		 * in the reply
459 		 */
460 		if (ap_rep.length) {
461 			if (ap_rep.data != NULL)
462 				krb5_xfree(ap_rep.data);
463 			ap_rep.data = NULL;
464 			ap_rep.length = 0;
465 		}
466 
467 		krberror.ctime = 0;
468 		krberror.cusec = 0;
469 		krberror.susec = 0;
470 		if (ret = krb5_timeofday(context, &krberror.stime))
471 			goto bailout;
472 
473 		/*
474 		 * This is really icky.  but it's what all the other callers
475 		 * to mk_error do.
476 		 */
477 		krberror.error = ret;
478 		krberror.error -= ERROR_TABLE_BASE_krb5;
479 		if (krberror.error < 0 || krberror.error > 128)
480 			krberror.error = KRB_ERR_GENERIC;
481 
482 		krberror.client = NULL;
483 		if (ret = krb5_build_principal(context, &krberror.server,
484 					    strlen(realm), realm,
485 					    "kadmin", "changepw", NULL)) {
486 			goto bailout;
487 		}
488 
489 		krberror.text.length = 0;
490 		krberror.e_data = clear;
491 
492 		ret = krb5_mk_error(context, &krberror, &cipher);
493 
494 		krb5_free_principal(context, krberror.server);
495 
496 		if (ret)
497 			goto bailout;
498 	}
499 
500 	/*
501 	 * Construct the reply
502 	 */
503 	rep->length = 6 + ap_rep.length + cipher.length;
504 	if ((rep->data = (char *)malloc(rep->length)) == NULL)  {
505 		ret = errno;
506 		goto bailout;
507 	}
508 	ptr = rep->data;
509 
510 	/*
511 	 * Length
512 	 */
513 	*ptr++ = (rep->length>>8) & 0xff;
514 	*ptr++ = rep->length & 0xff;
515 
516 	/*
517 	 * Version == 0x0001 big-endian
518 	 */
519 	*ptr++ = 0;
520 	*ptr++ = 1;
521 
522 	/*
523 	 * ap_rep length, big-endian
524 	 */
525 	*ptr++ = (ap_rep.length>>8) & 0xff;
526 	*ptr++ = ap_rep.length & 0xff;
527 
528 	/*
529 	 * ap-rep data
530 	 */
531 	if (ap_rep.length) {
532 		(void) memcpy(ptr, ap_rep.data, ap_rep.length);
533 		ptr += ap_rep.length;
534 	}
535 
536 	/*
537 	 * krb-priv or krb-error
538 	 */
539 	(void) memcpy(ptr, cipher.data, cipher.length);
540 
541 bailout:
542 	if (auth_context)
543 		krb5_auth_con_free(context, auth_context);
544 	if (changepw)
545 		krb5_free_principal(context, changepw);
546 	if (ap_rep.data != NULL)
547 		krb5_xfree(ap_rep.data);
548 	if (ticket)
549 		krb5_free_ticket(context, ticket);
550 	if (clear.data != NULL)
551 		krb5_xfree(clear.data);
552 	if (cipher.data != NULL)
553 		krb5_xfree(cipher.data);
554 	if (allocated_mem)
555 		krb5_xfree(local_kaddr.contents);
556 
557 	return (ret);
558 }
559 
560 
561 /*
562  * This routine is used to handle password-change requests received
563  * on kpasswd-port 464 from MIT/M$ clients.
564  */
565 void
566 handle_chpw(krb5_context context, int s1,
567 		void *serverhandle, kadm5_config_params *params)
568 {
569 	krb5_error_code ret;
570 	char req[MAXAPREQ];
571 	int len;
572 	struct sockaddr_in from;
573 	int fromlen;
574 	krb5_keytab kt;
575 	krb5_data reqdata, repdata;
576 	int s2 = -1;
577 
578 	reqdata.length = 0;
579 	reqdata.data = NULL;
580 	repdata.length = 0;
581 	repdata.data = NULL;
582 
583 	fromlen = sizeof (from);
584 
585 	if ((len = recvfrom(s1, req, sizeof (req), 0, (struct sockaddr *)&from,
586 			    &fromlen)) < 0) {
587 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Couldn't receive "
588 				"request: %s"), error_message(errno));
589 		return;
590 	}
591 
592 	/*
593 	 * Solaris Kerberos:
594 	 * The only caller is kadmind, which is the master and therefore has the
595 	 * correct keys in the KDB, rather than obtaining them via the
596 	 * kadm5.keytab, by default.
597 	 */
598 	if ((ret = krb5_kt_resolve(context, "KDB:", &kt))) {
599 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Couldn't open "
600 				"admin keytab %s"), error_message(ret));
601 		return;
602 	}
603 
604 	reqdata.length = len;
605 	reqdata.data = req;
606 
607 	/*
608 	 * This is really obscure.  s1 is used for all communications.  it
609 	 * is left unconnected in case the server is multihomed and routes
610 	 * are asymmetric.  s2 is connected to resolve routes and get
611 	 * addresses.  this is the *only* way to get proper addresses for
612 	 * multihomed hosts if routing is asymmetric.
613 	 *
614 	 * A related problem in the server, but not the client, is that
615 	 * many os's have no way to disconnect a connected udp socket, so
616 	 * the s2 socket needs to be closed and recreated for each
617 	 * request.  The s1 socket must not be closed, or else queued
618 	 * requests will be lost.
619 	 *
620 	 * A "naive" client implementation (one socket, no connect,
621 	 * hostname resolution to get the local ip addr) will work and
622 	 * interoperate if the client is single-homed.
623 	 */
624 
625 	if ((s2 = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
626 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Cannot create "
627 				"connecting socket: %s"), error_message(errno));
628 		goto cleanup;
629 	}
630 
631 	if (connect(s2, (struct sockaddr *)&from, sizeof (from)) < 0) {
632 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Couldn't connect "
633 				"to client: %s"), error_message(errno));
634 		if (s2 > 0)
635 			(void) close(s2);
636 		goto cleanup;
637 	}
638 
639 	if ((ret = process_chpw_request(context, serverhandle,
640 					params->realm, s2, kt, &from,
641 					&reqdata, &repdata))) {
642 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Error processing "
643 				"request: %s"), error_message(ret));
644 	}
645 
646 	if (s2 > 0)
647 		(void) close(s2);
648 
649 	if (repdata.length == 0 || repdata.data == NULL) {
650 		/*
651 		 * Just return.  This means something really bad happened
652 		 */
653 		goto cleanup;
654 	}
655 
656 	len = sendto(s1, repdata.data, repdata.length, 0,
657 		    (struct sockaddr *)&from, sizeof (from));
658 
659 	if (len < repdata.length) {
660 		krb5_xfree(repdata.data);
661 
662 		krb5_klog_syslog(LOG_ERR, gettext("chpw: Error sending reply:"
663 				" %s"), error_message(errno));
664 		goto cleanup;
665 	}
666 
667 	if (repdata.data != NULL)
668 		krb5_xfree(repdata.data);
669 cleanup:
670 	krb5_kt_close(context, kt);
671 }
672