воскресенье, 3 апреля 2011 г.

Проблемы сертификации: Эпизод I - Потеряный ключ

Вы когда-нибудь пытались разобраться, что от вас хочет CryptoAPI, когда сообщает невразумительное "Bad key"? Иногда бывает проще ответить на вопрос о смысле жизни, чем понять странные желания этой библиотеки. Не является исключением и .NET Framework, криптографическая часть которого более чем полностью, является всего лишь обёрткой над CryptoAPI. Поэтому, предоставляя пользователю весьма удобные интерфейсы, он наследует от своей старшей сестры ужасные сообщения об ошибках. Наиболее неприятным в моей практике был небольшой набор ошибок, связанных с криптованием и декриптованием данных, которые с завидной регулярностью возникали после начала эксплуатации программы клиентом или в лучшем случае вводили в ступор отдел контроля качества.


Keyset does not exist
Из текста сообщения можно предположить, что проблема заключается в отсутствии приватного ключа, который вы пытаетесь использовать для расшифровки данных (например, с помощью метода Decrypt класса RSACryptoServiceProvider из пространства имён System.Security.Cryptography). Но при этом MMC-оснастка Сертификаты (Certificates) утверждает обратное: все ключи - публичный и приватный - на месте. Дело в том, что CryptoAPI сообщает об отсутствии ключа в том числе и тогда, когда у запрашивающего его пользователя нет прав для доступа. Вспомните - аналогичным образом ADSI сообщает, что запрашиваемый объект не найден, если у вас нет прав на чтение каталога, в котором он располагается.

Приватный ключ является обычным файлом. Ключи, принадлежащие сертификатам из хранилища конкретного пользователя, располагаются в специальной папке внутри директории Application Data данного пользователя, а именно:
%AppData%\Microsoft\Crypto\RSA\<SID>
где <SID> - это SID данного пользователя в строковом формате (например, S-1-5-21-791563612-412897595-89479398972505).
Это правило справедливо и для ключей, относящихся к сертификатам из хранилища Local Computer, только искать их нужно внутри Application Data профайла All Users (или как её ещё называют Common Application Data), на моей Windows 7 это:
%ALLUSERSPROFILE%\Microsoft\Crypto\RSA\MachineKeys
обратите внимание, что вместо SID`а пользователя используется фиксированное название папки - MachineKeys.

Имя файла сертификата совпадает с уникальным именем криптографического контейнера, соответствующего данному ключу. Получить его можно, запросив параметр контейнера PP_UNIQUE_CONTAINER с помощью функции CryptoAPI CryptGetProvParam. Или через свойство UniqueKeyContainerName класса CspKeyContainerInfo в .NET:
private string GetPrivateKeyFileName(X509Certificate2 cert)
{
    RSACryptoServiceProvider rsa = cert.PrivateKey
        as RSACryptoServiceProvider;
    if (rsa != null)
    {
        return rsa.CspKeyContainerInfo.UniqueKeyContainerName;
    }


    throw new ApplicationException(
        "The certificate does not have an RSA private key.");
}


Единственный способ получить полный путь к файлу сертификата - это выполнить поиск файла с данным именем во всех возможных местах размещения файла ключа, а именно: в каталогах ключей системы и текущего пользователя:
private string FindPrivateKeyFile(string keyFileName)
{
    // try to search in the local machine store
    string commonAppData = Environment.GetFolderPath(
        Environment.SpecialFolder.CommonApplicationData);
    string localMachineStore = Path.Combine(
        commonAppData,"Microsoft\Crypto\RSA\MachineKeys");

    if (Directory.GetFiles(
        localMachineStore, keyFileName).Length > 0)
    {
        return Path.Combine(localMachineStore, keyFileName);
    }
    // try to search in the current user store
    string currUserAppData = Environment.GetFolderPath(
        Environment.SpecialFolder.ApplicationData);
    // you can detect the current user SID, or
    // you can enumerate all directories within the RSA
    // directory

    string currentUserStore = Path.Combine(
        currUserAppData, @"Microsoft\Crypto\RSA\");

    foreach (string directory in Directory.GetDirectories(
        currentUserStore))
    {
        if (Directory.GetFiles(
            directory, keyFileName).Length > 0)
        {
            return Path.Combine(directory, keyFileName);
        }
    }

    throw new ApplicationException(
        "The private key file does not exist");
}


Также весьма полезной для поиска сертификата может оказаться утилита Find Private Key Tool.

Правами доступа к ключу являются права доступа к данному файлу. Чтобы разрешить определённому пользователю использовать ключ, необходимо предоставить ему право Full Control.

Как правило, типичным примером воспроизведения данной ошибки является следующий сценарий. Вы пишете и отлаживаете свой сервис, прекрасно работающий под аккаунтом локальной системы. Затем незадолго до релиза, вы вспоминаете о правиле минимальных привилегий и меняете аккаунт сервиса с Local System на Network Service. И ваш сервис перестаёт работать! Вы просто не учли тот факт, что аккаунт Network Service по-умолчанию не имеет прав для доступа ни к одному приватному ключу в системе. Позаботиться о предоставлении таких прав вам придётся самостоятельно.

Второй типичный сценарий связан с последующей эксплуатацией приложения. Вы вроде всё сделали правильно: своевременно позаботились о правах для доступа пользователя к приватному ключу, но приложение всё равно падает с сообщением "Keyset does not exist". Причина может крыться в следующем: отсутствие прав доступа может означать в том числе и отсутствие прав доступа к контейнеру объекта (в данном случае к каталогу, где лежит ключ). Если сертификат заимпортировать, кликнув на pfx файл и пройдя через визард импорта, то он попадёт в хранилище текущего пользователя. Переместив затем его в хранилище Local Computer (или хранилище того пользователя, под которым будет запускаться сервис), используя MMC-оснастку Сертификаты (Certificates), вы переместите лишь сам сертификат с публичным ключом; приватный ключ так и останется лежать внутри вашего хранилища, куда изначально он был заимпортирован. Как не сложно догадаться, даже если вы предоставите пользователю, под которым запускается сервис, все права на этот файл, доступа к нему у него не будет всё равно, поскольку у него нет доступа к каталогам, где он располагается. Поэтому вам необходимо либо позаботиться о своей собственной процедуре импорта, либо научить отдел поддержки клиентов правильному способу импорта сертификатов: выбирать конкретное хранилище через оснастку MMC-оснастку Сертификаты (Certificates) и импортировать сертификат непосредственно в него.

Комментариев нет:

Отправить комментарий