Одним из вариантов сертификата при программном добавлении цифровой подписи в PDF документ может быть идентифицированный по SHA1-хешу сертификат из хранилища сертификатов Windows. Получение без использования сторонних библиотек списка персональных сертификатов из хранилища я рассмотрел в статье Список персональных сертификатов. Но структура CERT_CONTEXT (record TCertificate в Delphi) для хранения информации о сертификате не содержит нужный нам SHA1-хеш. Давайте посмотрим, как достать его из сертификата.
Для этого нам понадобится функция CertGetCertificateContextProperty, которая позволяет по указателю на структуру CERT_CONTEXT получить SHA1-хеш сертификата. Дополним программу получения списка персональных сертификатов импортом из Crypt32.dll функции CertGetCertificateContextProperty (в System.Net.HttpClient.Win.pas она не импортирована) и небольшой функцией для ее вызова:
Для этого нам понадобится функция CertGetCertificateContextProperty, которая позволяет по указателю на структуру CERT_CONTEXT получить SHA1-хеш сертификата. Дополним программу получения списка персональных сертификатов импортом из Crypt32.dll функции CertGetCertificateContextProperty (в System.Net.HttpClient.Win.pas она не импортирована) и небольшой функцией для ее вызова:
program CertList; {$APPTYPE CONSOLE} uses System.Classes, System.SysUtils, System.DateUtils, Winapi.Windows, System.Net.URLClient, System.Net.HttpClient.Win; function CertGetCertificateContextProperty(pCertContext: PCCERT_CONTEXT; dwPropId: DWORD; pvData : PVOID; pcbData : PDWORD):BOOL; stdcall; external Crypt32 name 'CertGetCertificateContextProperty' delayed; function GetCertSHA1Hash(const pCert: PCCERT_CONTEXT): String; const CERT_SHA1_HASH_PROP_ID = 3; var dwSize: DWORD; pbData: PByte; begin Result := ''; dwSize := 0; // получаем размер буфера для хеша if CertGetCertificateContextProperty(pCert, CERT_SHA1_HASH_PROP_ID, nil, @dwSize) and (dwSize > 0) then begin GetMem(pbData, dwSize); try // получаем хеш и преобразуем его в строку if CertGetCertificateContextProperty(pCert, CERT_SHA1_HASH_PROP_ID, pbData, @dwSize) then for var i := 0 to dwSize - 1 do Result := Result + IntToHex(pbData[i], 2); finally FreeMem(pbData); end; end; end; procedure GetCertificates; var hStore: HCERTSTORE; pCert: PCCERT_CONTEXT; CertInfo: TCertificate; begin // открываем хранилище сертификатов пользователя hStore := CertOpenSystemStore(0, 'MY'); if hStore = nil then WriteLn('Can''t open the store') else try // находим в хранилище первый сертификат pCert := CertEnumCertificatesInStore(hStore, nil); while pCert <> nil do begin // копируем информацию из структуры Win32 API в запись типа TCertificate CryptCertToTCertificate(pCert, CertInfo); if not CertInfo.IsEmpty then begin WriteLn('***'); WriteLn('Name: ' + CertInfo.CertName); WriteLn('SerialNum: ' + CertInfo.SerialNum); WriteLn('SHA1 digest: ' + GetCertSHA1Hash(pCert)); WriteLn('Start: ' + DateTimeToStr(TTimeZone.Local.ToLocalTime(CertInfo.Start))); WriteLn('Expiry: ' + DateTimeToStr(TTimeZone.Local.ToLocalTime(CertInfo.Expiry))); WriteLn('Subject: ' + CertInfo.Subject); WriteLn('Issuer: ' + CertInfo.Issuer); end; // находим в хранилище следующий сертификат pCert := CertEnumCertificatesInStore(hStore, pCert); end; finally // закрываем хранилище сертификатов CertCloseStore(hStore, CERT_CLOSE_STORE_FORCE_FLAG); end; end; begin ReportMemoryLeaksOnShutdown := True; try GetCertificates; except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.Теперь давайте разберемся с программой, которая использует для выбора персонального сертификата стандартное диалоговое окно Windows. Вызывающая его функция ShowSelectCertificateDialog возвращает информацию о сертификате в параметре типа TCertificate, а нам нужен указатель на структуру CERT_CONTEXT. Поэтому сначала найдем выбранный сертификат по его серийному номеру (в System.Net.HttpClient.Win.pas уже есть готовая для этого функция – FindCertWithSerialNumber), а потом вызовем CertGetCertificateContextProperty:
unit Unit1; interface uses Vcl.Forms, Vcl.Controls, Vcl.StdCtrls, System.Classes, System.SysUtils, System.DateUtils, Winapi.Windows, System.Net.HttpClient.Win, System.Net.URLClient; type TForm1 = class(TForm) btnSelectCert: TButton; mCert: TMemo; procedure btnSelectCertClick(Sender: TObject); private function GetCertSHA1Hash(const sSerialNumber: String): String; end; var Form1: TForm1; implementation {$R *.dfm} function CertGetCertificateContextProperty(pCertContext: PCCERT_CONTEXT; dwPropId: DWORD; pvData : PVOID; pcbData : PDWORD):BOOL; stdcall; external Crypt32 name 'CertGetCertificateContextProperty' delayed; function TForm1.GetCertSHA1Hash(const sSerialNumber: String): String; const CERT_SHA1_HASH_PROP_ID = 3; var hStore: HCERTSTORE; pCert : PCCERT_CONTEXT; begin Result := ''; // открываем хранилище сертификатов пользователя hStore := CertOpenSystemStore(0, 'MY'); if hStore = nil then Result := 'Ошибка открытия хранилища сертификатов ' + IntToStr(GetLastError) else try // ищем сертификат по его серийному номеру pCert := FindCertWithSerialNumber(hStore, sSerialNumber); if pCert = nil then Result := 'Сертификат ' + sSerialNumber + ' не найден' else try // получаем хеш var dwSize: DWORD := 0; // получаем размер буфера для хеша if CertGetCertificateContextProperty(pCert, CERT_SHA1_HASH_PROP_ID, nil, @dwSize) and (dwSize > 0) then begin var pbData: PByte; GetMem(pbData, dwSize); try // получаем хеш и преобразуем его в строку if CertGetCertificateContextProperty(pCert, CERT_SHA1_HASH_PROP_ID, pbData, @dwSize) then for var i := 0 to dwSize - 1 do Result := Result + IntToHex(pbData[i], 2); finally FreeMem(pbData); end; end; finally // освобождаем память CertFreeCertificateContext(pCert); end; finally // закрываем хранилище сертификатов CertCloseStore(hStore, CERT_CLOSE_STORE_FORCE_FLAG); end; end; procedure TForm1.btnSelectCertClick(Sender: TObject); var cert: TCertificate; begin mCert.Clear; if ShowSelectCertificateDialog(Handle, 'Это ATitle', 'Это ADisplayString', cert) then begin mCert.Lines.Add('Name: ' + cert.CertName); mCert.Lines.Add('SerialNum: ' + cert.SerialNum); mCert.Lines.Add('SHA1 digest: ' + GetCertSHA1Hash(cert.SerialNum)); mCert.Lines.Add('Start: ' + DateTimeToStr(TTimeZone.Local.ToLocalTime(cert.Start))); mCert.Lines.Add('Expiry: ' + DateTimeToStr(TTimeZone.Local.ToLocalTime(cert.Expiry))); mCert.Lines.Add('Subject: ' + cert.Subject); mCert.Lines.Add('Issuer: ' + cert.Issuer); end; end; end.P.S. Обе программы и модуль System.Net.HttpClient.Win.pas более подробно рассмотрены в статье Список персональных сертификатов.
Пара мелких замечаний:
ОтветитьУдалитьВместо GetMem я бы использовал TBytes.
Вместо цикла с IntToHex - BinToHex.