В UNIX-системах обычно выполняется стандартная аутентификация с использованием файлов паролей - /etc/passwd и /etc/shadow – но могут использоваться и различные схемы, связанные с централизованным хранением базы данных пользователей и паролей (NIS, RADIUS, контроллер домена Windows и т.п.). Изначально не было единого механизма, обеспечивающего общий интерфейс аутентификации для любой сетевой службы и любого способа аутентификации.
PAM (присоединяемые модули аутентификации, pluggable authentication modules) разработали Vipin Samar и Charlie Lai из Sun Microsystems в 1995 году. Эта модель предполагает, что любое приложение будет работать со стандартным интерфейсом аутентификации, но механизм аутентификации при этом может быть различным. Также предполагалось наличие возможности выбрать, какой вид аутентификации будет использоваться в системе по умолчанию и/или для каждой из служб в отдельности. В 1997 году Open Group опубликовала предварительные спецификации на X/Open Single Sign-on (XSSO), что стандартизировало API для PAM и добавило расширения для одноразовой или интегрированной подписи.
В PAM с каждым приложением можно связать несколько механизмов аутентификации, а общий интерфейс системы аутентификации для всех приложений означает, что системные подпрограммы, полагающиеся на систему аутентификации, не зависят от изменений этой системы. Архитектура PAM модульная и позволяет добавить любой модуль с поддержкой любого алгоритма аутентификации, но для обратной совместимости PAM API поддерживает ранее существовавшие вызовы подпрограмм аутентификации.
PAM поддерживается в Linux, FreeBSD, Solaris, HP-UX и в ряде других UNIX-подобных систем. Со стороны приложения PAM предоставляет API для обращения к функциям аутентификации. Приложение вызывает функцию PAM API в ситуации, требующей проведения аутентификации. Со стороны модуля аутентификации PAM располагает интерфейсом SPI (Service Provider Interface), через который вызов API транслируется к источнику аутентификации. API является общим для всех программ, а SPI содержит по одному модулю на каждый вариант аутентификации.
Структура PAM (из PAM Administration)
Далее приведена минимальная реализация модуля pam_unix(8), предоставляющая только сервисы аутентификации (присутствует расширение библиотеки
OpenPAM – функция pam_get_authtok(3)):
#include <sys/param.h>
#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <security/pam_modules.h>
#include <security/pam_appl.h>
#ifndef _OPENPAM
static char password_prompt[] = "Password:";
#endif
#ifndef PAM_EXTERN
#define PAM_EXTERN
#endif
PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags,
int argc, const char *argv[])
{
#ifndef _OPENPAM
struct pam_conv *conv;
struct pam_message msg;
const struct pam_message *msgp;
struct pam_response *resp;
#endif
struct passwd *pwd;
const char *user;
char *crypt_password, *password;
int pam_err, retry;
/* identify user */
if ((pam_err = pam_get_user(pamh, &user, NULL)) != PAM_SUCCESS)
return (pam_err);
if ((pwd = getpwnam(user)) == NULL)
return (PAM_USER_UNKNOWN);
/* get password */
#ifndef _OPENPAM
pam_err = pam_get_item(pamh, PAM_CONV, (const void **)&conv);
if (pam_err != PAM_SUCCESS)
return (PAM_SYSTEM_ERR);
msg.msg_style = PAM_PROMPT_ECHO_OFF;
msg.msg = password_prompt;
msgp = &msg;
#endif
for (retry = 0; retry < 3; ++retry) {
#ifdef _OPENPAM
pam_err = pam_get_authtok(pamh, PAM_AUTHTOK,
(const char **)&password, NULL);
#else
resp = NULL;
pam_err = (*conv->conv)(1, &msgp, &resp, conv->appdata_ptr);
if (resp != NULL) {
if (pam_err == PAM_SUCCESS)
password = resp->resp;
else
free(resp->resp);
free(resp);
}
#endif
if (pam_err == PAM_SUCCESS)
break;
}
if (pam_err == PAM_CONV_ERR)
return (pam_err);
if (pam_err != PAM_SUCCESS)
return (PAM_AUTH_ERR);
/* compare passwords */
if ((!pwd->pw_passwd[0] && (flags & PAM_DISALLOW_NULL_AUTHTOK)) ||
(crypt_password = crypt(password, pwd->pw_passwd)) == NULL ||
strcmp(crypt_password, pwd->pw_passwd) != 0)
pam_err = PAM_AUTH_ERR;
else
pam_err = PAM_SUCCESS;
#ifndef _OPENPAM
free(password);
#endif
return (pam_err);
}
PAM_EXTERN int
pam_sm_setcred(pam_handle_t *pamh, int flags,
int argc, const char *argv[])
{
return (PAM_SUCCESS);
}
PAM_EXTERN int
pam_sm_acct_mgmt(pam_handle_t *pamh, int flags,
int argc, const char *argv[])
{
return (PAM_SUCCESS);
}
PAM_EXTERN int
pam_sm_open_session(pam_handle_t *pamh, int flags,
int argc, const char *argv[])
{
return (PAM_SUCCESS);
}
PAM_EXTERN int
pam_sm_close_session(pam_handle_t *pamh, int flags,
int argc, const char *argv[])
{
return (PAM_SUCCESS);
}
PAM_EXTERN int
pam_sm_chauthtok(pam_handle_t *pamh, int flags,
int argc, const char *argv[])
{
return (PAM_SERVICE_ERR);
}
#ifdef PAM_MODULE_ENTRY
PAM_MODULE_ENTRY("pam_unix");
#endif
В Solaris настройки PAM хранятся в файле /etc/pam.conf, в некоторых других системах допускается их хранение в каталоге /etc/pam.d/. Если такой каталог существует, то подпрограммы PAM игнорируют /etc/pam.conf.
Аутентификацию через PAM используют программы login, passwd, su, rlogind, rshd, telnetd, ftpd, rpc.rexd, uucpd, init, sac, cron, ppp, dtsession, ssh и ttymon, а также dtlogin и gdm.
Модули PAM в принципе могут располагаться в любых каталогах, но чаще всего файл имеет имя, начинающееся с pam - pam_modulename.so.x - и хранится в подкаталогах каталога /usr/lib/security.
Модуль PAM фактически представляет собой динамическую библиотеку (файл с расширением .so).
В модели PAM аутентификация состоит из нескольких независимых задач:
- управление учетными записями;
- управление аутентификацией;
- управление паролями;
- управление сессиями.
Для описания этих задач в конфигурационном файле используются сокращения account, auth, password и session соответственно.
Account описывает службу проверки учетной записи;
authentication - службу проверки идентичности пользователя (это и есть аутентификация), причем идентификацию может выполнять оборудование, которое общается с написанным специально для него модулем PAM (например, аутентификация по смарт-картам);
password описывает службу изменения пароля, а
session определяет действия, которые должны быть выполнены до того, как ресурс будет предоставлен, или после того, как он будет освобожден (запись начала и конца соединения, монтирование домашнего каталога и т.п.).
Конфигурационный файл /etc/pam.conf определяет соответствие между приложением и PAM-модулями, которые выполняют аутентификацию. При обращении приложения к PAM для аутентификации происходит инициализация соединения приложения с PAM API и этом читается файл /etc/pam.conf. Пример такого файла (с модулем pam_ldap):
#
# PAM configuration file /etc/pam.conf
#
<…>
other auth requisite pam_authtok_get.so.1
# other auth required pam_dhkeys.so.1
other auth required pam_unix_cred.so.1
other auth sufficient pam_unix_auth.so.1
other auth required pam_ldap.so.1
other account requisite pam_roles.so.1
other account sufficient pam_unix_account.so.1
other account required pam_ldap.so.1
<…>
Как видно из приведенного отрывка, файл /etc/pam.conf состоит из правил – по одному правилу в строке. Если нужно, чтобы правило продолжалось на следующей строке, в конце продолжающейся строки должен быть символ "\" для экранирования следующего за ним перевода строки. Комментарии начинаются со знака "#" и продолжаются до конца строки.
Каждое правило состоит из пяти полей, которые отделяются друг от друга пробелами, причем первые три поля нечувствительны к регистру букв:
приложение задача управление путь к модулю аргументы вызова модуля
Имя приложения other зарезервировано для указания модулей аутентификации по умолчанию (не указанных явным образом в файле конфигурации). Несколько правил могут быть организованы в стек для последовательной аутентификации несколькими PAM-модулями.
"Управление" определяет поведение PAM в случае, если модуль примет решение об отказе в аутентификации. Возможные варианты действий:
- requisite – отказ приводит к немедленному прекращению аутентификации;
- required – отказ приводит к тому, что PAM API возвращает ошибку, но только после того, как будут вызваны все оставшиеся в стеке модули (для этого приложения);
- sufficient – успешной аутентификации достаточно для удовлетворения требований по аутентификации стека модулей (и если предыдущий модуль в стеке выдал отказ в аутентификации, то успех аутентификации текущего модуля игнорируется);
- optional – успех или отказ в аутентификации с использованием данного модуля имеет значение, только если это единственный модуль в стеке, ассоциированном с данным приложением и типом аутентификации.
Аргументы модуля – разделенные пробелами аргументы, которые могут использоваться для изменения поведения модуля.
Некоторые модули ограничиваются только поддержкой задачи auth, но существуют приложения, требующие выполнения задачи account при аутентификации. Тогда для выполнения второй задачи нужно использовать модуль pam_permit.so, разрешающий все задачи без проверки.
Пример PAM-приложения
Далее следует минимальная реализация программы su(1) с использованием PAM. В ней используется специфичная для OpenPAM функция взаимодействия openpam_ttyconv(3), объявление которой расположено в файле security/openpam.h.
#include <sys/param.h>
#include <sys/wait.h>
#include <err.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <security/pam_appl.h>
#include <security/openpam.h> /* for openpam_ttyconv() */
extern char **environ;
static pam_handle_t *pamh;
static struct pam_conv pamc;
static void
usage(void)
{
fprintf(stderr, "Usage: su [login [args]]\n");
exit(1);
}
int
main(int argc, char *argv[])
{
char hostname[MAXHOSTNAMELEN];
const char *user, *tty;
char **args, **pam_envlist, **pam_env;
struct passwd *pwd;
int o, pam_err, status;
pid_t pid;
while ((o = getopt(argc, argv, "h")) != -1)
switch (o) {
case 'h':
default:
usage();
}
argc -= optind;
argv += optind;
if (argc > 0) {
user = *argv;
--argc;
++argv;
} else {
user = "root";
}
/* initialize PAM */
pamc.conv = &openpam_ttyconv;
pam_start("su", user, &pamc, &pamh);
/* set some items */
gethostname(hostname, sizeof(hostname));
if ((pam_err = pam_set_item(pamh, PAM_RHOST, hostname)) != PAM_SUCCESS)
goto pamerr;
user = getlogin();
if ((pam_err = pam_set_item(pamh, PAM_RUSER, user)) != PAM_SUCCESS)
goto pamerr;
tty = ttyname(STDERR_FILENO);
if ((pam_err = pam_set_item(pamh, PAM_TTY, tty)) != PAM_SUCCESS)
goto pamerr;
/* authenticate the applicant */
if ((pam_err = pam_authenticate(pamh, 0)) != PAM_SUCCESS)
goto pamerr;
if ((pam_err = pam_acct_mgmt(pamh, 0)) == PAM_NEW_AUTHTOK_REQD)
pam_err = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
if (pam_err != PAM_SUCCESS)
goto pamerr;
/* establish the requested credentials */
if ((pam_err = pam_setcred(pamh, PAM_ESTABLISH_CRED)) != PAM_SUCCESS)
goto pamerr;
/* authentication succeeded; open a session */
if ((pam_err = pam_open_session(pamh, 0)) != PAM_SUCCESS)
goto pamerr;
/* get mapped user name; PAM may have changed it */
pam_err = pam_get_item(pamh, PAM_USER, (const void **)&user);
if (pam_err != PAM_SUCCESS || (pwd = getpwnam(user)) == NULL)
goto pamerr;
/* export PAM environment */
if ((pam_envlist = pam_getenvlist(pamh)) != NULL) {
for (pam_env = pam_envlist; *pam_env != NULL; ++pam_env) {
putenv(*pam_env);
free(*pam_env);
}
free(pam_envlist);
}
/* build argument list */
if ((args = calloc(argc + 2, sizeof *args)) == NULL) {
warn("calloc()");
goto err;
}
*args = pwd->pw_shell;
memcpy(args + 1, argv, argc * sizeof *args);
/* fork and exec */
switch ((pid = fork())) {
case -1:
warn("fork()");
goto err;
case 0:
/* child: give up privs and start a shell */
/* set uid and groups */
if (initgroups(pwd->pw_name, pwd->pw_gid) == -1) {
warn("initgroups()");
_exit(1);
}
if (setgid(pwd->pw_gid) == -1) {
warn("setgid()");
_exit(1);
}
if (setuid(pwd->pw_uid) == -1) {
warn("setuid()");
_exit(1);
}
execve(*args, args, environ);
warn("execve()");
_exit(1);
default:
/* parent: wait for child to exit */
waitpid(pid, &status, 0);
/* close the session and release PAM resources */
pam_err = pam_close_session(pamh, 0);
pam_end(pamh, pam_err);
exit(WEXITSTATUS(status));
}
pamerr:
fprintf(stderr, "Sorry\n");
err:
pam_end(pamh, pam_err);
exit(1);
}