xref: /netbsd-src/external/bsd/openpam/dist/lib/libpam/openpam_configure.c (revision 0d9d0fd8a30be9a1924e715bbcf67a4a83efd262)
1 /*	$NetBSD: openpam_configure.c,v 1.5 2023/06/30 21:46:20 christos Exp $	*/
2 
3 /*-
4  * Copyright (c) 2001-2003 Networks Associates Technology, Inc.
5  * Copyright (c) 2004-2015 Dag-Erling Smørgrav
6  * All rights reserved.
7  *
8  * This software was developed for the FreeBSD Project by ThinkSec AS and
9  * Network Associates Laboratories, the Security Research Division of
10  * Network Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
11  * ("CBOSS"), as part of the DARPA CHATS research program.
12  *
13  * Redistribution and use in source and binary forms, with or without
14  * modification, are permitted provided that the following conditions
15  * are met:
16  * 1. Redistributions of source code must retain the above copyright
17  *    notice, this list of conditions and the following disclaimer.
18  * 2. Redistributions in binary form must reproduce the above copyright
19  *    notice, this list of conditions and the following disclaimer in the
20  *    documentation and/or other materials provided with the distribution.
21  * 3. The name of the author may not be used to endorse or promote
22  *    products derived from this software without specific prior written
23  *    permission.
24  *
25  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
26  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
29  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
31  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
32  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
34  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35  * SUCH DAMAGE.
36  */
37 
38 #ifdef HAVE_CONFIG_H
39 # include "config.h"
40 #endif
41 
42 #include <sys/cdefs.h>
43 __RCSID("$NetBSD: openpam_configure.c,v 1.5 2023/06/30 21:46:20 christos Exp $");
44 
45 #include <sys/param.h>
46 
47 #include <errno.h>
48 #include <stdio.h>
49 #include <stdlib.h>
50 #include <string.h>
51 
52 #include <security/pam_appl.h>
53 
54 #include "openpam_impl.h"
55 #include "openpam_ctype.h"
56 #include "openpam_strlcat.h"
57 #include "openpam_strlcpy.h"
58 
59 static int openpam_load_chain(pam_handle_t *, const char *, pam_facility_t);
60 
61 /*
62  * Validate a service name.
63  *
64  * Returns a non-zero value if the argument points to a NUL-terminated
65  * string consisting entirely of characters in the POSIX portable filename
66  * character set, excluding the path separator character.
67  */
68 static int
valid_service_name(const char * name)69 valid_service_name(const char *name)
70 {
71 	const char *p;
72 
73 	if (OPENPAM_FEATURE(RESTRICT_SERVICE_NAME)) {
74 		/* path separator not allowed */
75 		for (p = name; *p != '\0'; ++p)
76 			if (!is_pfcs(*p))
77 				return (0);
78 	} else {
79 		/* path separator allowed */
80 		for (p = name; *p != '\0'; ++p)
81 			if (!is_pfcs(*p) && *p != '/')
82 				return (0);
83 	}
84 	return (1);
85 }
86 
87 /*
88  * Parse the facility name.
89  *
90  * Returns the corresponding pam_facility_t value, or -1 if the argument
91  * is not a valid facility name.
92  */
93 static pam_facility_t
parse_facility_name(const char * name)94 parse_facility_name(const char *name)
95 {
96 	int i;
97 
98 	for (i = 0; i < PAM_NUM_FACILITIES; ++i)
99 		if (strcmp(pam_facility_name[i], name) == 0)
100 			return (i);
101 	return ((pam_facility_t)-1);
102 }
103 
104 /*
105  * Parse the control flag.
106  *
107  * Returns the corresponding pam_control_t value, or -1 if the argument is
108  * not a valid control flag name.
109  */
110 static pam_control_t
parse_control_flag(const char * name)111 parse_control_flag(const char *name)
112 {
113 	pam_control_t i;
114 
115 	for (i = PAM_BINDING; i < PAM_NUM_CONTROL_FLAGS; ++i)
116 		if (strcmp(pam_control_flag_name[i], name) == 0)
117 			return (i);
118 	return ((pam_control_t)-1);
119 }
120 
121 /*
122  * Validate a file name.
123  *
124  * Returns a non-zero value if the argument points to a NUL-terminated
125  * string consisting entirely of characters in the POSIX portable filename
126  * character set, including the path separator character.
127  */
128 static int
valid_module_name(const char * name)129 valid_module_name(const char *name)
130 {
131 	const char *p;
132 
133 	if (OPENPAM_FEATURE(RESTRICT_MODULE_NAME)) {
134 		/* path separator not allowed */
135 		for (p = name; *p != '\0'; ++p)
136 			if (!is_pfcs(*p))
137 				return (0);
138 	} else {
139 		/* path separator allowed */
140 		for (p = name; *p != '\0'; ++p)
141 			if (!is_pfcs(*p) && *p != '/')
142 				return (0);
143 	}
144 	return (1);
145 }
146 
147 typedef enum { pam_conf_style, pam_d_style } openpam_style_t;
148 
149 /*
150  * Extracts given chains from a policy file.
151  *
152  * Returns the number of policy entries which were found for the specified
153  * service and facility, or -1 if a system error occurred or a syntax
154  * error was encountered.
155  */
156 static int
openpam_parse_chain(pam_handle_t * pamh,const char * service,pam_facility_t facility,FILE * f,const char * filename,openpam_style_t style)157 openpam_parse_chain(pam_handle_t *pamh,
158 	const char *service,
159 	pam_facility_t facility,
160 	FILE *f,
161 	const char *filename,
162 	openpam_style_t style)
163 {
164 	pam_chain_t *this, **next;
165 	pam_facility_t fclt;
166 	pam_control_t ctlf;
167 	char *name, *servicename, *modulename;
168 	int count, lineno, ret, serrno;
169 	char **wordv, *word;
170 	int i, wordc;
171 
172 	count = 0;
173 	this = NULL;
174 	name = NULL;
175 	lineno = 0;
176 	wordc = 0;
177 	wordv = NULL;
178 	while ((wordv = openpam_readlinev(f, &lineno, &wordc)) != NULL) {
179 		/* blank line? */
180 		if (wordc == 0) {
181 			FREEV(wordc, wordv);
182 			continue;
183 		}
184 		i = 0;
185 
186 		/* check service name if necessary */
187 		if (style == pam_conf_style &&
188 		    strcmp(wordv[i++], service) != 0) {
189 			FREEV(wordc, wordv);
190 			continue;
191 		}
192 
193 		/* check facility name */
194 		if ((word = wordv[i++]) == NULL ||
195 		    (fclt = parse_facility_name(word)) == (pam_facility_t)-1) {
196 			openpam_log(PAM_LOG_ERROR,
197 			    "%s(%d): missing or invalid facility",
198 			    filename, lineno);
199 			errno = EINVAL;
200 			goto fail;
201 		}
202 		if (facility != fclt && facility != PAM_FACILITY_ANY) {
203 			FREEV(wordc, wordv);
204 			continue;
205 		}
206 
207 		/* check for "include" */
208 		if ((word = wordv[i++]) != NULL &&
209 		    strcmp(word, "include") == 0) {
210 			if ((servicename = wordv[i++]) == NULL ||
211 			    !valid_service_name(servicename)) {
212 				openpam_log(PAM_LOG_ERROR,
213 				    "%s(%d): missing or invalid service name",
214 				    filename, lineno);
215 				errno = EINVAL;
216 				goto fail;
217 			}
218 			if (wordv[i] != NULL) {
219 				openpam_log(PAM_LOG_ERROR,
220 				    "%s(%d): garbage at end of line",
221 				    filename, lineno);
222 				errno = EINVAL;
223 				goto fail;
224 			}
225 			ret = openpam_load_chain(pamh, servicename, fclt);
226 			FREEV(wordc, wordv);
227 			if (ret < 0) {
228 				/*
229 				 * Bogus errno, but this ensures that the
230 				 * outer loop does not just ignore the
231 				 * error and keep searching.
232 				 */
233 				if (errno == ENOENT)
234 					errno = EINVAL;
235 				goto fail;
236 			}
237 			continue;
238 		}
239 
240 		/* get control flag */
241 		if (word == NULL || /* same word we compared to "include" */
242 		    (ctlf = parse_control_flag(word)) == (pam_control_t)-1) {
243 			openpam_log(PAM_LOG_ERROR,
244 			    "%s(%d): missing or invalid control flag",
245 			    filename, lineno);
246 			errno = EINVAL;
247 			goto fail;
248 		}
249 
250 		/* get module name */
251 		if ((modulename = wordv[i++]) == NULL ||
252 		    !valid_module_name(modulename)) {
253 			openpam_log(PAM_LOG_ERROR,
254 			    "%s(%d): missing or invalid module name",
255 			    filename, lineno);
256 			errno = EINVAL;
257 			goto fail;
258 		}
259 
260 		/* allocate new entry */
261 		if ((this = calloc((size_t)1, sizeof *this)) == NULL)
262 			goto syserr;
263 		this->flag = (int)ctlf;
264 
265 		/* load module */
266 		if ((this->module = openpam_load_module(modulename)) == NULL) {
267 			if (errno == ENOENT)
268 				errno = ENOEXEC;
269 			goto fail;
270 		}
271 
272 		/*
273 		 * The remaining items in wordv are the module's
274 		 * arguments.  We could set this->optv = wordv + i, but
275 		 * then free(this->optv) wouldn't work.  Instead, we free
276 		 * the words we've already consumed, shift the rest up,
277 		 * and clear the tail end of the array.
278 		 */
279 		this->optc = wordc - i;
280 		for (i = 0; i < wordc - this->optc; ++i) {
281 			FREE(wordv[i]);
282 		}
283 		for (i = 0; i < this->optc; ++i) {
284 			wordv[i] = wordv[wordc - this->optc + i];
285 			wordv[wordc - this->optc + i] = NULL;
286 		}
287 		this->optv = wordv;
288 		wordv = NULL;
289 		wordc = 0;
290 
291 		/* hook it up */
292 		for (next = &pamh->chains[fclt]; *next != NULL;
293 		     next = &(*next)->next)
294 			/* nothing */ ;
295 		*next = this;
296 		this = NULL;
297 		++count;
298 	}
299 	/*
300 	 * The loop ended because openpam_readword() returned NULL, which
301 	 * can happen for four different reasons: an I/O error (ferror(f)
302 	 * is true), a memory allocation failure (ferror(f) is false,
303 	 * feof(f) is false, errno is non-zero), the file ended with an
304 	 * unterminated quote or backslash escape (ferror(f) is false,
305 	 * feof(f) is true, errno is non-zero), or the end of the file was
306 	 * reached without error (ferror(f) is false, feof(f) is true,
307 	 * errno is zero).
308 	 */
309 	if (ferror(f) || errno != 0)
310 		goto syserr;
311 	if (!feof(f))
312 		goto fail;
313 	fclose(f);
314 	return (count);
315 syserr:
316 	serrno = errno;
317 	openpam_log(PAM_LOG_ERROR, "%s: %m", filename);
318 	errno = serrno;
319 	/* fall through */
320 fail:
321 	serrno = errno;
322 	if (this && this->optc && this->optv)
323 		FREEV(this->optc, this->optv);
324 	FREE(this);
325 	FREEV(wordc, wordv);
326 	FREE(wordv);
327 	FREE(name);
328 	fclose(f);
329 	errno = serrno;
330 	return (-1);
331 }
332 
333 /*
334  * Read the specified chains from the specified file.
335  *
336  * Returns 0 if the file exists but does not contain any matching lines.
337  *
338  * Returns -1 and sets errno to ENOENT if the file does not exist.
339  *
340  * Returns -1 and sets errno to some other non-zero value if the file
341  * exists but is unsafe or unreadable, or an I/O error occurs.
342  */
343 static int
openpam_load_file(pam_handle_t * pamh,const char * service,pam_facility_t facility,const char * filename,openpam_style_t style)344 openpam_load_file(pam_handle_t *pamh,
345 	const char *service,
346 	pam_facility_t facility,
347 	const char *filename,
348 	openpam_style_t style)
349 {
350 	FILE *f;
351 	int ret, serrno;
352 
353 	/* attempt to open the file */
354 	if ((f = fopen(filename, "r")) == NULL) {
355 		serrno = errno;
356 		openpam_log(errno == ENOENT ? PAM_LOG_DEBUG : PAM_LOG_ERROR,
357 		    "%s: %m", filename);
358 		errno = serrno;
359 		RETURNN(-1);
360 	} else {
361 		openpam_log(PAM_LOG_DEBUG, "found %s", filename);
362 	}
363 
364 	/* verify type, ownership and permissions */
365 	if (OPENPAM_FEATURE(VERIFY_POLICY_FILE) &&
366 	    openpam_check_desc_owner_perms(filename, fileno(f)) != 0) {
367 		/* already logged the cause */
368 		serrno = errno;
369 		fclose(f);
370 		errno = serrno;
371 		RETURNN(-1);
372 	}
373 
374 	/* parse the file */
375 	ret = openpam_parse_chain(pamh, service, facility,
376 	    f, filename, style);
377 	RETURNN(ret);
378 }
379 
380 /*
381  * Locates the policy file for a given service and reads the given chains
382  * from it.
383  *
384  * Returns the number of policy entries which were found for the specified
385  * service and facility, or -1 if a system error occurred or a syntax
386  * error was encountered.
387  */
388 static int
openpam_load_chain(pam_handle_t * pamh,const char * service,pam_facility_t facility)389 openpam_load_chain(pam_handle_t *pamh,
390 	const char *service,
391 	pam_facility_t facility)
392 {
393 	const char *p, **path;
394 	char filename[PATH_MAX];
395 	size_t len;
396 	openpam_style_t style;
397 	int ret;
398 
399 	ENTERS(facility < 0 ? "any" : pam_facility_name[facility]);
400 
401 	/* either absolute or relative to cwd */
402 	if (strchr(service, '/') != NULL) {
403 		if ((p = strrchr(service, '.')) != NULL && strcmp(p, ".conf") == 0)
404 			style = pam_conf_style;
405 		else
406 			style = pam_d_style;
407 		ret = openpam_load_file(pamh, service, facility,
408 		    service, style);
409 		RETURNN(ret);
410 	}
411 
412 	/* search standard locations */
413 	for (path = openpam_policy_path; *path != NULL; ++path) {
414 		/* construct filename */
415 		len = strlcpy(filename, *path, sizeof filename);
416 		if (len >= sizeof filename) {
417 			errno = ENAMETOOLONG;
418 			RETURNN(-1);
419 		}
420 		if (filename[len - 1] == '/') {
421 			len = strlcat(filename, service, sizeof filename);
422 			if (len >= sizeof filename) {
423 				errno = ENAMETOOLONG;
424 				RETURNN(-1);
425 			}
426 			style = pam_d_style;
427 		} else {
428 			style = pam_conf_style;
429 		}
430 		ret = openpam_load_file(pamh, service, facility,
431 		    filename, style);
432 		/* success */
433 		if (ret > 0)
434 			RETURNN(ret);
435 		/* the file exists, but an error occurred */
436 		if (ret == -1 && errno != ENOENT)
437 			RETURNN(ret);
438 		/* in pam.d style, an empty file counts as a hit */
439 		if (ret == 0 && style == pam_d_style)
440 			RETURNN(ret);
441 	}
442 
443 	/* no hit */
444 	errno = ENOENT;
445 	RETURNN(-1);
446 }
447 
448 /*
449  * OpenPAM internal
450  *
451  * Configure a service
452  */
453 
454 int
openpam_configure(pam_handle_t * pamh,const char * service)455 openpam_configure(pam_handle_t *pamh,
456 	const char *service)
457 {
458 	pam_facility_t fclt;
459 	int serrno;
460 
461 	ENTERS(service);
462 	if (!valid_service_name(service)) {
463 		openpam_log(PAM_LOG_ERROR, "invalid service name");
464 		RETURNC(PAM_SYSTEM_ERR);
465 	}
466 	if (openpam_load_chain(pamh, service, PAM_FACILITY_ANY) < 0) {
467 		if (errno != ENOENT)
468 			goto load_err;
469 	}
470 	for (fclt = 0; fclt < PAM_NUM_FACILITIES; ++fclt) {
471 		if (pamh->chains[fclt] != NULL)
472 			continue;
473 		if (OPENPAM_FEATURE(FALLBACK_TO_OTHER)) {
474 			if (openpam_load_chain(pamh, PAM_OTHER, fclt) < 0)
475 				goto load_err;
476 		}
477 	}
478 #ifdef __NetBSD__
479 	/*
480 	 * On NetBSD we require the AUTH chain to have a binding,
481 	 * a required, or requisite module.
482 	 */
483 	{
484 		pam_chain_t *this = pamh->chains[PAM_AUTH];
485 		for (; this != NULL; this = this->next)
486 			if (this->flag == PAM_BINDING ||
487 			    this->flag == PAM_REQUIRED ||
488 			    this->flag == PAM_REQUISITE)
489 				break;
490 		if (this == NULL) {
491 			openpam_log(PAM_LOG_ERROR,
492 			    "No required, requisite, or binding component "
493 			    "in service %s, facility %s",
494 			    service, pam_facility_name[PAM_AUTH]);
495 			goto load_err;
496 		}
497 	}
498 #endif
499 	RETURNC(PAM_SUCCESS);
500 load_err:
501 	serrno = errno;
502 	openpam_clear_chains(pamh->chains);
503 	errno = serrno;
504 	RETURNC(PAM_SYSTEM_ERR);
505 }
506 
507 /*
508  * NODOC
509  *
510  * Error codes:
511  *	PAM_SYSTEM_ERR
512  */
513