В этой статье мне бы хотелось рассказать о том, как приложения в Linux могут использовать систему Подключаемых Модулей Безопасности (Pluggable Authentication Modules) для прозрачной аутентификации пользователей. Мы немного покопаемся в истории развития механизмов аутентификации в Linux, разберемся с системой настроек PAM и разберем исходный код модуля аутентификации pam_p11, который позволяет проводить аутентификацию пользователей по смарт-картам.
В конце статьи мы рассмотрим на практике настройку и работу модуля аутентификации в сертифицированном по 3 классу защищенности СВТ и 2 уровню контроля отсутствия недекларированных возможностей дистрибутиве Astra Linux для аутентификации по USB-токенам Рутокен ЭЦП и Рутокен S. Учитывая то, что Рутокен S имеет сертификаты ФСТЭК по НДВ 3, а Рутокен ЭЦП по НДВ 4, это решение может применяться в информационных системах, обрабатывающих конфиденциальную информацию, вплоть до информации с грифом «С».
Немного истории
В старые добрые времена если приложению в Linux требовалось запросить аутентификацию пользователя, то ему приходилось обращаться к файлам /etc/passwd и /etc/shadow. Такой подход был прост как пробка, но при этом разработчикам приходилось думать не только о работе с файлами, но и о вопросах безопасности. В связи с этим возникла необходимость разработки прозрачного механизма аутентификации пользователей, не зависящего от способа хранения информации об их учетных записях.
Решением тому стал проект Linux-PAM. К слову сказать, сама архитектура PAM была впервые предложена компанией Sun в октябре 1995 года, а в августе 1996 года инфраструктура Linux-PAM была включена в дистрибутив Red Hat Linux. В настоящее время существуют три основных реализации PAM:
- Linux-PAM – основная реализация архитектуры PAM, рассматривается нами в этой статье
- OpenPAM – альтернативная реализация PAM, используемая в BSD-системах и Mac OS X
- Java PAM – Java-обертка над Linux-PAM
Структура PAM
Для начала разберемся, что же такое «Модуль PAM». Модули представляют собой библиотеки, в которых прописаны обработчики операций, которые может направлять к ним сам PAM. Для примера, стандартный модуль pam_unix умеет следующее:
- Запросить пароль у пользователя и проверить введенное значение с хранимым в системе
- Проверить, удовлетворяет ли пароль требованиям безопасности и не истек ли он
Ниже представлена общая схема работы PAM
Сильно упрощенная схема аутентификации в приложении, использующем PAM, выглядит следующим образом:
- Приложение инициализирует библиотеку PAM (libpam.so)
- PAM в соответствии с конфигурационным файлом для приложения обращается к требуемым модулям
- Модули выполняют возложенные на них действия
- Приложению возвращается результат операции
Конечно, PAM позволяет проводить не только аутентификацию. Функции PAM классифицируются по типу модулей. В скобках указаны обозначения модулей в конфигурационных файлах:
- Аутентификация (auth)
- Управление учетными записями (account)
- Управление сеансами (session)
- Управление паролями (passwd)
Сейчас нам интересна только аутентификация, поэтому рассмотрение остальных функций оставим любопытству читателя.
Конфигурация PAM
Если приложению требуется аутентификация, то оно должно создать файл со своим именем в каталоге /etc/pam.d, в котором должны быть указаны модули, с помощью которых производится аутентификация и прочие действия. Посмотрим, что лежит в каталоге /etc/pam.d в Ubuntu 11.10
$ ls /etc/pam.d/
atd common-account common-session-noninteractive lightdm other samba vmtoolsd chfn common-auth cron lightdm-autologin passwd sshd chpasswd common-password cups login polkit-1 su chsh common-session gnome-screensaver newusers ppp sudo
Для примера, посмотрим на абстрактный файл конфигурации для приложения login
# PAM configuration for login
auth requisite pam_securetty.so
auth required pam_nologin.so
auth required pam_env.so
auth required pam_unix.so nullok
account required pam_unix.so
session required pam_unix.so
session optional pam_lastlog.so
password required pam_unix.so nullok obscure min=4 max=8
Каждая строчка конфига записывается в виде
<тип модуля> <управляющий флаг> <путь к библиотеке> <параметры>
- Тип модуля соответствует обозначениям самих модулей (т.е. auth/account/session/passwd)
- Управляющий флаг указывает критичность модуля для успешного выполнения операции. Флаг может принимать следующие значения: requisite (необходимый), required (требуемый), sufficient (достаточный) и optional (необязательный).
- Путь к библиотеке задает собственно путь до файла модуля. По умолчанию они ищутся в /lib/security/
- Параметры задают список аргументов, которые будут переданы модулю. Аргументы передаются аналогично принципу argc/argv в функции main(), за исключением того, что argv[0] содержит не имя модуля, а конкретный аргумент.
Таким образом, мы получаем стек модулей, каждый из которых выполняет свое действие. PAM при этом разбирает стек как и положено – сверху вниз. В соответствии с управляющим флагом задаются следующие требования к успешности операции:
- requisite (необходимый): если модуль стека вернет отрицательный ответ, то запрос сразу же отвергается. Другие модули при этом не будут выполнены.
- required (требуемый): если один или несколько модулей стека вернут отрицательный ответ, все остальные модули будут выполнены, но запрос приложения будет отвергнут.
- sufficient (достаточный): если модуль помечен как достаточный и перед ним ни один из необходимых или достаточных модулей не возвратил отрицательного ответа, то все оставшиеся модули в стеке игнорируются, и возвращается положительный ответ.
- optional (дополнительный): если в стеке нет требуемых модулей, и если ни один из достаточных модулей не возвратил положительного ответа, то хотя бы один из дополнительных модулей приложения или службы должен вернуть положительный ответ
Конфигурационные файлы модулей хранятся в /usr/share/pam-configs/<имя модуля>. В каждом файле указывается полное имя модуля, включен ли он по умолчанию, приоритет модуля и параметры аутентификации.
Разработка модуля аутентификации для PAM
В этом разделе мы разберем исходный код модуля pam_p11 и рассмотрим основные моменты, которым стоит уделить внимание при написании своего собственного модуля.
pam_p11
Данный модуль позволяет осуществить двухфакторную аутентификацию пользователей по смарт-картам или USB-токенам с помощью ассиметричной криптографии. Рассмотрим общую схему его работы:
- На токене хранится сертификат пользователя и его закрытый ключ
- Сертификат также сохранен в домашнем каталоге пользователя как доверенный
Аутентификация происходит следующим образом:
- На токене выполняется поиск сертификата пользователя
- Через PAM производится запрос PIN-кода к токену
- Если аутентификация на токене прошла успешно, то производится подпись случайных данных с помощью закрытого ключа с токена. Сама подпись выполняется аппаратно.
- Полученная ЭЦП проверяется с помощью сертификата пользователя
Если в итоге проверка подписи осуществлена успешно, то модуль говорит наружу, что все хорошо.
В данной схеме используются ключевая пара RSA длины 2048 бит, сгенерированная аппаратно на токене.
Собственно разработка модуля
В зависимости от функционала модуля, PAM может требовать от него наличия следующих функций:
- pam_sm_authenticate, pam_sm_setcred – аутентификация
- pam_sm_acct_mgmt – управление учетными записями
- pam_sm_chauthtok – управление паролями
- pam_sm_open_session, pam_sm_close_session — управление сеансами
Для того чтобы модуль мог выполнять аутентификацию, нам необходимо реализовать в нем функции pam_sm_authenticate и pam_sm_setcred. В остальных функциях достаточно просто добавить заглушки, чтобы наш модуль нельзя было использовать для других операций.
Для работы с PAM необходимо определить специальные константы, а только затем подключить заголовочные файлы:
#define PAM_SM_AUTH
#define PAM_SM_ACCOUNT
#define PAM_SM_SESSION
#define PAM_SM_PASSWORD
#include <security/pam_appl.h>
#include <security/pam_modules.h>
Эти константы необходимы, чтобы PAM знал, что наш модуль может выполнять все функции, описанные выше. Конечно, если мы реализуем только аутентификацию, то остальные функции можно отбросить, но разработчики pam_p11 решили, что надежнее будет поставить заглушки вместо неиспользуемых функций.
Приступим к написанию функции pam_sm_authenticate. Она имеет следующую сигнатуру:
PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv);
Из важных параметров тут стоит отметить:
- pamh – хендл к PAM, полученный приложением
- argc, argv – аргументы, указанные в конфигурационном файле. Наш модуль принимает один аргумент – путь к библиотеке PKCS#11
Функция должна вернуть одно из следующих значений:
- PAM_AUTH_ERR – ошибка аутентификации
- PAM_CRED_INSUFFICIENT – у приложения недостаточно прав для выполнения аутентификации
- PAM_AUTHINFO_UNAVAIL – модулю не удалось получить информацию для выполнения аутентификации. Это может случиться из-за проблем в сети или другого отказа оборудования
- PAM_SUCCESS – аутентификация прошла успешно
- PAM_USER_UNKNOWN – пользователь с переданным именем не существует
- PAM_MAXTRIES – один или несколько модулей аутентификации превысили допустимый предел попыток.
Внутри нашего модуля мы будем пользоваться библиотекой libp11 для работы с API PKCS#11 и OpenSSL для работы с сертификатами.
Первым делом определим переменные, которые нам потребуются:
int i, rv;
const char *user; // имя пользователя
char *password; // вводимый пользователем пароль
char password_prompt[64]; // запрос на ввод пароля, показываемый приложением
// структуры PAM
struct pam_conv *conv; // функция диалога PAM
struct pam_message msg; // сообщения диалога PAM
struct pam_message *(msgp[1]);
struct pam_response *resp; // ответ PAM
// структуры lib_p11:
PKCS11_CTX *ctx; // контекст PKCS#11
PKCS11_SLOT *slot, *slots; // слоты
PKCS11_CERT *certs; // сертификаты
unsigned int nslots, ncerts;
PKCS11_KEY *authkey; // закрытый ключ
PKCS11_CERT *authcert; // сертификат
EVP_PKEY *pubkey; // открытый ключ OpenSSL для проверки подписи
unsigned char rand_bytes[RANDOM_SIZE];
unsigned char signature[MAX_SIGSIZE];
int fd;
unsigned siglen;
Затем проверим, передали ли нам путь к библиотеке PKCS#11
if (argc != 1) {
pam_syslog(pamh, LOG_ERR, "need pkcs11 module as argument");
return PAM_ABORT;
}
После чего инициализируем OpenSSL и контекст PKCS#11
OpenSSL_add_all_algorithms();
ERR_load_crypto_strings();
ctx = PKCS11_CTX_new();
Запросим у PAM имя пользователя
rv = pam_get_user(pamh, &user, NULL);
if (rv != PAM_SUCCESS) {
pam_syslog(pamh, LOG_ERR, "pam_get_user() failed %s", pam_strerror(pamh, rv));
return PAM_USER_UNKNOWN;
}
Теперь загрузим библиотеку PKCS#11, найдем первый доступный токен и получим с него сертификаты
rv = PKCS11_CTX_load(ctx, argv[0]);
if (rv) {
pam_syslog(pamh, LOG_ERR, "loading pkcs11 engine failed");
return PAM_AUTHINFO_UNAVAIL;
}
// получим все доступные слоты PKCS#11
rv = PKCS11_enumerate_slots(ctx, &slots, &nslots);
if (rv) {
pam_syslog(pamh, LOG_ERR, "listing slots failed");
return PAM_AUTHINFO_UNAVAIL;
}
// найдем первый слот с токеном
slot = PKCS11_find_token(ctx, slots, nslots);
if (!slot || !slot->token) {
pam_syslog(pamh, LOG_ERR, "no token available");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
// получим сертификаты с токена
rv = PKCS11_enumerate_certs(slot->token, &certs, &ncerts);
if (rv) {
pam_syslog(pamh, LOG_ERR, "PKCS11_enumerate_certs failed");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
if (ncerts <= 0) {
pam_syslog(pamh, LOG_ERR, "no certificates found");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
Теперь среди сертификатов на токене найдем тот, что лежит у нас в ~/.eid/authorized_certificates:
for (i = 0; i < ncerts; i++) {
authcert = &certs[i];
if (authcert != NULL) {
/* проверим, совпадает ли сертификат с введенным именем пользователя */
rv = match_user(authcert->x509, user);
if (rv < 0) {
pam_syslog(pamh, LOG_ERR, "match_user() failed");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
} else if (rv == 0) {
/* this is not the cert we are looking for */
authcert = NULL;
} else {
break;
}
}
}
if (!authcert) {
pam_syslog(pamh, LOG_ERR, "no matching certificates found");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
А сейчас самое интересное – нам нужно запросить через PAM пароль пользователя (который в нашем случае будет PIN-кодом к токену), а затем выполнить аутентификацию на токене
// для начала проверим, не сохранил ли PAM пароль при выполнении других модулей
rv = pam_get_item(pamh, PAM_AUTHTOK, (void *)&password);
if (rv == PAM_SUCCESS && password) {
password = strdup(password);
} else {
// если пароль не сохранен, то спросим его у пользователя
sprintf(password_prompt, "Password for token %.32s: ", slot->token->label);
// задаем параметры диалога PAM: запрос пароля без рисования "звездочек"
msg.msg_style = PAM_PROMPT_ECHO_OFF;
msg.msg = password_prompt;
// получаем указатель на структуру диалога PAM
rv = pam_get_item(pamh, PAM_CONV, (const void **)&conv);
if (rv != PAM_SUCCESS) {
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
if ((conv == NULL) || (conv->conv == NULL)) {
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
// вызываем функцию диалога, введенный пароль будет записан в resp
rv = conv->conv(1, (const struct pam_message **)msgp, &resp, conv->appdata_ptr);
if (rv != PAM_SUCCESS) {
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
if ((resp == NULL) || (resp[0].resp == NULL)) {
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
// запоминаем пароль и очищаем память ответа
password = strdup(resp[0].resp);
memset(resp[0].resp, 0, strlen(resp[0].resp));
free(&resp[0]);
}
Теперь мы можем выполнить аутентификацию на токене:
rv = PKCS11_login(slot, 0, password);
// не забываем очистить память, выделенную под пароль
memset(password, 0, strlen(password));
free(password);
if (rv != 0) {
pam_syslog(pamh, LOG_ERR, "PKCS11_login failed");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
На этом завершается первый этап аутентификации. Теперь нам нужно проверить, обладает ли владелец токена закрытым ключом. Для этого вычислим ЭЦП для произвольного блока данных и проверим ее с помощью доверенного сертификата.
Для начала считаем 128 байт из /dev/random
fd = open(RANDOM_SOURCE, O_RDONLY);
if (fd < 0) {
pam_syslog(pamh, LOG_ERR, "fatal: cannot open RANDOM_SOURCE: ");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
rv = read(fd, rand_bytes, RANDOM_SIZE);
if (rv < 0) {
pam_syslog(pamh, LOG_ERR, "fatal: read from random source failed: ");
close(fd);
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
if (rv < RANDOM_SIZE) {
pam_syslog(pamh, LOG_ERR, "fatal: read returned less than %d<%d bytesn", rv, RANDOM_SIZE);
close(fd);
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
close(fd);
Затем получим закрытый ключ, соответствующий сертификату и подпишем на нем случайные данные
// поиск закрытого ключа по сертификату
authkey = PKCS11_find_key(authcert);
if (!authkey) {
pam_syslog(pamh, LOG_ERR, "no key matching certificate available");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
// аппаратное вычисление ЭЦП
siglen = MAX_SIGSIZE;
rv = PKCS11_sign(NID_sha1, rand_bytes, RANDOM_SIZE, signature, &siglen, authkey);
if (rv != 1) {
pam_syslog(pamh, LOG_ERR, "fatal: pkcs11_sign failedn");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
Проверим подпись. Для этого сначала средствами OpenSSL получим открытый ключ из сертификата, а затем выполним проверку ЭЦП
pubkey = X509_get_pubkey(authcert->x509);
if (pubkey == NULL) {
pam_syslog(pamh, LOG_ERR, "could not extract public key");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
// программно проверяем ЭЦП с помощью OpenSSL
rv = RSA_verify(NID_sha1, rand_bytes, RANDOM_SIZE, signature, siglen, pubkey->pkey.rsa);
if (rv != 1) {
pam_syslog(pamh, LOG_ERR, "fatal: RSA_verify failedn");
rv = PAM_AUTHINFO_UNAVAIL;
goto out;
}
Если проверка подписи прошла успешно, то мы можем завершить работу с библиотекой PKCS#11 и вернуть PAM_SUCCESS.
rv = PAM_SUCCESS;
out:
PKCS11_release_all_slots(ctx, slots, nslots);
PKCS11_CTX_unload(ctx);
PKCS11_CTX_free(ctx);
return rv;
Вместо остальных функций оставим никому не интересные заглушки, соберем модуль и приступим к его настройке и использованию.
Практическое использование
В качестве подопытного дистрибутива можно было бы взять свежую Ubuntu, но учитывая то, что в 12.04 все слишком хорошо работает, мы решили с пользой для общего дела настроить аутентификацию в релизе «Смоленск» операционной системы Astra Linux Special Edition по USB-токенам Рутокен ЭЦП и Рутокен S.
Установка дополнительных пакетов
Для начала пришлось установить некоторые пакеты. Для работы Рутокен S необходима старая версия OpenSC: 0.11.13, а для работы Рутокен ЭЦП – более новая: 0.12.2. В качестве middleware для обоих токенов используется OpenCT версии 0.6.20.
В итоге были поставлены пакеты, переданные разработчиками дистрибутива:
- libopenct1 (0.6.20-1.2): libopenct1_0.6.20-1.2_amd64.deb
- openct (0.6.20-1.2): openct_0.6.20-1.2_amd64.deb
Для Рутокен S
- libopensc2_0.11.13-1.1_amd64.deb
- opensc_0.11.13-1.1_amd64.deb
- mozilla-opensc_0.11.13-1.1_amd64.deb
Для Рутокен ЭЦП
- opensc (0.12.2-2): opensc_0.12.2-2_amd64.deb
При установке новой версии opensc потребовалось удовлетворить зависимости пакетов. Для этого были взяты следующие пакеты из репозитория Debian squeeze:
- libltdl7 (>= 2.2.6b): libltdl7_2.2.6b-2_amd64.deb
- libssl0.9.8 (>= 0.9.8m-1): libssl0.9.8_0.9.8o-4squeeze11_amd64.deb
Модуль PAM и его зависимости
Для осуществления аутентификации по токену были установлены пакеты:
- libp11-1 (0.2.7-2): libp11-1_0.2.7-2_amd64.deb
- libpam-p11 (0.1.5-1): libpam-p11_0.1.5-1+b1_amd64.deb
- libengine-pkcs11-openssl (0.1.8-2): libengine-pkcs11-openssl_0.1.8-2_amd64.deb
Настройка pam_p11
К счастью, нам не почти не придется править конфиги руками. Достаточно только создать файл /usr/share/pam-configs/p11 со следующим содержанием:
Name: Pam_p11
Default: yes
Priority: 800
Auth-Type: Primary
Auth: sufficient pam_p11_opensc.so /usr/lib/opensc-pkcs11.so
Интерес предоставляет последняя строчка конфига, в которой мы указываем тип модуля, имя библиотеки и параметры, передаваемые модулю. Наш модуль принимает в качестве параметра путь к библиотеке PKCS#11.
Теперь нам осталось только выполнить команду
$ pam-auth-update
В появившемся диалоге необходимо выбрать pam_p11. Если вы хотите отключить аутентификацию по паролям, то можно отключить Unix authentication. Поскольку в конфигурационном файле профиля было указано, что модуль будет «sufficient», то при получении от нашего модуля ответа «PAM_SUCCESS» весь процесс аутентификации будет считаться успешным.
Создание ключа и сертификата
Для начала создаем ключевую пару RSA длины 2048 бит c ID «45» (id стоит запомнить, он понадобится при создании сертификата).
$ pkcs15-init --generate-key rsa/2048 --auth-id 02 --id 45
<вводим PIN пользователя>
Проверим сгенерированный ключ:
$ pkcs15-tool --list-keys
Using reader with a card: Aktiv Rutoken ECP 00 00
Private RSA Key [Private Key]
Object Flags : [0x3], private, modifiable
Usage : [0x4], sign Access Flags : [0x1D], sensitive, alwaysSensitive, neverExtract, local
ModLength : 2048
Key ref : 1 (0x1)
Native : yes
Path : 3f001000100060020001
Auth ID : 02
ID : 45
Теперь с помощью OpenSSL создадим самоподписанный сертификат. Запускаем openssl и подгружаем модуль поддержки pkcs11:
$ openssl
OpenSSL> engine dynamic -pre SO_PATH:/usr/lib/engines/engine_pkcs11.so -pre ID:pkcs11 -pre LIST_ADD:1 -pre LOAD -pre MODULE_PATH:opensc-pkcs11.so
(dynamic) Dynamic engine loading support
[Success]: SO_PATH:/usr/lib/engines/engine_pkcs11.so
[Success]: ID:pkcs11
[Success]: LIST_ADD:1
[Success]: LOAD
Loaded: (pkcs11) pkcs11 engine
Создаем сертификат в PEM-формате:
OpenSSL> req -engine pkcs11 -new -key 1:45 -keyform engine -x509 -out cert.pem –text
В последней команде 1:45 — это пара :<id ключа>. Таким образом, мы создали сертификат на базе ключевой пары, хранящейся на токене. При этом в текущем каталоге должен создаться файл сертификата с именем cert.pem.
Теперь сохраним сертификат на токен:
$ pkcs15-init --store-certificate cert.pem --auth-id 02 --id 45 --format pem
<Вводим PIN пользователя>
Занесение сертификата в список доверенных
На данном этапе нам осталось только прочитать с токена сертификат с нужным ID и записать его в файл доверенных сертификатов:
mkdir ~/.eid
chmod 0755 ~/.eid
pkcs15-tool -r <certificate_id> > ~/.eid/authorized_certificates
chmod 0644 ~/.eid/authorized_certificates
Заключение
В статье я постарался рассмотреть механизм работы PAM, особо не углубляясь в специфику работы его внутренних функций. В связи с этим остались без особого внимания такие вещи, как механизм диалогов PAM, функции для работы со структурами PAM и некоторые тонкости настройки всей системы. Сами по себе они претендуют на отдельную статью, поэтому, если будет интерес, то могу их описать в новой статье.
Описанные шаги по настройке системы аутентификации можно использовать как инструкцию в любом современном дистрибутиве Linux.
Автор: lunatik42