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
56int debug = 0;
57
58static int getscope(char *);
59
60void
61dlog(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
82const char *
83ldap_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
132static int
133parse_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
170int
171parse_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
313int
314do_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;
420fail:
421	aldap_close(ctx->ld);
422	return 0;
423}
424
425int
426conn(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
439static int
440getscope(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 */
460char *
461parse_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