CryptoPro / corefx

This repo contains the .NET Core foundational libraries, called CoreFX. It includes classes for collections, file systems, console, XML, async and many others. We welcome contributions.
https://github.com/dotnet/core
MIT License
27 stars 7 forks source link

Поддержка CRYPT_SILENT в ссылке на ключ #33

Closed ekx77 closed 2 years ago

ekx77 commented 3 years ago

При чтении/установке X509Certificate2::PrivateKey теряется флаг CRYPT_SILENT, который может присутствовать в CRYPT_KEY_PROV_INFO::dwFlags. Примерный сценарий для чтения:

// задача - сериализовать ссылку на ключ
X509Certificate2 cert = ... // поиск сертификата со ссылкой в хранилище, например установленым туда средствами УЦ 2.0
var key = cert.PrivateKey as Gost3410_2012_256CryptoServiceProvider;
var containerInfo = key.CspKeyContainerInfo;
// далее из containerInfo можно прочитать ProviderType, ProvideName и т.д., но из флагов будет доступен MachineKeyStore,
// а про Silent ничего узнать не получится - 
// хорошо бы иметь соответствующее свойство или вообще уметь получать CspParameters из классов *CryptoServiceProvider.

Примерный сценарий для записи:

var cert = new X509Certificate2("test.cer");
var p = new CspParameters(provType, provName, containerName)
{
    KeyNumber = 1,
    Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.NoPrompt,
};

var key = new Gost3410_2012_256CryptoServiceProvider(p);
cert.PrivateKey = key; // вот здесь в ссылку на ключ попадет только CRYPT_MACHINE_KEYSET, CRYPT_SILENT не попадет
Fasjeit commented 3 years ago

Сейчас при установке ключа пользуемся самописным методом SetCspPrivateKey https://github.com/CryptoPro/corefx/blob/c704e18e502d41e9ffb624e4aaf362180f7f7cd8/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.PrivateKey.Win.cs#L73-L91

Сделан он по аналогии с методом копирования ключа от ms CopyWithPersistedCapiKey https://github.com/CryptoPro/corefx/blob/c704e18e502d41e9ffb624e4aaf362180f7f7cd8/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.PrivateKey.Win.cs#L182-L196

Который является переносом аналогичного метода из .NET Framework https://referencesource.microsoft.com/#System/security/system/security/cryptography/x509/x509certificate2.cs,c99bf10e3e4c5c5e

Почему они переносят только флаг расположения ключа - хороший вопрос.

Более того, при получении ключа из сертификата тоже будет вариться объект исключительно смотря на флаг MachineKeyStore https://github.com/CryptoPro/corefx/blob/c704e18e502d41e9ffb624e4aaf362180f7f7cd8/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.PrivateKey.Win.cs#L126-L132

Так с ходу ломать внутренние интерфейсы не возьмёмся пока что. Если что то выясниться дополнительно - отпишусь.

Получить флаги из CspKeyContainerInfo можно через reflection, поле _parameters типа CspParameters. Аналогичное поле можно достать и из CspKeyContainerInfo. Свойство всегда было закрытым и в старом фремворке, открывать пока не планируем.

ekx77 commented 3 years ago

C рефлексией связываться очень бы не хотелось, лучше метод или свойство, хотя бы для Gost* классов.

Fasjeit commented 3 years ago

Применительно к описанному сценарию вряд ли от использования CspParameters будет толк.

 // поиск сертификата со ссылкой в хранилище, например установленным туда средствами УЦ 2.0
X509Certificate2 cert = ...
// При чтении ключа вызывается метод GetGost3410_2012_256PrivateKey,
// который перевызывает метод  GetPrivateKey<T>
// и далее GetPrivateKeyCsp, который, как указано выше, игнорирует все флаги кроме CRYPT_MACHINE_KEYSET
// и заполняет cspParameters. Далее на основе данных параметров открывается 
// контекст провайдера и получается ключ. 
var key = cert.PrivateKey as Gost3410_2012_256CryptoServiceProvider;
var containerInfo = key.CspKeyContainerInfo;
var cert = new X509Certificate2("test.cer");
var p = new CspParameters(provType, provName, containerName)
{
    KeyNumber = 1,
    Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.NoPrompt,
};

var key = new Gost3410_2012_256CryptoServiceProvider(p);
// Логика при выставлении ключа - копируем параметры, из флагов копирует только CRYPT_MACHINE_KEYSET
// (см выше). Заполняем и запоминаем cspParameters.
cert.PrivateKey = key; 
// если предположить, что реализовано свойство чтения\записи у `CspParameters Parameters` объекта
// Gost3410_2012_256CryptoServiceProvider, то при обращении к cert.PrivateKey произойдёт его 
// инициализация на основе существующих параметров (см комментарии ранее) параметров.
// после инициализации менять параметры дело бесполезное, так как ключ и провайдер уже 
// проинициализированы. Поэтому поля CspParameters закрыты или read-only, так как из изменение
// не вызывает переинициализацию провайдеров.

Иными словами, при чтении вы этот параметр не получите, так как он не выставляется, а при записи не сможете использовать, так как криптопровайдер и ключ уже инициализированы.

ps. Если нужно поработать с сертификатом pfx с ключом без окошек и созданием контейнеров можно экспериментально воспользоваться новым флагом X509KeyStorageFlags.CspNoPersistKeySet в конструкторе x509.

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

using (var certificate = new X509Certificate2(
    path,
    "1",
    X509KeyStorageFlags.CspNoPersistKeySet))
{
}
ekx77 commented 3 years ago

Менять CspParameters у существующего PrivateKey или Gost*Provider не требуется.

Возможный вариант при чтении: 1) В GetPrivateKeyCsp у CRYPT_KEY_PROV_INFO флаг CRYPT_SILENT не игнорировать, а записывать в CspParameters, который используется для создания Gost*Provider. 2) У классов Gost*Provider сделать getter, который бы возвращал копию CspParameters. переданного в конструкторе.

Возможный вариант при записи: 1) Создавать Gost*Provider по CspParameters в конструкторе. 2) В SetCspPrivateKey учитывать NoPrompt при заполнении CRYPT_KEY_PROV_INFO.

maxdm commented 3 years ago

По опыту, CRYPT_SILENT в ссылке на ключ - причина многих необъяснимых проблем у пользователей. Вероятно поэтом, MSFT не стал переносить этот флаг из ссылки на ключ в объект криптопровайдера. Соответсвенно и мы не будем так делать по умолчанию.