xref: /openbsd-src/libexec/login_ldap/util.c (revision 93f6aaeac35d17b9ef4d669893f6a2ad3d4defb7)
1 /*
2  * $OpenBSD: util.c,v 1.5 2023/04/19 12:34:23 jsg Exp $
3  * Copyright (c) 2002 Institute for Open Systems Technology Australia (IFOST)
4  * Copyright (c) 2007 Michael Erdely <merdely@openbsd.org>
5  * Copyright (c) 2019 Martijn van Duren <martijn@openbsd.org>
6  *
7  * All rights reserved.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in the
16  *    documentation and/or other materials provided with the distribution.
17  * 3. The name of the author may not be used to endorse or promote products
18  *    derived from this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
21  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
22  * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
23  * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
26  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
27  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
28  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
29  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 #include <sys/socket.h>
33 #include <sys/time.h>
34 #include <sys/types.h>
35 #include <sys/stat.h>
36 #include <sys/un.h>
37 #include <netinet/in.h>
38 
39 #include <ctype.h>
40 #include <grp.h>
41 #include <unistd.h>
42 #include <stdio.h>
43 #include <stdarg.h>
44 #include <stdlib.h>
45 #include <string.h>
46 #include <limits.h>
47 #include <errno.h>
48 #include <syslog.h>
49 #include <tls.h>
50 #include <netdb.h>
51 #include <login_cap.h>
52 
53 #include "aldap.h"
54 #include "login_ldap.h"
55 
56 int debug = 0;
57 
58 static int getscope(char *);
59 
60 void
dlog(int d,char * fmt,...)61 dlog(int d, char *fmt, ...)
62 {
63 	va_list ap;
64 
65 	/*
66 	 * if debugging is on, print everything to stderr
67 	 * otherwise, syslog it if d = 0. messing with
68 	 * newlines means there wont be newlines in stuff
69 	 * that goes to syslog.
70 	 */
71 
72 	va_start(ap, fmt);
73 	if (debug) {
74 		vfprintf(stderr, fmt, ap);
75 		fputc('\n', stderr);
76 	} else if (d == 0)
77 		vsyslog(LOG_WARNING, fmt, ap);
78 
79 	va_end(ap);
80 }
81 
82 const char *
ldap_resultcode(enum result_code code)83 ldap_resultcode(enum result_code code)
84 {
85 #define CODE(_X)	case _X:return (#_X)
86 	switch (code) {
87 	CODE(LDAP_SUCCESS);
88 	CODE(LDAP_OPERATIONS_ERROR);
89 	CODE(LDAP_PROTOCOL_ERROR);
90 	CODE(LDAP_TIMELIMIT_EXCEEDED);
91 	CODE(LDAP_SIZELIMIT_EXCEEDED);
92 	CODE(LDAP_COMPARE_FALSE);
93 	CODE(LDAP_COMPARE_TRUE);
94 	CODE(LDAP_STRONG_AUTH_NOT_SUPPORTED);
95 	CODE(LDAP_STRONG_AUTH_REQUIRED);
96 	CODE(LDAP_REFERRAL);
97 	CODE(LDAP_ADMINLIMIT_EXCEEDED);
98 	CODE(LDAP_UNAVAILABLE_CRITICAL_EXTENSION);
99 	CODE(LDAP_CONFIDENTIALITY_REQUIRED);
100 	CODE(LDAP_SASL_BIND_IN_PROGRESS);
101 	CODE(LDAP_NO_SUCH_ATTRIBUTE);
102 	CODE(LDAP_UNDEFINED_TYPE);
103 	CODE(LDAP_INAPPROPRIATE_MATCHING);
104 	CODE(LDAP_CONSTRAINT_VIOLATION);
105 	CODE(LDAP_TYPE_OR_VALUE_EXISTS);
106 	CODE(LDAP_INVALID_SYNTAX);
107 	CODE(LDAP_NO_SUCH_OBJECT);
108 	CODE(LDAP_ALIAS_PROBLEM);
109 	CODE(LDAP_INVALID_DN_SYNTAX);
110 	CODE(LDAP_ALIAS_DEREF_PROBLEM);
111 	CODE(LDAP_INAPPROPRIATE_AUTH);
112 	CODE(LDAP_INVALID_CREDENTIALS);
113 	CODE(LDAP_INSUFFICIENT_ACCESS);
114 	CODE(LDAP_BUSY);
115 	CODE(LDAP_UNAVAILABLE);
116 	CODE(LDAP_UNWILLING_TO_PERFORM);
117 	CODE(LDAP_LOOP_DETECT);
118 	CODE(LDAP_NAMING_VIOLATION);
119 	CODE(LDAP_OBJECT_CLASS_VIOLATION);
120 	CODE(LDAP_NOT_ALLOWED_ON_NONLEAF);
121 	CODE(LDAP_NOT_ALLOWED_ON_RDN);
122 	CODE(LDAP_ALREADY_EXISTS);
123 	CODE(LDAP_NO_OBJECT_CLASS_MODS);
124 	CODE(LDAP_AFFECTS_MULTIPLE_DSAS);
125 	CODE(LDAP_OTHER);
126 	}
127 
128 	return ("UNKNOWN_ERROR");
129 };
130 
131 
132 static int
parse_server_line(char * buf,struct aldap_url * s)133 parse_server_line(char *buf, struct aldap_url *s)
134 {
135 	/**
136 	 * host=[<protocol>://]<hostname>[:port]
137 	 *
138 	 * must have a hostname
139 	 * protocol can be "ldap", "ldaps", "ldap+tls" or "ldapi"
140 	 * for ldap and ldap+tls, port defaults to 389
141 	 * for ldaps, port defaults to 636
142 	 */
143 
144 	if (buf == NULL) {
145 		dlog(1, "%s got NULL buf!", __func__);
146 		return 0;
147 	}
148 
149 	dlog(1, "parse_server_line buf = %s", buf);
150 
151 	memset(s, 0, sizeof(*s));
152 
153 	if (aldap_parse_url(buf, s) == -1) {
154 		dlog(0, "failed to parse host %s", buf);
155 		return 0;
156 	}
157 
158 	if (s->protocol == -1)
159 		s->protocol = LDAP;
160 	if (s->protocol != LDAPI && s->port == 0) {
161 		if (s->protocol == LDAPS)
162 			s->port = 636;
163 		else
164 			s->port = 389;
165 	}
166 
167 	return 1;
168 }
169 
170 int
parse_conf(struct auth_ctx * ctx,const char * path)171 parse_conf(struct auth_ctx *ctx, const char *path)
172 {
173 	FILE *cf;
174 	struct stat sb;
175 	struct group *grp;
176 	struct aldap_urlq *url;
177 	char *buf = NULL, *key, *value, *tail;
178 	const char *errstr;
179 	size_t buflen = 0;
180 	ssize_t linelen;
181 
182 	dlog(1, "Parsing config file '%s'", path);
183 
184 	if ((cf = fopen(path, "r")) == NULL) {
185 		dlog(0, "Can't open config file: %s", strerror(errno));
186 		return 0;
187 	}
188 	if (fstat(fileno(cf), &sb) == -1) {
189 		dlog(0, "Can't stat config file: %s", strerror(errno));
190 		return 0;
191 	}
192 	if ((grp = getgrnam("auth")) == NULL) {
193 		dlog(0, "Can't find group auth");
194 		return 0;
195 	}
196 	if (sb.st_uid != 0 ||
197 	    sb.st_gid != grp->gr_gid ||
198 	    (sb.st_mode & S_IRWXU) != (S_IRUSR | S_IWUSR) ||
199 	    (sb.st_mode & S_IRWXG) != S_IRGRP ||
200 	    (sb.st_mode & S_IRWXO) != 0) {
201 		dlog(0, "Wrong permissions for config file");
202 		return 0;
203 	}
204 
205 	/* We need a default scope */
206 	ctx->gscope = ctx->scope = getscope(NULL);
207 
208 	while ((linelen = getline(&buf, &buflen, cf)) != -1) {
209 		if (buf[linelen - 1] == '\n')
210 			buf[linelen -1] = '\0';
211 		/* Allow leading spaces */
212 		for (key = buf; key[0] != '\0' && isspace(key[0]); key++)
213 			continue;
214 		/* Comment or white lines */
215 		if (key[0] == '#' || key[0] == '\0')
216 			continue;
217 		if ((tail = value = strchr(key, '=')) == NULL) {
218 			dlog(0, "Missing value for option '%s'", key);
219 			return 0;
220 		}
221 		value++;
222 		/* Don't fail over trailing key spaces */
223 		for (tail--; isspace(tail[0]); tail--)
224 			continue;
225 		tail[1] = '\0';
226 		if (strcmp(key, "host") == 0) {
227 			if ((url = calloc(1, sizeof(*url))) == NULL) {
228 				dlog(0, "Failed to add %s: %s", value,
229 				    strerror(errno));
230 				continue;
231 			}
232 			if (parse_server_line(value, &(url->s)) == 0) {
233 				free(url);
234 				return 0;
235 			}
236 			TAILQ_INSERT_TAIL(&(ctx->s), url, entries);
237 		} else if (strcmp(key, "basedn") == 0) {
238 			free(ctx->basedn);
239 			if ((ctx->basedn = strdup(value)) == NULL) {
240 				dlog(0, "%s", strerror(errno));
241 				return 0;
242 			}
243 		} else if (strcmp(key, "binddn") == 0) {
244 			free(ctx->binddn);
245 			if ((ctx->binddn = parse_filter(ctx, value)) == NULL)
246 				return 0;
247 		} else if (strcmp(key, "bindpw") == 0) {
248 			free(ctx->bindpw);
249 			if ((ctx->bindpw = strdup(value)) == NULL) {
250 				dlog(0, "%s", strerror(errno));
251 				return 0;
252 			}
253 		} else if (strcmp(key, "timeout") == 0) {
254 			ctx->timeout = strtonum(value, 0, INT_MAX, &errstr);
255 			if (ctx->timeout == 0 && errstr != NULL) {
256 				dlog(0, "timeout %s", errstr);
257 				return 0;
258 			}
259 		} else if (strcmp(key, "filter") == 0) {
260 			free(ctx->filter);
261 			if ((ctx->filter = parse_filter(ctx, value)) == NULL)
262 				return 0;
263 		} else if (strcmp(key, "scope") == 0) {
264 			if ((ctx->scope = getscope(value)) == -1)
265 				return 0;
266 		} else if (strcmp(key, "cacert") == 0) {
267 			free(ctx->cacert);
268 			if ((ctx->cacert = strdup(value)) == NULL) {
269 				dlog(0, "%s", strerror(errno));
270 				return 0;
271 			}
272 		} else if (strcmp(key, "cacertdir") == 0) {
273 			free(ctx->cacertdir);
274 			if ((ctx->cacertdir = strdup(value)) == NULL) {
275 				dlog(0, "%s", strerror(errno));
276 				return 0;
277 			}
278 		} else if (strcmp(key, "gbasedn") == 0) {
279 			free(ctx->gbasedn);
280 			if ((ctx->gbasedn = strdup(value)) == NULL) {
281 				dlog(0, "%s", strerror(errno));
282 				return 0;
283 			}
284 		} else if (strcmp(key, "gfilter") == 0) {
285 			free(ctx->gfilter);
286 			if ((ctx->gfilter = strdup(value)) == NULL) {
287 				dlog(0, "%s", strerror(errno));
288 				return 0;
289 			}
290 		} else if (strcmp(key, "gscope") == 0) {
291 			if ((ctx->scope = getscope(value)) == -1)
292 				return 0;
293 		} else {
294 			dlog(0, "Unknown option '%s'", key);
295 			return 0;
296 		}
297 	}
298 	if (ferror(cf)) {
299 		dlog(0, "Can't read config file: %s", strerror(errno));
300 		return 0;
301 	}
302 	if (TAILQ_EMPTY(&(ctx->s))) {
303 		dlog(0, "Missing host");
304 		return 0;
305 	}
306 	if (ctx->basedn == NULL && ctx->binddn == NULL) {
307 		dlog(0, "Missing basedn or binddn");
308 		return 0;
309 	}
310 	return 1;
311 }
312 
313 int
do_conn(struct auth_ctx * ctx,struct aldap_url * url)314 do_conn(struct auth_ctx *ctx, struct aldap_url *url)
315 {
316 	struct addrinfo		 ai, *res, *res0;
317 	struct sockaddr_un	 un;
318 	struct aldap_message	*m;
319 	struct tls_config	*tls_config;
320 	const char		*errstr;
321 	char			 port[6];
322 	int			 fd, code;
323 
324 	dlog(1, "host %s, port %d", url->host, url->port);
325 
326 	if (url->protocol == LDAPI) {
327 		memset(&un, 0, sizeof(un));
328 		un.sun_family = AF_UNIX;
329 		if (strlcpy(un.sun_path, url->host,
330 		    sizeof(un.sun_path)) >= sizeof(un.sun_path)) {
331 			dlog(0, "socket '%s' too long", url->host);
332 			return 0;
333 		}
334 		if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1 ||
335 		    connect(fd, (struct sockaddr *)&un, sizeof(un)) == -1) {
336 			dlog(0, "can't create socket '%s'", url->host);
337 			return 0;
338 		}
339 	} else {
340 		memset(&ai, 0, sizeof(ai));
341 		ai.ai_family = AF_UNSPEC;
342 		ai.ai_socktype = SOCK_STREAM;
343 		ai.ai_protocol = IPPROTO_TCP;
344 		(void)snprintf(port, sizeof(port), "%u", url->port);
345 		if ((code = getaddrinfo(url->host, port,
346 		    &ai, &res0)) != 0) {
347 			dlog(0, "%s", gai_strerror(code));
348 			return 0;
349 		}
350 		for (res = res0; res; res = res->ai_next, fd = -1) {
351 			if ((fd = socket(res->ai_family, res->ai_socktype,
352 			    res->ai_protocol)) == -1)
353 				continue;
354 
355 			if (connect(fd, res->ai_addr, res->ai_addrlen) >= 0)
356 				break;
357 
358 			close(fd);
359 		}
360 		freeaddrinfo(res0);
361 		if (fd == -1)
362 			return 0;
363 	}
364 
365 	ctx->ld = aldap_init(fd);
366 	if (ctx->ld == NULL) {
367 		dlog(0, "aldap_open(%s:%hd) failed", url->host, url->port);
368 		return 0;
369 	}
370 
371 	dlog(1, "connect success!");
372 
373 	if (url->protocol == LDAPTLS) {
374 		dlog(1, "starttls!");
375 		if (aldap_req_starttls(ctx->ld) == -1) {
376 			dlog(0, "failed to request STARTTLS");
377 			goto fail;
378 		}
379 
380 		if ((m = aldap_parse(ctx->ld)) == NULL) {
381 			dlog(0, "failed to parse STARTTLS response");
382 			goto fail;
383 		}
384 
385 		if (ctx->ld->msgid != m->msgid ||
386 		    (code = aldap_get_resultcode(m)) != LDAP_SUCCESS) {
387 			dlog(0, "STARTTLS failed: %s(%d)",
388 			    ldap_resultcode(code), code);
389 			aldap_freemsg(m);
390 			goto fail;
391 		}
392 		aldap_freemsg(m);
393 	}
394 	if (url->protocol == LDAPTLS || url->protocol == LDAPS) {
395 		dlog(1, "%s: starting TLS", __func__);
396 
397 		if ((tls_config = tls_config_new()) == NULL) {
398 			dlog(0, "TLS config failed");
399 			goto fail;
400 		}
401 
402 		if (ctx->cacert != NULL &&
403 		    tls_config_set_ca_file(tls_config, ctx->cacert) == -1) {
404 			dlog(0, "Failed to set ca file %s", ctx->cacert);
405 			goto fail;
406 		}
407 		if (ctx->cacertdir != NULL &&
408 		    tls_config_set_ca_path(tls_config, ctx->cacertdir) == -1) {
409 			dlog(0, "Failed to set ca dir %s", ctx->cacertdir);
410 			goto fail;
411 		}
412 
413 		if (aldap_tls(ctx->ld, tls_config, url->host) < 0) {
414 			aldap_get_errno(ctx->ld, &errstr);
415 			dlog(0, "TLS failed: %s", errstr);
416 			goto fail;
417 		}
418 	}
419 	return 1;
420 fail:
421 	aldap_close(ctx->ld);
422 	return 0;
423 }
424 
425 int
conn(struct auth_ctx * ctx)426 conn(struct auth_ctx *ctx)
427 {
428 	struct aldap_urlq *url;
429 
430 	TAILQ_FOREACH(url, &(ctx->s), entries) {
431 		if (do_conn(ctx, &(url->s)))
432 			return 1;
433 	}
434 
435 	/* all the urls have failed */
436 	return 0;
437 }
438 
439 static int
getscope(char * scope)440 getscope(char *scope)
441 {
442 	if (scope == NULL || scope[0] == '\0')
443 		return LDAP_SCOPE_SUBTREE;
444 
445 	if (strcmp(scope, "base") == 0)
446 		return LDAP_SCOPE_BASE;
447 	else if (strcmp(scope, "one") == 0)
448 		return LDAP_SCOPE_ONELEVEL;
449 	else if (strcmp(scope, "sub") == 0)
450 		return LDAP_SCOPE_SUBTREE;
451 
452 	dlog(0, "Invalid scope");
453 	return -1;
454 }
455 
456 /*
457  * Convert format specifiers from the filter in login.conf to their
458  * real values. return the new filter in the filter argument.
459  */
460 char *
parse_filter(struct auth_ctx * ctx,const char * str)461 parse_filter(struct auth_ctx *ctx, const char *str)
462 {
463 	char tmp[PATH_MAX];
464 	char hostname[HOST_NAME_MAX+1];
465 	const char *p;
466 	char *q;
467 
468 	if (str == NULL)
469 		return NULL;
470 
471 	/*
472 	 * copy over from str to q, if we hit a %, substitute the real value,
473 	 * if we hit a NULL, its the end of the filter string
474 	 */
475 	for (p = str, q = tmp; p[0] != '\0' &&
476 	    ((size_t)(q - tmp) < sizeof(tmp)); p++) {
477 		if (p[0] == '%') {
478 			p++;
479 
480 			/* Make sure we can find the end of tmp for strlcat */
481 			q[0] = '\0';
482 
483 			/*
484 			 * Don't need to check strcat for truncation, since we
485 			 * will bail on the next iteration
486 			 */
487 			switch (p[0]) {
488 			case 'u': /* username */
489 				q = tmp + strlcat(tmp, ctx->user, sizeof(tmp));
490 				break;
491 			case 'h': /* hostname */
492 				if (gethostname(hostname, sizeof(hostname)) ==
493 				    -1) {
494 					dlog(0, "couldn't get host name for "
495 					    "%%h %s", strerror(errno));
496 					return NULL;
497 				}
498 				q = tmp + strlcat(tmp, hostname, sizeof(tmp));
499 				break;
500 			case 'd': /* user dn */
501 				if (ctx->userdn == NULL) {
502 					dlog(0, "no userdn has been recorded");
503 					return 0;
504 				}
505 				q = tmp + strlcat(tmp, ctx->userdn,
506 				    sizeof(tmp));
507 				break;
508 			case '%': /* literal % */
509 				q[0] = p[0];
510 				q++;
511 				break;
512 			default:
513 				dlog(0, "%s: invalid filter specifier",
514 				    __func__);
515 				return NULL;
516 			}
517 		} else {
518 			q[0] = p[0];
519 			q++;
520 		}
521 	}
522 	if ((size_t) (q - tmp) >= sizeof(tmp)) {
523 		dlog(0, "filter string too large, unable to process: %s", str);
524 		return NULL;
525 	}
526 
527 	q[0] = '\0';
528 	q = strdup(tmp);
529 	if (q == NULL) {
530 		dlog(0, "%s", strerror(errno));
531 		return NULL;
532 	}
533 
534 	return q;
535 }
536