среда, 9 ноября 2011 г.

Проверка серверного сертификата SSL в Delphi

Понадобилось мне осуществить проверку серверного сертификата в моей программе на Delphi. Эта программа была написана довольно давно, на Delphi 7 с использованием Indy 9.

Вот как выглядит проверка сертификата:
var
  IdHTTP: TIdHTTP;
  IdSSLIOHandlerSocket: TIdSSLIOHandlerSocket;
begin
  IdHTTP := TIdHTTP.Create(nil);
  IdSSLIOHandlerSocket := TIdSSLIOHandlerSocket.Create(nil);
  IdHTTP.IOHandler := IdSSLIOHandlerSocket;

  with IdSSLIOHandlerSocket do
  begin
    SSLOptions.Method := sslvSSLv23;
    SSLOptions.Mode := sslmClient;
    SSLOptions.VerifyMode := [sslvrfPeer];
    SSLOptions.VerifyDepth := 10;
  end;

  IdHTTP.Get('https://www.google.com');

  FreeAndNil(IdHTTP); 
  FreeAndNil(IdSSLIOHandlerSocket);
end;

Если попробовать выполнить выше приведенный код, то результат будет всегда такой: Error connecting with SSL.

Проблема тут в том, что в Indy используется OpenSSL, который не умеет работать с хранилищем сертификатов Windows и не может проверить подписан ли сертификат, полученный от сервера, одним из доверенных корневых сертификатов из этого хранилища.

Посмотрим в исходник модуля IdSSLOpenSSL. В нем есть такая функция:
function VerifyCallback(Ok: Integer; ctx: PX509_STORE_CTX): Integer; cdecl;
var
  hcert: PX509;
  Certificate: TIdX509;
  hSSL: PSSL;
  IdSSLSocket: TIdSSLSocket;
 // str: String;
  VerifiedOK: Boolean;
  Depth: Integer;
 // Error: Integer;
begin
  LockVerifyCB.Enter;
  try
    VerifiedOK := True;
    try
      hcert := IdSslX509StoreCtxGetCurrentCert(ctx);
      hSSL := IdSslX509StoreCtxGetAppData(ctx);
      Certificate := TIdX509.Create(hcert);

      if hSSL <> nil then begin
        IdSSLSocket := TIdSSLSocket(IdSslGetAppData(hSSL));
      end
      else begin
        Result := Ok;
        exit;
      end;

      //Error :=
      IdSslX509StoreCtxGetError(ctx);
      //
      Depth := IdSslX509StoreCtxGetErrorDepth(ctx);
     // str := Format('Certificate: %s', [Certificate.Subject.OneLine]);    {Do not Localize}
     // str := IdSSLSocket.GetSessionIDAsString;
     // ShowMessage(str);

      if (IdSSLSocket.fParent is TIdSSLIOHandlerSocket) then begin
        VerifiedOK := TIdSSLIOHandlerSocket(IdSSLSocket.fParent).DoVerifyPeer(Certificate);
      end;

      if (IdSSLSocket.fParent is TIdServerIOHandlerSSL) then begin
        VerifiedOK := TIdServerIOHandlerSSL(IdSSLSocket.fParent).DoVerifyPeer(Certificate);
      end;

      if not ((Ok>0) and (IdSSLSocket.fSSLContext.VerifyDepth>=Depth)) then begin
        Ok := 0;
        {if Error = OPENSSL_X509_V_OK then begin
          Error := OPENSSL_X509_V_ERR_CERT_CHAIN_TOO_LONG;
        end;}
      end;
      FreeAndNil(Certificate);
    except
    end;
    if VerifiedOK and (Ok > 0) then begin
      Result := 1;
    end
    else begin
      Result := 0;
    end;

  //  Result := Ok; // testing
  finally
    LockVerifyCB.Leave;
  end;
end;

Эта callback-функция вызывается при проверке сертификата библиотекой OpenSSL, и для того, чтобы не случилось ошибки "Error connecting with SSL" она должна возвращать значение Result = 1. Видно, что VerifyCallback возвращает Result = 1 только в том случае, когда Ok > 0. А параметр Ok для последнего сертификата в цепочке проверяемых всегда будет = 0, т.к. корневой сертификат, с помощью которого его можно проверить, не определен.

Таким образом, задача в следующем: проверить - подписан ли последний в цепочке сертификат одним из доверенных корневых (ROOT) сертификатов в хранилище Windows, и если подписан, то присвоить Ok = 1.

К сожалению, при проектировании Indy 9 было допущено много ошибок, и для того, чтобы решить поставленную задачу, придется вносить изменения в код модуля IdSSLOpenSSL, а затем перекомпилировать пакет
Indy70.dpk.

Внесем следующие изменения:

1. Изменим определение события TVerifyPeerEvent на следующее:
TVerifyPeerEvent = function(Certificate: TIdX509; Error: Integer; var Ok: Integer): Boolean of object;

2. Изменим определение метода DoVerifyPeer в классах TIdSSLIOHandlerSocket и TIdServerIOHandlerSSL на следующее:
function DoVerifyPeer(Certificate: TIdX509; Error: Integer; var Ok: Integer): Boolean; virtual;

3. Изменим реализации метода DoVerifyPeer в классах TIdSSLIOHandlerSocket и TIdServerIOHandlerSSL на следующие:
function TIdServerIOHandlerSSL.DoVerifyPeer(Certificate: TIdX509;
  Error: Integer; var Ok: Integer): Boolean;
begin
  Result := True;
  if Assigned(fOnVerifyPeer) then
    Result := fOnVerifyPeer(Certificate, Error, Ok);
end;

function TIdSSLIOHandlerSocket.DoVerifyPeer(Certificate: TIdX509;
  Error: Integer; var Ok: Integer): Boolean;
begin
  Result := True;
  if Assigned(fOnVerifyPeer) then
    Result := fOnVerifyPeer(Certificate, Error, Ok);
end;

4. Добавим public-свойство X509 классу TIdX509:
property X509: PX509 read FX509;

5. Изменим функцию VerifyCallback на следующую:
function VerifyCallback(Ok: Integer; ctx: PX509_STORE_CTX): Integer; cdecl;
var
  hcert: PX509;
  Certificate: TIdX509;
  hSSL: PSSL;
  IdSSLSocket: TIdSSLSocket;
 // str: String;
  VerifiedOK: Boolean;
  Depth: Integer;
  Error: Integer;
begin
  LockVerifyCB.Enter;
  try
    VerifiedOK := True;
    try
      hcert := IdSslX509StoreCtxGetCurrentCert(ctx);
      hSSL := IdSslX509StoreCtxGetAppData(ctx);
      Certificate := TIdX509.Create(hcert);

      if hSSL <> nil then begin
        IdSSLSocket := TIdSSLSocket(IdSslGetAppData(hSSL));
      end
      else begin
        Result := Ok;
        exit;
      end;

      Error :=
      IdSslX509StoreCtxGetError(ctx);
      //
      Depth := IdSslX509StoreCtxGetErrorDepth(ctx);
     // str := Format('Certificate: %s', [Certificate.Subject.OneLine]);    {Do not Localize}
     // str := IdSSLSocket.GetSessionIDAsString;
     // ShowMessage(str);

      if (IdSSLSocket.fParent is TIdSSLIOHandlerSocket) then begin
        VerifiedOK := TIdSSLIOHandlerSocket(IdSSLSocket.fParent).DoVerifyPeer(Certificate, Error, Ok);
      end;

      if (IdSSLSocket.fParent is TIdServerIOHandlerSSL) then begin
        VerifiedOK := TIdServerIOHandlerSSL(IdSSLSocket.fParent).DoVerifyPeer(Certificate, Error, Ok);
      end;

      if not ((Ok>0) and (IdSSLSocket.fSSLContext.VerifyDepth>=Depth)) then begin
        Ok := 0;
        {if Error = OPENSSL_X509_V_OK then begin
          Error := OPENSSL_X509_V_ERR_CERT_CHAIN_TOO_LONG;
        end;}
      end;
      FreeAndNil(Certificate);
    except
    end;
    if VerifiedOK and (Ok > 0) then begin
      Result := 1;
    end
    else begin
      Result := 0;
    end;

  //  Result := Ok; // testing
  finally
    LockVerifyCB.Leave;
  end;
end;

Теперь нужно перекомпилировать Indy70.dpk.

В результате наших изменений обработчик события OnVerifyPeer получает два дополнительных параметра: Error - код ошибки проверки сертификата (это может быть, например, OPENSSL_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) и параметр Ok, который можно изменить. Ниже приведена реализация обработчика:
function TForm1.SSLIOHandlerVerifyPeer(ThePeerCert: TIdX509; Error: Integer; var Ok: Integer): Boolean;
var
  LBIO: PBIO;
  LLen: Integer;
  LStr: String;
  hStore: HCERTSTORE;
  pCert, pIssuer: PCCERT_CONTEXT;
  dwFlags: DWORD;
begin
  //Result := False;
  //
  if (Ok = 0)
  then begin
    hStore := nil;
    pCert := nil;
    pIssuer := nil;
    LBIO := BIO_new(BIO_s_mem());
    try
      i2d_X509_bio(LBIO, ThePeerCert.X509);
      LLen := BIO_ctrl_pending(LBIO);
      SetLength(LStr, LLen);
      BIO_read(LBIO, @LStr[1], LLen);
      BIO_free(LBIO);
      LBIO := nil;
      hStore := CertOpenSystemStore(0, 'ROOT');
      if hStore <> nil then
      begin
        pCert := CertCreateCertificateContext(
          PKCS_7_ASN_ENCODING or X509_ASN_ENCODING, @LStr[1], LLen);
        if pCert <> nil then
        begin
          dwFlags := CERT_STORE_REVOCATION_FLAG or
                     CERT_STORE_SIGNATURE_FLAG or
                     CERT_STORE_TIME_VALIDITY_FLAG;
          pIssuer := CertGetIssuerCertificateFromStore(
                       hStore, pCert, nil, @dwFlags);
          if pIssuer <> nil then
          begin
            Ok := 1;
            CertFreeCertificateContext(pIssuer);
            pIssuer := nil;
          end;
          CertFreeCertificateContext(pCert);
          pCert := nil;
        end;
        CertCloseStore(hStore, 0);
        hStore := nil;
      end;
    finally
      if LBIO <> nil then
        BIO_free(LBIO);
      if pIssuer <> nil then
        CertFreeCertificateContext(pIssuer);
      if pCert <> nil then
        CertFreeCertificateContext(pCert);
      if hStore <> nil then
        CertCloseStore(hStore, 0);
    end;
  end;
  //
  Result := (Ok > 0);
end;

В этом обработчике использованы функции OpenSSL и СryptoAPI, объявления которых можно найти в libeay32.pas и wcrypt2.pas. Собственно, из-за необходимости подключения этих модулей я и вынес проверку в обработчик события OnVerifyPeer, вместо того, чтобы сделать все в функции VerifyCallback.

Подключаем обработчик SSLIOHandlerVerifyPeer к IdSSLIOHandlerSocket и окончательно получаем загрузку страницы по протоколу https: с проверкой серверного сертификата:
var
  IdHTTP: TIdHTTP;
  IdSSLIOHandlerSocket: TIdSSLIOHandlerSocket;
begin
  IdHTTP := TIdHTTP.Create(nil);
  IdSSLIOHandlerSocket := TIdSSLIOHandlerSocket.Create(nil);
  IdHTTP.IOHandler := IdSSLIOHandlerSocket;

  with IdSSLIOHandlerSocket do
  begin
    SSLOptions.Method := sslvSSLv23;
    SSLOptions.Mode := sslmClient;
    SSLOptions.VerifyMode := [sslvrfPeer];
    SSLOptions.VerifyDepth := 10;
    OnVerifyPeer := SSLIOHandlerVerifyPeer;
  end;

  IdHTTP.Get('https://www.google.com');

  FreeAndNil(IdHTTP); 
  FreeAndNil(IdSSLIOHandlerSocket);
end;

PS. Проверка серверного сертификата и Enhanced Security

===
Перепечатка материалов блога разрешается с обязательной ссылкой на blog.coolsoftware.ru

2 комментария:

  1. Дело в том, что сейчас сложно найти libeay32.pas и wcrypt2.pas для Indy9. У вас их не осталось?

    ОтветитьУдалить
    Ответы
    1. Извините, только сейчас заметил Ваш комментарий.
      Осталось, но сейчас (уже давно на самом деле), есть смысл переходить на более новые версии Indy, которые работают с новыми версиями libeay32.dll и ssleay32.dll, в которых есть поддержка TLS 1.1, 1.2 и т.д.

      Удалить