Введение

В этой статье мы расскажем, как использовать сторонние библиотеки PAM непосредственно из кода для делегирования задач достаточно гибким способом. В прошлом у нас уже была статья про разработку и применение простого PAM-модуля для работы со смарт-картами. Рекомендуем ознакомиться для полного понимания.

Как мы использовали kinit и почему отказались

В одном из новых проектов нам потребовалось реализовать средства двухфакторной аутентификации в домене при помощи записанного на смарт-карту сертификата. Мы написали PAM-модуль, выполняющий аутентификацию через функцию kinit, где в качестве параметра X509_user_identity указали "PKCS11".

К сожалению, решение не прижилось, так как в новой версии продукта потребовалось кешировать тикеты Kerberos, а также проверять сертификаты на отзыв с помощью OCSP. Данная функциональность уже реализована в SSSD, поэтому нам стало интересно, сможем ли мы полностью положиться на проверенные временем стандартные средства.

Функции pam_get_item/pam_set_item

Внутри одного стека PAM-модули взаимодействуют друг с другом через передачу ограниченного числа данных:

int pam_get_item(const pam_handle_t *pamh, int item_type,
                 const void **item);

int pam_set_item(pam_handle_t *pamh, int item_type,
                 const void *item);

Мы используем их для определения имени сервиса (item_type == PAM_SERVICE)
и записи аутентификационных данных (item_type == PAM_AUTHTOK), однако для более комплексного взаимодействия они не подходят в связи со следующими ограничениями:

  1. Мы не можем узнать, успешно ли переданы данные.

  2. Мы не можем узнать статус операции в чужом PAM-модуле.

  3. Старые версии PAM-модулей могут игнорировать наши данные.

Выходом из такого положения стал запуск нужных нам PAM-модулей в отдельном стеке. Таким образом наш PAM-модуль начал работать как PAM-приложение.

Реализация PAM приложения

Для вызова PAM-цепочки наше приложение использует функцию pam_start. Ниже приведено ее объявление на языке C:

void pam_start(const char *service_name, const char *user,
	           const struct pam_conv *pam_conversation,
               pam_handle_t **pamh);

service_name – это, по сути, имя файла, который мы, например, создали в папке
/etc/pam.d. Параметр содержит цепочку PAM. Стандартными сервисами являются
common-auth, common-password и другие. В частности, у sudo есть свой сервис. Их также можно включить в другую PAM-цепочку через директиву @include.

user – это имя аутентифицируемого пользователя. Если передать nullptr, PAM запросит его, когда будет нужно.

Рассмотрим параметр pam_conversation подробнее. Ниже приведено объявление этой структуры на языке C:

struct pam_conv {
    int (*conv)(int num_msg, const struct pam_message **msg,
                struct pam_response **resp, void *appdata_ptr);
    void *appdata_ptr;
};

Данная структура содержит ссылку на функцию для коммуникации посредством текстовых сообщений conv и generic данные приложения appdata_ptr.

Рассмотрим функцию conv. Первый параметр num_msg – это число сообщений от PAM-модуля. Второй параметр, соответственно, содержит эти сообщения.

Каждое pam_message представляет из себя пару msg_style и msg. Поле msg_style хранит информацию о характере сообщения и может принимать одно из следующих значений: PAM_TEXT_INFO, PAM_ERROR_MSG, PAM_PROMPT_ECHO_ON, PAM_PROMPT_ECHO_OFF. Поле msg представляет собой текст сообщения.

Выходной параметр pam_response должен содержать ответы на каждое сообщение в порядке следования этих сообщений в параметре pam_message. Каждый pam_response представляет собой пару из текста resp и кода ошибки resp_retcode.

Важно, что ответственность за выделение памяти под ответы лежит на стороне PAM-приложения. Используемый PAM-модуль должен освободить эту память.

Наконец, возвращаемое значение функции conv – это обычный код ошибки PAM, например, PAM_SUCCESS.

Наша реализация на языке C++

Для удобства мы реализовали следующую диаграмму классов:

PamConv – содержит набор callback функций, реагирующих на сообщения и запросы PAM-модуля.

PamApplication – запускает цепочку через pam_start. Вызывает функции pam_authenticate, pam_open_session и т.д. Обрабатывает полученные ошибки. В деструкторе вызывает pam_end.

PamService – выполняет код юзкейса, например, чтение данных с токена + доменная аутентификация. В нашем проекте потребовались две реализации: SssdPamService (работа с pam_sss.so) и UnixPamService (работа с pam_unix.so).

Таким образом наши пользователи смогут спать спокойно, потому что ничего лучше линукса еще не изобрели, потому что мы доверяем процессы аутентификации стандартным средствам Linux.

Примеры использования

Итак, благодаря описанной выше схеме, мы можем:

  1. Подменять промты. Например, перевести строку по-своему, скрыть определенное сообщение.

  2. Передать модифицированный ввод пользователя.

  3. Вообще перестать запрашивать ввод от пользователя. Например, программно выбирать сертификат, который запрашивает SSSD.

Перед началом работы с PAM-модулем, необходимо узнать, какие строки он возвращает. Надежнее всего искать текст промптов в исходном коде, если он находится в свободном доступе.

Важно учесть, что PAM-модули могут присылать локализованные строки. В таких случаях рациональнее всего выставлять английскую локаль перед вызовом функций PAM.

Пример кода

auto showMsg = [this](const char* msg) {
    // Скрываем сообщение
    if (boost::regex_match(msg, boost::regex(R"(^Certificate.*selected$)"))) {
        return;
    }
    pam_prompt(getPamHandle(), PAM_TEXT_INFO, nullptr, msg);
};

auto showErr = [this](const char* msg) {
    pam_prompt(getPamHandle(), PAM_ERROR_MSG, nullptr, msg);
};

auto getInfo = [this](const char* msg_) {
    // Эмулируем выбор сертификата
    std::string msg{ msg_ };
    boost::smatch match;

    std::string certLabel = ...;
    auto certRegex = boost::regex(R"(^[(\d+)]:\r?\n([^\r\n]+)\r?\n)");
    while (boost::regex_search(msg, match, certRegex)) {
        if (match[2] == certLabel) {
            return match[1];
        }
        msg = match.suffix().str();
    }
    throw PamServiceException("Cert not found");
};

auto getPasswd = [](const char*) -> Utils::SecString {
    assert(false && "getPasswd not expected");
    return {};
};

PamConv pamConv{ .showMsg = showMsg,
                 .showErr = showErr,
                 .getInfo = getInfo,
                 .getPasswd = getPasswd };

const char* serviceName = "rtlogon-sss-cert";
m_pamApplication = PamApplication(
    serviceName, getUser(), std::move(pamConv));

// Пользовательский код
std::string localeBackup = std::setlocale(LC_ALL, nullptr);
std::setlocale(LC_ALL, "en_US.UTF-8");

m_pamApplication.authenticate();

std::setlocale(LC_ALL, localeBackup.c_str());

Файл /etc/pam.d/rtlogon-sss-cert

auth        required    pam_sss.so try_cert_auth
account     required    pam_sss.so
password    required    pam_sss.so
session     required    pam_sss.so

Конфигурация промтов SSSD

Недавно мы узнали, что промты SSSD можно конфигурировать под конкретные сервисы. Теперь мы используем этот механизм для полной уверенности, что мы сможем отличить запрос SSSD от запросов других сервисов.

На каждый метод аутентификации, а также сервис, можно задать свой текст в соответствующей секции конфигурационного файла SSSD. Приведём пример такой конфигурации ниже:

; Текст запроса пароля для всех сервисов, для которых нет отдельной конфигурации
; Обратите внимание, что SSSD считывает и печатает текст запроса вместе с кавычками, а также не учитывает пробел в конце строки
[prompting/password]
password_prompt=Global password prompt:

; Текст запроса пароля только для сервиса service_name
[prompting/password/service_name]
password_prompt=Password prompt for service_name:

[prompting/2fa]
; Текст запроса первого фактора
first_prompt=First factor prompt:

; Текст запроса первого фактора
second_prompt=Second factor prompt:

; Принимает значения True/False. Если выставлен True, на два фактора будет только один запрос с текстом first_prompt
single_prompt=True

Чтобы заменить промт пароля для сервиса rtlogon-sss-passwd, мы создали конфигурационный файл в директории /etc/sssd/conf.d/, откуда SSSD считывает дополнительные конфигурации. Чтобы изменения вступили в силу, необходимо перезапустить службу sssd.

Файл /etc/sssd/conf.d/50-rtlogon.conf

[prompting/password/rtlogon-sss-passwd]
password_prompt = Password:

Теперь текст запроса пароля для нашего сервиса зафиксирован, а значит глобальная конфигурация или разница в версиях SSSD (то есть в теории разные версии текста) не повлияют на корректную работу аутентификации через наш модуль.

Заключение

Подводя итоги, система PAM позволяет во многом увеличить надежность пользовательских приложений за счет делегирования вопросов безопасности стандартным и проверенным временем средствам.

В этой статье мы постарались подробно описать несколько из способов реализации взаимодействия между PAM-модулями, которые успешно применили на практике.

Оказалась ли эта статья полезной для Вас? Пишите в комментариях! Будем рады ответить на любые вопросы.

Комментарии (0)