Одним из вариантов сертификата при программном добавлении цифровой подписи в 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.