Pull to refresh
74.7

Разработка и применение модуля PAM для аутентификации в Astra Linux с использованием Рутокен ЭЦП и Рутокен S

Reading time 13 min
Views 48K


В этой статье мне бы хотелось рассказать о том, как приложения в 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:
  1. Linux-PAM – основная реализация архитектуры PAM, рассматривается нами в этой статье
  2. OpenPAM – альтернативная реализация PAM, используемая в BSD-системах и Mac OS X
  3. Java PAM – Java-обертка над Linux-PAM

Структура PAM


Для начала разберемся, что же такое «Модуль PAM». Модули представляют собой библиотеки, в которых прописаны обработчики операций, которые может направлять к ним сам PAM. Для примера, стандартный модуль pam_unix умеет следующее:
  • Запросить пароль у пользователя и проверить введенное значение с хранимым в системе
  • Проверить, удовлетворяет ли пароль требованиям безопасности и не истек ли он

Ниже представлена общая схема работы PAM

Сильно упрощенная схема аутентификации в приложении, использующем PAM, выглядит следующим образом:
  1. Приложение инициализирует библиотеку PAM (libpam.so)
  2. PAM в соответствии с конфигурационным файлом для приложения обращается к требуемым модулям
  3. Модули выполняют возложенные на них действия
  4. Приложению возвращается результат операции

Конечно, 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-токенам с помощью ассиметричной криптографии. Рассмотрим общую схему его работы:
  • На токене хранится сертификат пользователя и его закрытый ключ
  • Сертификат также сохранен в домашнем каталоге пользователя как доверенный

Аутентификация происходит следующим образом:
  1. На токене выполняется поиск сертификата пользователя
  2. Через PAM производится запрос PIN-кода к токену
  3. Если аутентификация на токене прошла успешно, то производится подпись случайных данных с помощью закрытого ключа с токена. Сама подпись выполняется аппаратно.
  4. Полученная ЭЦП проверяется с помощью сертификата пользователя

Если в итоге проверка подписи осуществлена успешно, то модуль говорит наружу, что все хорошо.
В данной схеме используются ключевая пара 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 bytes\n", 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 failed\n");
	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 failed\n");
	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.
Tags:
Hubs:
+19
Comments 7
Comments Comments 7

Articles

Information

Website
aktiv-company.ru
Registered
Founded
1994
Employees
101–200 employees
Location
Россия