2004 г.
Windows и Delphi на защите секретов (часть 4)
Константин Виноградов, Комиздат
Первые три части статьи
Что такое сертификат? Какова его роль в защите информации? И, самое главное, - как можно использовать на практике механизм сертификации? Об этом и пойдет речь в нашей статье
Предположим, две стороны (назовем их по традиции Алисой и Бобом) решили обменяться открытыми ключами по незащищенному каналу связи, например по телефонной линии или интернету. Некто третий (по имени Ева) во время пересылки может подменить ключи и получить таким образом доступ к секретной переписке. Чтобы исключить эту угрозу безопасности, нужно иметь возможность подтвердить подлинность полученного открытого ключа. Для этого и предусмотрены сертификаты.
Сертификаты
Представим себе, что существует организация, которой Боб полностью доверяет. Назовем ее Центром сертификации (Certification Authority). Алиса отсылает свой открытый ключ в Центр сертификации (ЦС). Там у нее запрашивают документы и устанавливают, действительно ли она та особа, за которую себя выдает. Затем ЦС генерирует электронный документ, содержащий информацию об Алисе и ее открытый ключ, и подписывает его своим закрытым ключом. Этот цифровой документ и является сертификатом. Сертификат может не только служить для аутентификации личности (как в случае с Алисой), но и подтверждать подлинность оборудования, веб-сайта, организации и т.п. Поэтому набор сведений, входящих в его состав, может изменяться и удовлетворять одному из стандартов.
Как правило, сертификат содержит следующие данные:
- серийный номер сертификата;
- ФИО (название) владельца сертификата;
- открытый ключ владельца;
- срок действия (даты начала и конца пригодности сертификата);
- название ЦС;
- электронная цифровая подпись ЦС.
Итак, ЦС удостоверяет личность Алисы и своей подписью заверяет связь между ней и ее ключом.
Поскольку сведения об Алисе подписаны, Боб может убедиться в их целостности. Для этого ему нужно обратиться в ЦС и проверить цифровую подпись, входящую в состав сертификата. Даже если Ева, о который мы упомянули вначале, перехватит сертификат и изменит сведения об Алисе, то ей придется подделывать подпись ЦС. Кроме того, Боб тоже может получить в ЦС "электронное удостоверение личности" - и тогда уверенностью в подлинности открытых ключей будут обладать оба корреспондента.
Остался один невыясненный вопрос: на чем держится доверие к Центру сертификации? Если Алиса и Боб - сотрудники одной и той же организации, а переписку ведут в деловых целях, то в качестве ЦС может выступить сама организация. Для этого ей потребуется обзавестись штатным специалистом по защите информации и поручить ему выполнение всех процедур, связанных с выдачей и проверкой сертификатов. Ну а на чем должно основываться доверие фирмы к своему сотруднику, думаем, объяснять не нужно.
В случае же, если Алиса и Боб - представители различных организаций, им придется обращаться в независимый центр сертификации. Деятельность таких центров, как правило, лицензируется службами безопасности страны или другими государственными органами. Например, украинские организации, работающие в сфере защиты информации, должны получить лицензию СБУ. Сертификату, выданному таким ЦС, можно доверять в той же степени, в какой вы доверяете паспорту гражданина некого государства.
Центры сертификации могут находиться в различных странах, работать с различным программным обеспечением, поддерживать разные стандарты сертификатов и отличающиеся процедуры их выдачи. Поэтому возникает необходимость наладить отношения доверия между самими ЦС. Эта система доверительных отношений может строиться по иерархическому принципу. Существует небольшое число ЦС, называющихся корневыми (Root Certification Authority). Они удостоверяют сертификаты дочерних центров, которые, в свою очередь, подтверждают подлинность других ЦС и т.д. В глобальном масштабе структура ЦС представляет собой несколько иерархий (см. рис. 1).
Поддержка сертификатов в CryptoAPI
Рассмотрим решение основных задач, возникающих при выпуске и обслуживании сертификатов на уровне библиотеки ОС Windows - CryptoAPI. Кстати говоря, для "внутреннего" употребления сертификатов (в рамках одной организации) процедуру лицензирования проходить не нужно. Никто ведь не может запретить вам обмениваться файлами, которые по секрету от СБУ вы будете считать цифровыми документами.
Чтобы обзавестись собственным сертификатом, прежде всего нужно создать запрос специального формата, который должен содержать ваше имя, открытые ключи обмена ключами и подписи. Этот запрос подписывается отправителем при помощи личного закрытого ключа и отсылается в центр сертификации.
Запрос на получение сертификата
Рассмотрим процесс создания запроса более подробно. Для хранения данных запроса в CryptoAPI предназначена специальная структура - CERT_REQUEST_INFO. Она содержит ссылки на другие структуры, хранящие имя владельца сертификата, информацию об открытых ключах и, возможно, некоторые дополнительные атрибуты (рис. 2). В результате для создания запроса сертификата следует выполнить следующие шаги:
- Создать строку, содержащую поле Subject сертификата.
- Создать массив структур CERT_RDN_ATTR (в простейшем случае он будет состоять из единственного элемента) и инициализировать каждый его элемент следующими данными: строкой идентификатора объекта, типом хранимого значения, длиной строки, созданной на первом шаге, и самой этой строкой, приведенной к типу PBYTE. Аббревиатура RDN означает Relative Distinguished Name (относительные отличительные признаки). Структура RDN может хранить, например, имя пользователя, адрес, страну, наименование подразделения организации и т.п. При этом хранимые данные характеризуются идентифицирующей строкой: скажем, имя владельца сертификата должно иметь строку-идентификатор "2.5.4.3", двухбуквенный код страны - "2.5.4.6", и т.д. (подробное описание можно найти в справке по CryptoAPI). Способ представления данных строки описывается типом хранимого значения - например, CERT_RDN_PRINTABLE_STRING или CERT_RDN_ENCODED_BLOB.
- Создать массив структур CERT_RDN (в простейшем случае и он будет содержать лишь один элемент) и инициализировать каждый его элемент количеством элементов в соответствующем массиве CERT_RDN_ATTR, созданном на шаге 2, и ссылкой на первый элемент этого массива.
- Создать структуру CERT_NAME_INFO и инициализировать ее поля значениями количества элементов массива, созданного на шаге 3, и ссылкой на первый элемент этого массива.
- Вызвать функцию CryptEncodeObject, передав ей в качестве соответствующего параметра структуру CERT_NAME_INFO из шага 4. Результатом работы функции будет структура типа CERT_NAME_BLOB, содержащая входную информацию в закодированном виде. При обращении к функции следует указать тип кодировки; документация по CryptoAPI рекомендует использовать кодировку в виде комбинации следующих констант: PKCS_7_ASN_ENCODING or X509_ASN_ENCODING (другие типы могут не поддерживаться).
- Записать в поле Subject структуры CERT_REQUEST_INFO ссылку на структуру CERT_NANE_BLOB, созданную и инициализированную на шаге 5.
- Если в запрос сертификата должна быть включена некоторая дополнительная информация, нужно поместить ее в заранее созданный массив структур CRYPT_ATTR_BLOB, а ссылками на такие массивы инициализировать массив CRYPT_ATTRIBUTE. Ссылка на последний массив и количество элементов в нем записывается в соответствующие поля структуры CERT_REQUEST_INFO. Для ясности изложения этот этап рассматривать не будем.
- В структуру CERT_REQUEST_INFO необходимо вписать номер версии сертификата. CryptoAPI поддерживает сертификаты трех версий: 1 - сертификат содержит минимальный набор данных (работу именно с такими сертификатами мы и рассмотрим); 2 - сертификат содержит дополнительно уникальные идентификаторы владельца и издателя; 3 - сертификат включает дополнительную информацию об издателе (Центре сертификации), например его адрес электронной почты или лицензию на выпуск сертификатов.
- Вызвать функцию CryptExportPublicKeyInfo, которая вернет инициализированную структуру CERT_PUBLIC_KEY_INFO, содержащую открытые части ключей пользователя.
- Записать в поле SubjectPublicKeyInfo структуры CERT_REQUEST_INFO ссылку на структуру, созданную на шаге 9.
- Вызвать функцию CryptSignAndEncodeCertificate, передав ей в качестве аргумента структуру CERT_REQUEST_INFO. Эта функция закодирует структуру CERT_REQUEST_INFO и все данные, на которые в этой структуре имеются ссылки, подпишет эту закодированную информацию и еще раз закодирует уже подписанные данные.
Полученный в результате подписанный и закодированный запрос сертификата нужно сохранить в файле и отправить в центр сертификации. Далее приведен текст процедуры (без обработки ошибок), реализующей описанный выше алгоритм:
{SubjectEdit - поле формы, в которое
пользователь вводит текст
поля Subject сертификата; поле формы
ContainerEdit может содержать
имя контейнера ключей (если остается пустым,
используется контейнер
по умолчанию)}
procedure TCreateReqForm.OKBtnClick (Sender: TObject);
var nameAttr: CERT_RDN_ATTR;
nameString: PChar;
rdn: CERT_RDN;
nameInfo: CERT_NAME_INFO;
certReqInfo: CERT_REQUEST_INFO;
subjNameBlob: CERT_NAME_BLOB;
encNameLen: DWORD;
encName: PBYTE;
prov: HCRYPTPROV;
pubKeyInfoLen: DWORD;
pubKeyInfo: PCERT_PUBLIC_KEY_INFO;
encCertReqLen: DWORD;
params: CRYPT_OBJID_BLOB;
sigAlg: CRYPT_ALGORITHM_IDENTIFIER;
signedEncCertReq: PBYTE;
cont: PChar;
err: string;
encType: DWORD;
f: file;
begin
encType:= PKCS_7_ASN_ENCODING or X509_ASN_ENCODING;
nameString:= StrAlloc (length (SubjectEdit.text)+1);
StrPCopy (nameString, SubjectEdit.Text);
nameAttr.pszObjId:= '2.5.4.3';
nameAttr.dwValueType:= CERT_RDN_PRINTABLE_STRING;
nameAttr.Value.cbData:= length (SubjectEdit.text);
nameAttr.Value.pbData:= PBYTE (nameString);
rdn.cRDNAttr:= 1;
rdn.rgRDNAttr:= @nameAttr;
nameInfo.cRDN:= 1;
nameInfo.rgRDN:= @rdn;
{выясняем размер закодированного имени пользователя}
CryptEncodeObject (encType, X509_NAME,
@nameInfo, nil, @encNameLen) or (encNameLen < 1);
GetMem (encName, encNameLen);
{кодируем имя пользователя}
CryptEncodeObject (PKCS_7_ASN_ENCODING or
X509_ASN_ENCODING, X509_NAME, @nameInfo, encName,
@encNameLen) or (encNameLen < 1);
subjNameBlob.cbData:= encNameLen;
subjNameBlob.pbData:= encName;
certReqInfo.Subject:= subjNameBlob;
certReqInfo.cAttribute:= 0;
certReqInfo.rgAttribute:= nil;
certReqInfo.dwVersion:= CERT_REQUEST_V1;
if length (ContainerEdit.Text) = 0
then cont:= nil
else
begin
err:= ContainerEdit.Text;
cont:= StrAlloc (length (err) + 1);
StrPCopy (cont, err);
end;
CryptAcquireContext (@prov, cont, nil,
PROV_RSA_FULL, 0);
CryptExportPublicKeyInfo (prov, AT_SIGNATURE,
encType, nil, @pubKeyInfoLen);
GetMem (pubKeyInfo, pubKeyInfoLen);
CryptExportPublicKeyInfo (prov, AT_SIGNATURE,
encType, pubKeyInfo, @pubKeyInfoLen);
certReqInfo.SubjectPublicKeyInfo:= pubKeyInfo^;
FillChar (params, sizeof (params), 0);
sigAlg.pszObjId:= szOID_OIWSEC_sha1RSASign;
sigAlg.Parameters:= params;
CryptSignAndEncodeCertificate (prov, AT_SIGNATURE,
encType,
X509_CERT_REQUEST_TO_BE_SIGNED, @certReqInfo,
@sigAlg, nil, nil,
@encCertReqLen);
GetMem (signedEncCertReq, encCertReqLen);
CryptSignAndEncodeCertificate (prov, AT_SIGNATURE,
encType, X509_CERT_REQUEST_TO_BE_SIGNED,
@certReqInfo, @sigAlg, nil, signedEncCertReq,
@encCertReqLen);
if SaveDlg.Execute then
begin
AssignFile (f, SaveDlg.FileName);
rewrite (f, 1);
BlockWrite (f, signedEncCertReq^, encCertReqLen);
CloseFile (f);
end;
StrDispose (nameString);
FreeMem (encName, encNameLen);
if cont <> nil then StrDispose (cont);
FreeMem (pubKeyInfo, pubKeyInfoLen);
FreeMem (signedEncCertReq, encCertReqLen);
CryptReleaseContext (prov, 0);
end;
Итак, запрос сертификата создан, но что с ним делать? Если вам нужен сертификат, который можно использовать для организации безопасной связи с зарубежными партнерами, нужно отправить созданный запрос в центр сертификации, предоставив необходимые подтверждающие личность документы и уплатив соответствующую сумму. Если вы собираетесь обмениваться только в рамках сети предприятия, располагающей сервером Windows 2000 и выше, можно воспользоваться Microsoft Certificate Server. Если же вашей целью, например, является только отладка программ, или если группа, внутри которой вы собираетесь организовать обмен информацией, располагает лишь компьютерами под Windows 98 (и ниже), то можно подписать сертификат самостоятельно.
Подписание сертификата
При этом важно не забыть цель создания сертификата - он создавался, чтобы связь между именем пользователя и его открытым ключом удостоверялась подписью некоторой доверенной стороны. Такой "доверенный" пользователь внутри группы (будем называть его администратором) должен создать ключевую пару администратора и создать для нее сертификат, который он подписывает тем же закрытым ключом администратора. Этот сертификат называется корневым. Ключ администратора используется и для подписания сертификатов пользователей. Когда один пользователь хочет проверить подлинность сертификата другого пользователя, он проверяет подпись под ним администратора, используя для проверки тот самый корневой сертификат.
Итак, сделаем из только что созданного запроса сертификата подписанный корневой сертификат. Это можно проделать при помощи функций CryptoAPI. Сама процедура в документации даже не упоминается, однако ее можно "вывести" из описания соответствующих функций. Проследим ее на примере программы, позволяющей открыть файл с запросом сертификата, проверить подпись под ним и выпустить подписанный сертификат с заданным сроком действия. Программа управляется формой, показанной на рис. 3.
Вначале необходимо открыть запрос и проверить подпись под ним.
Проверяем, задано ли нестандартное имя контейнера, и подключаемся к криптопровайдеру.
Открываем файл с закодированным запросом сертификата и считываем его содержимое в память (указатель reqEncoded).
Декодируем запрос:
{encType, size, rsize: DWORD;
buf: PBYTE;}
encType:= PKCS_7_ASN_ENCODING or X509_ASN_ENCODING;
GetMem (buf, 2048);
rsize:= 2048;
CryptDecodeObject (encType, X509_CERT,
reqEncoded, size, 0, buf, @rsize);
В декодированном запросе выделяем и декодируем информацию, предназначенную для подписания - уже знакомую нам структуру CERT_REQUEST_INFO:
{p: pointer;
signedContent: PCERT_SIGNED_CONTENT_INFO;
DERBLOB: CRYPT_DER_BLOB;
buf2: PBYTE;
pCertReqInfo: PCERT_REQUEST_INFO;}
p:= buf;
signedContent:= p;
DERBLOB:= signedContent^.toBeSigned;
rsize:= 512;
GetMem (buf2, rsize);
CryptDecodeObject (encType,
X509_CERT_REQUEST_TO_BE_SIGNED,
DERBLOB.pbData, DERBLOB.cbData, 0, buf2, @rsize);
p:= buf2;
pCertReqInfo:= p;
Извлекаем из полученной информации строку с именем (названием) владельца сертификата, чтобы отобразить ее на форме:
{subjNameBLOB: CERT_NAME_BLOB;
subjNameString: PChar;}
subjNameBLOB:= pCertReqInfo^.Subject;
rsize:= CertNameToStr (encType, @subjNameBLOB,
CERT_SIMPLE_NAME_STR, subjNameString, 512);
subjectEdit.Text:= subjNameString;
Проверяем подпись под запросом сертификата:
signCheckBox.Checked:=
CryptVerifyCertificateSignature(prov, encType,
reqEncoded, size,
@(pCertReqInfo^.SubjectPublicKeyInfo));
Напоследок - заполняем поля формы "Действителен с" и "Действителен по" текущей датой и датой, отстоящей на год вперед:
NotBeforeEdit.Text:= DateTimeToStr (Now);
NotAfterEdit.Text:= DateTimeToStr (Now + 365);
Освобождаем выделенную память и отключаемся от криптопровайдера.
После того, как пользователь убедился в правильности подписи под запросом, откорректировал при необходимости срок действия создаваемого сертификата, указал его серийный номер, вписал название издателя (администратора, центра сертификации) и указал в поле "Контейнер" имя контейнера ключей администратора, начинаем процесс подписания. Для этого следует создать и заполнить структуру CERT_INFO, являющуюся, как сказано в документации, "сердцем сертификата" (рис. 4 - серым цветом обозначены поля, содержащие закодированную информацию).
Подключаемся к контейнеру ключей администратора.
Заполняем поля сертификата, представленного структурой CERT_INFO:
{certInfo: CERT_INFO;
serNum: int64;
params: CRYPT_OBJID_BLOB;
nameStr: PChar;
nameAttr: CERT_RDN_ATTR;
rdn: CERT_RDN;
nameInfo: CERT_NAME_INFO;}
certInfo.dwVersion:= CERT_V1; {версия 1}
serNum:= strtoint64(serNumEdit.Text);
certInfo.SerialNumber.cbData:= sizeof (serNum);
certInfo.SerialNumber.pbData:= @serNum;
FillChar (params, sizeof (params), 0);
certInfo.SignatureAlgorithm.pszObjId:=
szOID_OIWSEC_sha1RSASign;
certInfo.SignatureAlgorithm.Parameters:= params;
nameStr:= StrAlloc (length (IssuerEdit.text)+1);
StrPCopy (nameStr, IssuerEdit.Text);
nameAttr.pszObjId:= '2.5.4.3';
nameAttr.dwValueType:= CERT_RDN_PRINTABLE_STRING;
nameAttr.Value.cbData:= length (IssuerEdit.text);
nameAttr.Value.pbData:= PBYTE (nameStr);
rdn.cRDNAttr:= 1;
rdn.rgRDNAttr:= @nameAttr;
nameInfo.cRDN:= 1;
nameInfo.rgRDN:= @rdn;
Кодируем строку, содержащую название издателя сертификата:
{encNameLen: DWORD;
encName: PBYTE;
issNameBLOB: CERT_NAME_BLOB;}
CryptEncodeObject (encType, X509_NAME,
@nameInfo, nil, @encNameLen);
GetMem (encName, encNameLen);
CryptEncodeObject (encType, X509_NAME,
@nameInfo, encName, @encNameLen);
issNameBlob.cbData:= encNameLen;
issNameBlob.pbData:= encName;
certInfo.Issuer:= issNameBLOB;
Кодируем даты начала и конца срока действия сертификата:
{sysTime: TSystemTime;}
DateTimeToSystemTime (StrToDateTime
(NotBeforeEdit.Text), sysTime);
SystemTimeToFileTime (sysTime, certInfo.notBefore);
DateTimeToSystemTime (StrToDateTime
(NotAfterEdit.Text), sysTime);
SystemTimeToFileTime (sysTime,
certInfo.notAfter);
Поля сертификата, содержащие информацию о его владельце, заполняем на основании данных, содержащихся в декодированном запросе сертификата:
certInfo.Subject:= pCertReqInfo.Subject;
certInfo.SubjectPublicKeyInfo:=
pCertReqInfo.SubjectPublicKeyInfo;
В неиспользуемые поля вписываем нули и пустые указатели:
certInfo.IssuerUniqueId.cbData:= 0;
certInfo.IssuerUniqueId.pbData:= nil;
certInfo.IssuerUniqueId.cUnusedBits:= 0;
certInfo.SubjectUniqueId.cbData:= 0;
certInfo.SubjectUniqueId.pbData:= nil;
certInfo.SubjectUniqueId.cUnusedBits:= 0;
certInfo.cExtension:= 0;
certInfo.rgExtension:= nil;
Подписываем и кодируем сертификат:
{encCertLen: DWORD;
encCert: PByte;}
CryptSignAndEncodeCertificate (prov, AT_SIGNATURE,
encType,
X509_CERT_TO_BE_SIGNED, @certInfo,
@(certInfo.SignatureAlgorithm),
nil, nil, @encCertLen);
GetMem (encCert, encCertLen);
CryptSignAndEncodeCertificate (prov, AT_SIGNATURE,
encType,
X509_CERT_TO_BE_SIGNED, @certInfo,
@(certInfo.SignatureAlgorithm), nil,
encCert, @encCertLen);
Сохраняем подписанный и закодированный сертификат в файле, освобождаем память и дескриптор криптопровайдера.
Хранение сертификатов
Созданный сертификат владелец может отправлять своим корреспондентам для организации безопасной связи. Чтобы сделать полученный сертификат доступным системе, его нужно поместить в одно из хранилищ сертификатов. Windows предусматривает существование нескольких системных хранилищ сертификатов: MY (для хранения сертификатов отдельного пользователя), СА (от Certification Authority - для хранения сертификатов центров сертификации) и ROOT (для хранения корневых сертификатов). Открытое (загруженное в память) хранилище сертификатов представляет собой связанный список блоков данных, каждый из которых содержит ссылку на следующий блок и на данные сертификата. Каждое хранилище сертификатов физически размещается либо в отдельном файле, либо в реестре Windows. При этом для работы с системными хранилищами не нужно знать их местоположения - достаточно указать приведенные выше имена.
Для помещения сохраненного в файле подписанного сертификата в системное хранилище нужно выполнить следующие шаги.
Открыть файл и считать его содержимое в буфер.
При помощи специальной функции создать контекст сертификата:
{encCertLen: DWORD; - размер закодированного сертификата
encCert: PByte; - данные сертификата
context: PCCERT_CONTEXT;
encType: DWORD;}
encType:= PKCS_7_ASN_ENCODING or X509_ASN_ENCODING;
context:= CertCreateCertificateContext
(encType, encCert, encCertLen);
Открыть системное хранилище сертификатов:
{store: HCERTSTORE;}
store:= CertOpenSystemStore (0, 'MY');
Добавить контекст сертификата в открытое хранилище:
n:= nil;
CertAddCertificateContextToStore (store, context,
CERT_STORE_ADD_REPLACE_EXISTING, n)
Функции CertAddCertificateContextToStore, кроме дескрипторов хранилища и добавляемого контекста сертификата, передается параметр, определяющий действия системы в том случае, если в данном хранилище уже имеется идентичный сертификат. Использованная константа CERT_STORE_ADD_REPLACE_EXISTING предписывает в таком случае удалить старый сертификат и заменить его новым. Последний параметр функции позволяет получить указатель на указатель на копию сертификата, созданную при добавлении контекста в хранилище (если параметр равен пустому указателю, то ссылка не возвращается).
Закрыть хранилище, освободить контекст сертификата и память.
CertCloseStore (store, 0);
CertFreeCertificateContext (context);
FreeMem (encCert, encCertLen);
Просмотреть имеющиеся в хранилище сертификаты можно с помощью функции CertEnumCertificatesInStore. Ей нужно передать дескриптор нужного хранилища и, при первом вызове, пустой указатель, а при последующих - указатель на предыдущий сертификат. Например, для просмотра содержимого одного из системных хранилищ сертификатов может быть использован следующий фрагмент программы (форма, из которой он вызывается, с результатами работы показана на рис. 5):
{store: HCERTSTORE;
cont, stor: PChar;
err: string;
cert: PCCERT_CONTEXT;
nameString: PChar;
size: DWORD;
nameBLOB: CERT_NAME_BLOB;
подключение к криптопровайдеру считается выполненным!}
err:= CertStoreBox.Text;
RepMemo.Lines.Add ('');
RepMemo.Lines.Add ('====================');
RepMemo.Lines.Add ('Contents of store ' + err);
RepMemo.Lines.Add ('====================');
stor:= StrAlloc (length (err) + 1);
StrPCopy (stor, err);
store:= CertOpenSystemStore (prov, stor);
cert:= CertEnumCertificatesInStore (store, nil);
nameString:= StrAlloc (512);
while cert <> nil do
begin
RepMemo.Lines.Add ('---------------');
RepMemo.Lines.Add ('Subject:');
nameBLOB:= cert^.pCertInfo^.Subject;
size:= CertNameToStr (encType, @nameBlob,
CERT_SIMPLE_NAME_STR, nameString, 512);
if size > 1 then
RepMemo.Lines.Add (nameString)
else
RepMemo.Lines.Add ('Error');
RepMemo.Lines.Add ('Issuer:');
nameBLOB:= cert^.pCertInfo^.Issuer;
size:= CertNameToStr (encType, @nameBlob,
CERT_SIMPLE_NAME_STR,
nameString, 512);
if size > 1 then
RepMemo.Lines.Add (nameString)
else
RepMemo.Lines.Add ('Error');
cert:= CertEnumCertificatesInStore (store, cert);
end;
StrDispose (nameString);
Проверка сертификата
Получив новый сертификат, конечно, следует убедиться в корректности подписи под ним. При этом сам сертификат, скорее всего, будет помещен в личное хранилище пользователя (MY), а сертификат его издателя может находиться в хранилищах CA или ROOT. Кстати, при установке Windows в эти хранилища помещается множество сертификатов признанных центров сертификации, список которых можно просмотреть при помощи приведенной выше программы.
Для выполнения проверки следует считать из проверяемого сертификата строку с именем его издателя и найти в одном из системных хранилищ его сертификат. Для облегчения операции поиска в нескольких хранилищах CryptoAPI 2.0 поддерживает механизм коллекций (или логических хранилищ) сертификатов. Коллекция может объединять данные из нескольких физических хранилищ и выполнять операции над всеми их сертификатами одновременно. Кстати говоря, в Windows 2000 и выше системные хранилища сертификатов сами представляют собой коллекции.
Поиск сертификата издателя и проверка заданного сертификата выполняются одновременно функцией CertGetIssuerCertificateFromStore. Ей следует передать дескриптор хранилища сертификатов, ссылку на контекст проверяемого сертификата, возможно - ссылку на предыдущий найденный сертификат издателя (если вызов функции производится повторно) и ссылку на флаговую переменную. Эта переменная при вызове функции задает режим проверки, а при возврате - служит индикатором успеха проверки. Функция поддерживает несколько режимов проверки, которые могут комбинироваться; мы воспользуемся сочетанием двух из них - CERT_STORE_SIGNATURE_FLAG (проверить подпись издателя под заданным сертификатом) и CERT_STORE_TIME_VALIDITY_FLAG (проверить срок действия заданного сертификата).
Рассмотрим процедуру проверки сертификата с заданным полем Subject на примере программы.
Подключаемся к криптопровайдеру.
Открываем выбранное хранилище сертификатов.
Считываем из формы поле Subject и ищем соответствующий сертификат в хранилище:
{subj: PWideChar;
err: string;
cert: PCCERT_CONTEXT;
тип и значение encType - см. выше}
err:= SubjectEdit.Text;
GetMem (subj, 2 * length (err) + 1);
StringToWideChar (err, subj, 2 * length (err) + 1);
cert:= CertFindCertificateInStore (store, encType, 0,
CERT_FIND_SUBJECT_STR, subj, nil);
FreeMem (subj, 2 * length (err) + 1);
if cert = nil then
begin
MessageDlg ('Certificate not found',
mtError, [mbOK], 0);
CertCloseStore (store, 0);
exit;
end;
Создаем коллекцию (логическое хранилище) сертификатов:
{collect: HCERTSTORE;}
collect:= CertOpenStore (CERT_STORE_PROV_COLLECTION,
0, 0, 0, nil);
Открываем и помещаем в коллекцию интересующие нас системные хранилища сертификатов - MY, CA, ROOT:
{mystore, castore, rootstore: HCERTSTORE;}
mystore:= CertOpenSystemStore (prov, 'MY');
CertAddStoreToCollection (collect, mystore, 0, 0);
castore:= CertOpenSystemStore (prov, 'CA');
CertAddStoreToCollection (collect, castore, 0, 2);
rootstore:= CertOpenSystemStore (prov, 'ROOT');
CertAddStoreToCollection (collect, rootstore, 0, 1)
Последним параметром функции CertAddStoreToCollection задается приоритет добавляемого хранилища в коллекции. Мы ожидаем, что сертификат издателя окажется в хранилище СА, поэтому для этого хранилища задан самый высокий приоритет, для ROOT - более низкий, а для MY - низший.
Устанавливаем режим проверки:
{flags: DWORD;}
flags:= CERT_STORE_SIGNATURE_FLAG or
CERT_STORE_TIME_VALIDITY_FLAG;
Выполняем поиск сертификата издателя и проверку заданного сертификата:
{icert: PCCERT_CONTEXT;}
icert:= CertGetIssuerCertificateFromStore
(collect, cert, nil, @flags);
if icert = nil then
case int64(GetLastError) of
CRYPT_E_NOT_FOUND: MessageDlg ('Issuer certificate not
found', mtError, [mbOK], 0);
CRYPT_E_SELF_SIGNED: MessageDlg ('This is root
certificate', mtWarning, [mbOK], 0);
else MessageDlg ('Invalid arguments',
mtError, [mbOK], 0);
end
else if flags = 0
then MessageDlg ('Certificate is valid',
mtInformation, [mbOK], 0)
else MessageDlg ('Certificate is invalid',
mtError, [mbOK], 0);
В результате при правильном обращении к функции может возникнуть одна из четырех ситуаций:
- сертификат издателя не найден - в этом случае нужно узнать издателя данного сертификата (по строке, выдаваемой, например, рассмотренной выше программой просмотра содержимого хранилища) и на его сайте взять сертификат для проверки;
- проверяемый сертификат является корневым - использование таких сертификатов (если, конечно, это не ваш собственный сертификат, созданный под впечатлением этой статьи) требует предельного внимания, так как в этом случае подлинность данных сертификата ничем не подтверждается. Принимая такой сертификат, вы должны любыми путями убедиться, что он поступил из надежного источника (например, был помещен в хранилище ROOT при установке Windows). В противном случае вы рискуете подвергнуться такой, например, атаке: злоумышленник может прислать вам по электронной почте сообщение об обновлении корневого сертификата, заменит своими данными хранящийся у вас сертификат и сможет присылать вам фальшивые сертификаты пользователей, якобы заверенные подписью корневого центра сертификации;
- сертификат верен - подпись издателя под проверяемым сертификатом верна, срок действия проверяемого сертификата включает текущую системную дату;
- сертификат не прошел проверку - проверяемый сертификат либо просрочен, либо является подделкой. Правда, возможен еще один вариант: издатель имеет несколько сертификатов - и проверяемый сертификат подписан с использованием ключа из другого сертификата данного издателя. В таком случае нужно вызвать функцию CertGetIssuerCertificateFromStore повторно, передав в качестве третьего параметра контекст текущего сертификата издателя.
Итак, мы рассмотрели механизмы создания и управления сертификатами безопасности на основе CryptoAPI. Используя сертификаты, вы сможете выполнять действия с сообщениями при помощи многочисленных функций CryptoAPI, автоматизирующих процессы шифрования и расшифровки, подписания и проверки подписи, и представляющие результаты своих действий в соответствии с существующими стандартами.
Приложение.
Архив (rar, 84,3 KB)
Авторы ищут издателя для публикации учебного пособия по данной теме.
Персональный сайт авторов - http://www.is.svitonline.com/vilys/.