如何验证服务器主机名

How to verify server hostname

我正在使用 Indy TIdHTTP(XE2 附带)和 OpenSSL 库 DLL V1.0.1m 在通过 HTTPS 连接时验证证书。我已经为 TIdSSLIOHandlerSocketOpenSSL 组件的 OnVerifyPeer 事件实现了一个事件处理程序。

function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
  AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
  (...)
end;

According to RFC 2818, chapter 3.1., if the hostname is available to the client, the client MUST check it against the server's identity as presented in the server's Certificate message, in order to prevent man-in-the-middle attacks.

现在我在验证服务器证书的主机名时遇到了问题:

尽管通用名称 (CN) 中存在通配符 服务器证书中主题字段中的字段 (*.google.com), OnVerifyPeer 事件的参数 Certificate.Subject.OneLine returns 没有任何通配符的 CN(即 google.com 而不是 *.google.com)。

As stated in RFC 2818, chapter 3.1. the wildcard character * is used to match any single domain name component or component fragment.

  1. 谁能确认 Indy 或 OpenSSL 库删除了通配符,尽管有必要验证主机名?

  2. 有没有人知道在这种情况下验证主机名?

非常感谢任何帮助。感谢阅读。

Can anyone confirm that the wildcard character is removed by Indy or the OpenSSL libraries, although it is necessary to verify the hostname?

不,OpenSSL 不会删除它。

我不知道 Indy 库。


Can anyone confirm that the wildcard character is removed by Indy or the OpenSSL libraries, although it is necessary to verify the hostname?

我两次引用它是有原因的 :) IETF 和 CA/B 论坛都反对将服务器名称放在 通用名称 (CN) 中(浏览器遵循什么)。

您可能遇到的情况类似于 CN=example.com。在这种情况下,example.com 不是 服务器名称;相反,它 一个域。所以你不应该假设它意味着匹配*.example.com.

并且如果服务器在 https://example.com 应答,您应该仅在 主题备用名称 包含 example.com 时才接受证书,因为域列在CN 由 public 个 CA。 Public CA 将 DNS 名称放在 SAN 中,因为它们遵循 CA/B 论坛。


Has anyone an idea to verify the hostname under these circumstances?

1.1.0 之前的 OpenSSL 执行主机名匹配。开发人员必须这样做。 OpenSSL 1.1.0 及更高版本具有内置功能。请参阅 X509_check_host(3) 和朋友。

要匹配主机名,您应该从 Common Name (CN) 主题别名 (SAN)。然后,它通常就像正则表达式匹配一样简单。

IETF 是快速而松散的,它们允许主机名出现在 CN 或 SAN 中。 CA/B 论坛和浏览器更严格:如果主机名在 CN 中,那么它也必须存在于 SAN 中(是的,它必须列出两次)。否则,CA/B 论坛和浏览器需要 SAN 中的所有主机名。

我相信 OpenSSL 和 CA/B 论坛只允许在最左边的标签中使用通配符。我相信 IETF 允许通配符出现在任何地方。

如果您想查看示例代码,请查看 cURL 的实现。 cURL 使用 OpenSSL,但不依赖于 1.1.0 的 X509_check_host(3) 和朋友。 cURL 有自己的实现。


快速警告。主机名匹配是一种魔法。例如....

IETF 允许匹配到全球顶级域 (gTLD),例如 *.com*.net;和国家顶级域名 (ccTLD),例如 *.uk*.us。我认为这是一种攻击,因为我知道没有任何一个 CA 可以声称 "own" 或 "certify" 一个 gTLD。如果我在野外遇到其中一个证书,那么我会拒绝它。

CA/B 论坛不允许使用通配符的 gTLD 或 ccTLD。浏览器试图通过使用 Public Suffix List (PSL) 来避免它。虚荣域的情况只会变得更糟,例如 *.google.

浏览器试图用 PSL 做另一件事。他们试图在子域上划分行政边界。例如,Amazon 拥有所有 amazon.com,但他们将权限委托给子域,例如 example.amazon.com。因此,PSL 试图让亚马逊控制他们的域 amazon.com,而不是您的商家相关子域 example.amazon.com

IETF 正试图解决 DBOUND Working Group 中的行政边界问题。但事情似乎在委员会中停滞不前。

不幸的是,由于内部规范,我不得不坚持使用 XE2-Indy 和 OpenSSL V1.0.1m。

为了根据主题 CN 和主题备用名称验证主机名,我已完成以下操作(使用方法 cURL's implementation):

1.在应用程序启动时,我尝试一次扩展对 Indy 加密库中方法的访问。

function ExtendIndyCryptoLibrary(): Boolean;
var
  hIdCrypto: HMODULE;
begin
  Result := False;

  // Try to get handle to Indy used crypto library
  if not IdSSLOpenSSL.LoadOpenSSLLibrary() then
    Exit;
  hIdCrypto := IdSSLOpenSSLHeaders.GetCryptLibHandle();
  if hIdCrypto = 0 then
    Exit();

  // Try to get exported methods that are needed additionally
  @X509_get_ext_d2i := GetProcAddress(hIdCrypto, 'X509_get_ext_d2i');

  Result := Assigned(X509_get_ext_d2i);
end;

2。以下class帮助我访问和验证SAN和CN。

type
  THostnameValidationResult = (hvrMatchNotFound, hvrNoSANPresent, hvrMatchFound);
var
  X509_get_ext_d2i: function(a: PX509; nid: TIdC_INT; var pcrit: PIdC_INT; var pidx: PIdC_INT): PSTACK_OF_GENERAL_NAME; cdecl = nil;
type
  TIdX509Access = class(TIdX509)
  protected
    function Hostmatch(Hostname, Pattern: String): Boolean;
    function MatchesSAN(Hostname: String): THostnameValidationResult;
    function MatchesCN(Certificate: TIdX509; Hostname: String): THostnameValidationResult;
  public
    function ValidateHostname(Certificate: TIdX509; Hostname: String): THostnameValidationResult;
  end;

implementation

{ TIdX509Access }

function TIdX509Access.Hostmatch(Hostname, Pattern: String): Boolean;
begin
  // Match hostname against pattern using RFC, CA/Browser Forum, ...
  // (...)
end;

function TIdX509Access.MatchesSAN(Hostname: String): THostnameValidationResult;
var
  pcrit, pidx: PIdC_INT;
  psan_names: PSTACK_OF_GENERAL_NAME;
  san_names_nb: Integer;
  pcurrent_name: PGENERAL_NAME;
  i: Integer;
  DnsName: String;
begin
  Result := hvrMatchNotFound;

  // Try to extract the names within the SAN extension from the certificate
  pcrit := nil;
  pidx := nil;
  psan_names := X509_get_ext_d2i(FX509, NID_subject_alt_name, pcrit, pidx);
  // Check if SAN is present
  if psan_names <> nil then
  begin
    san_names_nb := sk_num(PSTACK(psan_names));
    // Check each name within the extension
    for i := 0 to san_names_nb-1 do
    begin
      pcurrent_name := PGENERAL_NAME( sk_value(PSTACK(psan_names), i) );
      if pcurrent_name._type = GEN_DNS then
      begin
        // Current name is a DNS name, let's check it
        DnsName := String(pcurrent_name.d.dNSName.data);
        // Compare expected hostname with the DNS name
        if Hostmatch(Hostname, DnsName) then
        begin
          Result := hvrMatchFound;
          Break;
        end;
      end;
    end;
  end
  else
    Result := hvrNoSANPresent;
  // Clean up
  sk_free(PSTACK(psan_names));
end;

function TIdX509Access.MatchesCN(Certificate: TIdX509;
  Hostname: String): THostnameValidationResult;
var
  TempList: TStringList;
  Cn: String;
begin
  Result := hvrMatchNotFound;

  // Extract CN from Subject
  TempList := TStringList.Create();
  TempList.Delimiter := '/';
  TempList.DelimitedText := Certificate.Subject.OneLine;
  Cn := Trim(TempList.Values['CN']);
  FreeAndNil(TempList);

  // Compare expected hostname with the CN
  if Hostmatch(Hostname, Cn) then
    Result := hvrMatchFound;
end;

function TIdX509Access.ValidateHostname(Certificate: TIdX509;
  Hostname: String): THostnameValidationResult;
begin
  // First try the Subject Alternative Names extension
  Result := MatchesSAN(Hostname);
  if Result = hvrNoSANPresent then
  begin
    // Extension was not found: try the Common Name
    Result := MatchesCN(Certificate, Hostname);
  end;
end;

3。在TIdSSLIOHandlerSocketOpenSSL组件的OnVerifyPeer事件中,class可以这样使用:

function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
  AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
  // (...)
    Result := TIdX509Access(Certificate).ValidateHostname(Certificate, IdHttp1.URL.Host) = hvrMatchFound;
  // (...)
end;