06 июля 2021

Получение SHA1-хеша цифрового сертификата

    Одним из вариантов сертификата при программном добавлении цифровой подписи в PDF документ может быть идентифицированный по SHA1-хешу сертификат из хранилища сертификатов Windows. Получение без использования сторонних библиотек списка персональных сертификатов из хранилища я рассмотрел в статье Список персональных сертификатов. Но структура CERT_CONTEXT (record TCertificate в Delphi) для хранения информации о сертификате не содержит нужный нам SHA1-хеш. Давайте посмотрим, как достать его из сертификата.
    Для этого нам понадобится функция 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 более подробно рассмотрены в статье Список персональных сертификатов.

1 комментарий:

  1. Анонимный20 июля, 2021 11:10

    Пара мелких замечаний:
    Вместо GetMem я бы использовал TBytes.
    Вместо цикла с IntToHex - BinToHex.

    ОтветитьУдалить