Найти тему
Программный техник

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

В этой статье будут рассматривать, как приложения в 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
  1. Java PAM – Java-обертка над Linux-PAM

Структура PAM

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

  • Запросить пароль у пользователя и проверить введенное значение с хранимым в системе
  • Проверить, удовлетворяет ли пароль требованиям безопасности и не истек ли он

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

Общая схема работы PAM
Общая схема работы 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 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.