~armaan/opendoas

22203dec632c0a030e3c83c39ae59feff8d4f8b0 — Armaan Bhojwani 3 months ago b5d211d
Remove PAM authentication option

I don't use it and don't want the potential security risk of having it
there
6 files changed, 6 insertions(+), 430 deletions(-)

M GNUmakefile
M README.md
M configure
M doas.c
M doas.h
D pam.c
M GNUmakefile => GNUmakefile +0 -1
@@ 27,7 27,6 @@ install: ${PROG} ${MAN}

uninstall:
	rm -f ${DESTDIR}${BINDIR}/${PROG}
	rm -f ${DESTDIR}${PAMDIR}/doas
	rm -f ${DESTDIR}${MANDIR}/man1/doas.1
	rm -f ${DESTDIR}${MANDIR}/man5/doas.conf.5


M README.md => README.md +3 -21
@@ 16,15 16,9 @@ There are a few steps you have to carefully consider before building and install
opendoas:

* There are less eyes on random doas ports, just because sudo had a vulnerability
  does not mean random doas ports are more secure if they are not reviewed
  or pam is configured incorrectly.
* If you want to use pam; You have to [configure pam](#pam-configuration)
  and failing to do so correctly might leave a big open door.
  does not mean random doas ports are more secure if they are not reviewed.
* Use the configure script.
* Use the default make target.
* If you really want to install a setuid binary that depends on
  pam being correctly configured, use the make install target
  to install the software.

## About the port



@@ 32,25 26,13 @@ This is not an official port/project from OpenBSD!

As much as possible I've attempted to stick to `doas` as tedu desired
it. As things stand it's essentially just code lifted from OpenBSD with
PAM or shadow based authentication glommed on to it.
shadow based authentication glommed on to it.

Compatibility functions in libopenbsd come from openbsd directly
(`strtonum.c`, `reallocarray.c`, `strlcpy.c`, `strlcat.c`),
from openssh (`readpassphrase.c`) or from sudo (`closefrom.c`).

The PAM and shadow authentication code does not come from the OpenBSD project.

### pam configuration

I will not ship pam configuration files, they are distribution specific and
its simply not safe or productive to ship and install those files.

If you want to use opendoas on your system and there is no package that
ships with a working pam configuration file, then you have to write and
test it yourself.

A good starting point is probably the distribution maintained `/etc/pam.d/sudo`
file.
The shadow authentication code does not come from the OpenBSD project.

### Persist/Timestamp/Timeout


M configure => configure +3 -40
@@ 23,9 23,6 @@ usage: configure [options]
  --enable-debug         enable debugging
  --enable-static        prepare for static build

  --without-pam          disable pam support
  --without-shadow       disable shadow support

  --with-timestamp       enable timestamp support

  --without-insults      disable insults


@@ 58,10 55,6 @@ for x; do
	--target) TARGET=$var ;;
	--enable-debug) DEBUG=yes ;;
	--enable-static) BUILD_STATIC=yes ;;
	--with-pam) WITHOUT_PAM=; WITHOUT_SHADOW=yes ;;
	--with-shadow) WITHOUT_SHADOW=; WITHOUT_PAM=yes ;;
	--without-pam) WITHOUT_PAM=yes ;;
	--without-shadow) WITHOUT_SHADOW=yes ;;
	--without-insults) WITHOUT_INSULTS=yes ;;
	--with-timestamp) WITHOUT_TIMESTAMP= ;;
	--without-timestamp) WITHOUT_TIMESTAMP=yes ;;


@@ 189,39 182,15 @@ check_func() {
}

authmethod() {
	#
	# Check for pam_appl.h.
	#
	src='
#include <security/pam_appl.h>
int main(void) {
	return 0;
}'
	[ -z "$WITHOUT_PAM" ] && check_func "pam_appl_h" "$src" && {
		printf 'SRCS     +=	pam.c\n' >>$CONFIG_MK
		printf 'LDLIBS +=	-lpam\n' >>$CONFIG_MK
		printf '#define USE_PAM\n' >>$CONFIG_H
		printf 'pam\n'
		return 0
	}

	#
	# Check for shadow.h.
	#
	src='
#include <shadow.h>
int main(void) {
	return 0;
}'
	[ -z "$WITHOUT_SHADOW" ] && check_func "shadow_h" "$src" && {
		printf 'SRCS     +=	shadow.c\n' >>$CONFIG_MK
		printf 'LDLIBS +=	-lcrypt\n' >>$CONFIG_MK
		printf '#define USE_SHADOW\n' >>$CONFIG_H
		printf 'shadow\n'
		return 0
	}

	return 1
printf 'SRCS     +=	shadow.c\n' >>$CONFIG_MK
printf 'LDLIBS +=	-lcrypt\n' >>$CONFIG_MK
return 0
}

definsults() {


@@ 554,12 523,6 @@ check_func "__attribute__" "$src" || {
}

auth=$(authmethod)
if [ $? -eq 0 ]; then
	printf 'Using auth method\t\t\t%s.\n' "$auth" >&2
else
	printf 'Error auth method\t\t\n' >&2
	exit 1
fi

insults=$(definsults)


M doas.c => doas.c +0 -12
@@ 362,7 362,6 @@ main(int argc, char **argv)
		errc(1, EPERM, NULL);
	}

#if defined(USE_SHADOW)
	if (!(rule->options & NOPASS)) {
		if (nflag)
			errx(1, "Authentication required");


@@ 371,12 370,6 @@ main(int argc, char **argv)
		if (ret == 5)
			authfail(rule->options & INSULT);
	}
#elif !defined(USE_PAM)
	/* no authentication provider, only allow NOPASS rules */
	(void) nflag;
	if (!(rule->options & NOPASS))
		errx(1, "Authentication required");
#endif

	if ((p = getenv("PATH")) != NULL)
		formerpath = strdup(p);


@@ 394,11 387,6 @@ main(int argc, char **argv)
	if (targpw == NULL)
		errx(1, "no passwd entry for target");

#if defined(USE_PAM)
	pamauth(targpw->pw_name, mypw->pw_name, !nflag, rule->options & NOPASS,
	    rule->options & PERSIST);
#endif

#ifdef HAVE_LOGIN_CAP_H
	if (setusercontext(NULL, targpw, target, LOGIN_SETGROUP |
	    LOGIN_SETPATH |

M doas.h => doas.h +0 -6
@@ 45,13 45,7 @@ char **prepenv(const struct rule *, const struct passwd *,
#define NOLOG		0x8
#define INSULT		0x10

#ifdef USE_PAM
int pamauth(const char *, const char *, int, int, int);
#endif

#ifdef USE_SHADOW
int shadowauth(const char *, int);
#endif

#ifdef USE_TIMESTAMP
int timestamp_open(int *, int);

D pam.c => pam.c +0 -350
@@ 1,350 0,0 @@
/*
 * Copyright (c) 2015 Nathan Holstein <nathan.holstein@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "config.h"

#include <sys/types.h>
#include <sys/wait.h>

#include <err.h>
#include <errno.h>
#include <limits.h>
#include <pwd.h>
#ifdef HAVE_READPASSPHRASE
#	include <readpassphrase.h>
#else
#	include "sys-readpassphrase.h"
#endif
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include <security/pam_appl.h>

#include "openbsd.h"
#include "doas.h"

#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX _POSIX_HOST_NAME_MAX
#endif

#define PAM_SERVICE_NAME "doas"

static pam_handle_t *pamh = NULL;
static char doas_prompt[128];
static sig_atomic_t volatile caught_signal = 0;

static void
catchsig(int sig)
{
	caught_signal = sig;
}

static char *
pamprompt(const char *msg, int echo_on, int *ret)
{
	const char *prompt;
	char *pass, buf[PAM_MAX_RESP_SIZE];
	int flags = RPP_REQUIRE_TTY | (echo_on ? RPP_ECHO_ON : RPP_ECHO_OFF);

	/* overwrite default prompt if it matches "Password:[ ]" */
	if (strncmp(msg,"Password:", 9) == 0 &&
	    (msg[9] == '\0' || (msg[9] == ' ' && msg[10] == '\0')))
		prompt = doas_prompt;
	else
		prompt = msg;

	pass = readpassphrase(prompt, buf, sizeof(buf), flags);
	if (!pass)
		*ret = PAM_CONV_ERR;
	else if (!(pass = strdup(pass)))
		*ret = PAM_BUF_ERR;
	else
		*ret = PAM_SUCCESS;

	explicit_bzero(buf, sizeof(buf));
	return pass;
}

static int
pamconv(int nmsgs, const struct pam_message **msgs,
		struct pam_response **rsps, __UNUSED void *ptr)
{
	struct pam_response *rsp;
	int i, style;
	int ret;

	if (!(rsp = calloc(nmsgs, sizeof(struct pam_response))))
		err(1, "could not allocate pam_response");

	for (i = 0; i < nmsgs; i++) {
		switch (style = msgs[i]->msg_style) {
		case PAM_PROMPT_ECHO_OFF:
		case PAM_PROMPT_ECHO_ON:
			rsp[i].resp = pamprompt(msgs[i]->msg, style == PAM_PROMPT_ECHO_ON, &ret);
			if (ret != PAM_SUCCESS)
				goto fail;
			break;

		case PAM_ERROR_MSG:
		case PAM_TEXT_INFO:
			if (fprintf(style == PAM_ERROR_MSG ? stderr : stdout,
			    "%s\n", msgs[i]->msg) < 0)
				goto fail;
			break;

		default:
			errx(1, "invalid PAM msg_style %d", style);
		}
	}

	*rsps = rsp;
	rsp = NULL;

	return PAM_SUCCESS;

fail:
	/* overwrite and free response buffers */
	for (i = 0; i < nmsgs; i++) {
		if (rsp[i].resp == NULL)
			continue;
		switch (msgs[i]->msg_style) {
		case PAM_PROMPT_ECHO_OFF:
		case PAM_PROMPT_ECHO_ON:
			explicit_bzero(rsp[i].resp, strlen(rsp[i].resp));
			free(rsp[i].resp);
		}
		rsp[i].resp = NULL;
	}
	free(rsp);

	return PAM_CONV_ERR;
}

void
pamcleanup(int ret, int sess, int cred)
{
	if (sess) {
		ret = pam_close_session(pamh, 0);
		if (ret != PAM_SUCCESS)
			errx(1, "pam_close_session: %s", pam_strerror(pamh, ret));
	}
	if (cred) {
		ret = pam_setcred(pamh, PAM_DELETE_CRED | PAM_SILENT);
		if (ret != PAM_SUCCESS)
			warn("pam_setcred(?, PAM_DELETE_CRED | PAM_SILENT): %s",
			    pam_strerror(pamh, ret));
	}
	pam_end(pamh, ret);
}

void
watchsession(pid_t child, int sess, int cred)
{
	sigset_t sigs;
	struct sigaction act, oldact;
	int status = 1;

	/* block signals */
	sigfillset(&sigs);
	if (sigprocmask(SIG_BLOCK, &sigs, NULL)) {
		warn("failed to block signals");
		caught_signal = 1;
		goto close;
	}

	/* setup signal handler */
	act.sa_handler = catchsig;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;

	/* unblock SIGTERM and SIGALRM to catch them */
	sigemptyset(&sigs);
	if (sigaddset(&sigs, SIGTERM) ||
	    sigaddset(&sigs, SIGALRM) ||
	    sigaddset(&sigs, SIGTSTP) ||
	    sigaction(SIGTERM, &act, &oldact) ||
	    sigprocmask(SIG_UNBLOCK, &sigs, NULL)) {
		warn("failed to set signal handler");
		caught_signal = 1;
		goto close;
	}

	/* wait for child to be terminated */
	if (waitpid(child, &status, 0) != -1) {
		if (WIFSIGNALED(status)) {
			fprintf(stderr, "%s%s\n", strsignal(WTERMSIG(status)),
					WCOREDUMP(status) ? " (core dumped)" : "");
			status = WTERMSIG(status) + 128;
		} else
			status = WEXITSTATUS(status);
	}
	else if (caught_signal)
		status = caught_signal + 128;
	else
		status = 1;

close:
	if (caught_signal && child != (pid_t)-1) {
		fprintf(stderr, "\nSession terminated, killing shell\n");
		kill(child, SIGTERM);
	}

	pamcleanup(PAM_SUCCESS, sess, cred);

	if (caught_signal) {
		if (child != (pid_t)-1) {
			/* kill child */
			sleep(2);
			kill(child, SIGKILL);
			fprintf(stderr, " ...killed.\n");
		}

		/* unblock cached signal and resend */
		sigaction(SIGTERM, &oldact, NULL);
		if (caught_signal != SIGTERM)
			caught_signal = SIGKILL;
		kill(getpid(), caught_signal);
	}

	exit(status);
}

int
pamauth(const char *user, const char *myname, int interactive, int nopass, int persist)
{
	static const struct pam_conv conv = {
		.conv = pamconv,
		.appdata_ptr = NULL,
	};
	const char *ttydev;
	pid_t child;
	int ret, sess = 0, cred = 0;

#ifdef USE_TIMESTAMP
	int fd = -1;
	int valid = 0;
#else
	(void) persist;
#endif

	if (!user || !myname)
		return(5);

	ret = pam_start(PAM_SERVICE_NAME, myname, &conv, &pamh);
	if (ret != PAM_SUCCESS)
		errx(1, "pam_start(\"%s\", \"%s\", ?, ?): failed",
		    PAM_SERVICE_NAME, myname);

	ret = pam_set_item(pamh, PAM_RUSER, myname);
	if (ret != PAM_SUCCESS)
		warn("pam_set_item(?, PAM_RUSER, \"%s\"): %s",
		    pam_strerror(pamh, ret), myname);

	if (isatty(0) && (ttydev = ttyname(0)) != NULL) {
		if (strncmp(ttydev, "/dev/", 5) == 0)
			ttydev += 5;

		ret = pam_set_item(pamh, PAM_TTY, ttydev);
		if (ret != PAM_SUCCESS)
			warn("pam_set_item(?, PAM_TTY, \"%s\"): %s",
			    ttydev, pam_strerror(pamh, ret));
	}


#ifdef USE_TIMESTAMP
	if (persist)
		fd = timestamp_open(&valid, 5 * 60);
	if (fd != -1 && valid == 1)
		nopass = 1;
#endif

	if (!nopass) {
		if (!interactive)
			return(5);

		/* doas style prompt for pam */
		char host[HOST_NAME_MAX + 1];
		if (gethostname(host, sizeof(host)))
			snprintf(host, sizeof(host), "?");
		snprintf(doas_prompt, sizeof(doas_prompt),
		    "\rdoas (%.32s@%.32s) password: ", myname, host);

		/* authenticate */
		ret = pam_authenticate(pamh, 0);
		if (ret != PAM_SUCCESS) {
			pamcleanup(ret, sess, cred);
			syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed auth for %s", myname);
			return(5);
		}
	}


	ret = pam_acct_mgmt(pamh, 0);
	if (ret == PAM_NEW_AUTHTOK_REQD)
		ret = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);

	/* account not vaild or changing the auth token failed */
	if (ret != PAM_SUCCESS) {
		pamcleanup(ret, sess, cred);
		syslog(LOG_AUTHPRIV | LOG_NOTICE, "failed auth for %s", myname);
		return(5);
	}

	/* set PAM_USER to the user we want to be */
	ret = pam_set_item(pamh, PAM_USER, user);
	if (ret != PAM_SUCCESS)
		warn("pam_set_item(?, PAM_USER, \"%s\"): %s", user,
		    pam_strerror(pamh, ret));

	ret = pam_setcred(pamh, PAM_REINITIALIZE_CRED);
	if (ret != PAM_SUCCESS)
		warn("pam_setcred(?, PAM_REINITIALIZE_CRED): %s", pam_strerror(pamh, ret));
	else
		cred = 1;

	/* open session */
	ret = pam_open_session(pamh, 0);
	if (ret != PAM_SUCCESS)
		errx(1, "pam_open_session: %s", pam_strerror(pamh, ret));
	sess = 1;

	if ((child = fork()) == -1) {
		pamcleanup(PAM_ABORT, sess, cred);
		err(1, "fork");
	}

	/* return as child */
	if (child == 0) {
#ifdef USE_TIMESTAMP
		if (fd != -1)
			close(fd);
#endif
		return;
	}

#ifdef USE_TIMESTAMP
	if (fd != -1) {
		timestamp_set(fd, 5 * 60);
		close(fd);
	}
#endif
	watchsession(child, sess, cred);
	return(0);
}