все что связано с моей работой
Главная » Програмирование » Установка сертификатов.

Установка сертификатов.

В заметке “Установка сертификатов на компьютеры в домене” описывается стандартная утилита из SDK для установки сертификатов из командной строки, которая решает проблему распространения корневого сертификата на компьютеры домена. Но есть в ней один минус…

Утилита не проверяет существования устанавливаемого сертификата, т.е. если сертификат уже установлен в системе, то утилита сначала удаляет его, а затем устанавливает новый. И в общем-то, все логично, но беда в том, что при установке вываливается окно подтверждения и надо нажать “Да”, и при удалении тоже вываливается окно подтверждения. В итоге при установке сертификата пользователю надо один раз подтвердить удаление, а потом подтвердить установку. А теперь вспомним, что это планировалось использовать в logon-скрипте. И выходит что, минимум каждый день пользователю надо будет два раза нажимать кнопку при загрузке… да они за неделю мозг вынесут…

Поискав у “всезнающего”, и него не найдя было решено сделать свой велосипед, с блэкджеком и сопутствующими …

Первый вопрос который нужно решить – это проверка сертификата в хранилище установлен он уже или нет. Интерфейс минимален, в качестве параметра программе передается имя владельца сертификата (Common Name, CN), а она в свою очередь в ExitCode возвращает один из кодов:

  • 0 – сертификат установлен;
  • 1 – не найден;
  • 2 – произошла ошибка.

В windows API есть CriptoAPI для работы с сертификатами и не только. Поискав библиотеку для Lazarus ничего не нашел, тогда для “родственного” Delphi – тоже не густо, нашел реализацию CriptoAPI в JCL. Немного исправив она нормально заработала с fpc. Но надо признать, что она или сильно устарела или по какой-то другой причине часть  API в ней не реализовано, приходилось по ходу добавлять константы. Скачать модуль можно в конце статьи.

Руководства.

Есть хорошая статья на CiTForum из двух частей: первая часть – про создание, подпись  и т.д., во второй статье есть про работу с хранилищами сертификатов. Естественно MSDN,  также полезным оказался RFC5280 (описание x509 и списков отзыва). Значения констант, которые добавлял, нашел в проекте Wine, думаю wincrypt.h можно было найти и от создателя, но от вайна мне попался раньше. Еще много информации на форме КриптоПро, например про загрузку сертификата.

Поиск по CN.

Логика проста:

  1. Открыть хранилище
  2. Сравнить CN сертификатов с переданным CN
  3. Закрыть хранилище.

Открывается хранилище: CertOpenSystemStore(0,’ROOT’):HCERTSTORE; – возвращает дескриптор хранилища или nil в случае неудачи. Функция CertEnumCertificatesInStore(store:HCERTSTORE, prev:PCCERT_CONTEXT):PCCERT_CONTEXT; – возвращает в указатель на структуру сертификата из хранилища store, следующий за prev, если prev:=nil, то первый сертификат. Закрывается хранилище функцией CertCloseStore(store:HCERTSTORE, 0).

Данные о владельце сертификата хранятся в поле cert^.pCertInfo^.Subject, данные кто выдал сертификат в cert^.pCertInfo^.Issuer. Но данные там, хранятся не в текстовом виде, и есть специально обученная функция которая преобразует их в строку: CertNameToStr(dwCertEncodingType:DWORD; pName:PCERT_NAME_BLOB; dwStrType:DWORD; psz:LPTSTR; csz:DWORD):DWORD. psz – буфер под строку, csz – размер буфера, если psz = nil, то функция вернет только размер строки. Т.е. ее надо вызывать два раза, первый раз чтоб выяснить размер строки, создать буфер этого размера, а затем второй раз чтоб уже получить саму строку, например так:

function GetName(Enc:DWORD; blob:CERT_NAME_BLOB):string;
var size:integer=0;
    cn:pchar=nil;
begin
  size:=0;
  size:=CertNameToStr(Enc,@blob,CERT_X500_NAME_STR,nil,0); // CERT_OID_NAME_STR CERT_X500_NAME_STR
  if size>0 then begin
     cn:=StrAlloc(size);
     size:=CertNameToStr(Enc,@blob,CERT_X500_NAME_STR,cn,size);
     Result:=AnsiToUtf8(cn);
     StrDispose(cn);
     size:=pos('CN=',Result);
     delete(Result,1,size+2);
     size:=pos(',',Result);
     if size>0 then
       delete(Result,size,Length(Result));
  end;
end;

dwStrType – определяет в каком виде вернуть данные, может принимать следующие значения:

  • CERT_X500_NAME_STR – возвращает строку вида: “CN=Финуправление, OU=Администрация Аксайского района …”
  • CERT_OID_NAME_STR – тоже что и CERT_X500_NAME_STR, только CN, OU и другие заменены на OID: “1.1.432.23=Финуправление, 1.1.432.23=Администрация Аксайского района …”, в примере OID-ы взял “от балды”
  • CERT_SIMPLE_NAME_STR – возвращает только текст: “Финуправление, Администрация Аксайского района …”

Ну зная все это сделать поиск по CN – плевое дело:

var S:string;
    store:HCERTSTORE;
    cert:PCCERT_CONTEXT=nil;
    count:integer=0;
    strName:string;
{$R *.res}

begin
  if Paramcount<1 then begin
    writeln('Usage: findCert ');
    writeln(' is CN, full string in OEM charset');
    halt(2);
  end;
  S:=AnsiToUtf8(ParamStr(1));
  store:=CertOpenSystemStore(0,'ROOT');
  if store=nil then begin
    writeln('Error: open root store');
    halt(2);
  end;
  ExitCode:=1; // Not found
  try
    cert:=CertEnumCertificatesInStore(store, nil);
    while cert<>nil do begin
      inc(count);
      strName:=GetName(cert^.dwCertEncodingType,cert^.pCertInfo^.Subject);
      if (Length(strName)>0) and (SameText(strName,S)) then begin
        ExitCode:=0; // Found
        break;
      end;
      cert:=CertEnumCertificatesInStore(store, cert);
    end;
  finally
    if not CertCloseStore(store, 0) then begin
      writeln('Error: closing store');
      halt(2);
    end;
  end;
  //  Напечатаем результат поиска
  WriteLn(IntToStr(ExitCode));
end.

Скачать готовую программку можно в конце статьи

Зачем останавливаться на достигнутом

Вроде проблема решена, теперь можно в “логон” скрипте реализовать проверку сертификата, установлен или нет, и при необходимости установить с помощью cermgr.exe. Но как всегда ложка дегтя: у меня “логон” скрипт написан на wsh (читай visual basic), и в нем реализовать запуск программы с проверкой кода возврата или читать вывод программы и парсить потом на предмет результата работы – брррр… Тем более, что есть достаточно простая функция установки: CertAddCertificateContextToStore.

Читая документацию на MSDN об этой функции, наткнулся на очень полезное свойство, в моем случае, – при установке уже установленного сертификата возвращается ошибка $80092005! Т.е. поиск нам уже и не нужен получается, мы просто пытаемся установить сертификат и обрабатываем код ошибки в случае неудачи.

Вот как это выглядит:

function InstallCert(FileName:string):boolean;
var store:HCERTSTORE;
    cert:PCCERT_CONTEXT;
    n:pointer=nil;
begin
  Result:=false;
  ExitCode:=2;
  write(FileName);
  if not LoadCert(FileName, cert) then begin
    writeln(' - error load cert. [',GetLastError,'] ',UTF8ToConsole(SysToUTF8(SysErrorMessage(GetLastError))),#10' Only DER format.');
    exit;
  end else begin
    store:=CertOpenSystemStore(0,'ROOT');
    if store=nil then begin
      writeln(' - error open root store.');
      exit;
    end else begin
      if not CertAddCertificateContextToStore(store,cert,CERT_STORE_ADD_NEW,n) then begin
        if GetLastError = $80092005 then begin
          writeln(' - exist');
          Result:=true;
        end else begin
          writeln(' - error add to store [',GetLastError,'] ',UTF8ToConsole(SysToUTF8(SysErrorMessage(GetLastError))));
        end;
      end else begin
        Result:=true;
        writeln(' - installed');
      end;
      CertCloseStore(store, 0);
    end;
    CertFreeCertificateContext(cert);
  end;
end;

Общий алгоритм теперь укоротился и программа тоже.

Правда есть некоторые ограничения: сертификат ДОЛЖЕН быть в формате .DER, как ни пытался .PEM (BASE64) ни в какую не захотел загружаться. И второе не смог сделать silent режим, при установке всегда появляется окно-предупреждение с информацией о сертификате, где надо подтвердить установку. Похоже это ограничение системы, чтоб вирусы не ставили свои сертификаты :-)

Скачать

  • wcrypt2.zip — адаптированный модуль для работы с крипто библиотекой из jcl
  • findCert.zip – программа поиска сертификата в корневом хранилище
  • addCertInRoot.zip – установка сертификата в корневое хранилище

Комментариев нет

No comments yet.

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.