Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to verify server hostname

I'm using Indy TIdHTTP (shipped with XE2) and the OpenSSL library DLLs V1.0.1m to verify a certificate when connecting over HTTPS. I have implemented an event handler for the OnVerifyPeer event of the TIdSSLIOHandlerSocketOpenSSL component.

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.

Now I have a problem to verify the hostname of the server certificate:

Although a wildcard is present in the Common Name (CN) field in the Subject field within the server certificate (*.google.com), the parameter Certificate.Subject.OneLine of the OnVerifyPeer event returns the CN without any wildcard (i.e. google.com instead of *.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. Can anyone confirm that the wildcard character is removed by Indy or the OpenSSL libraries, although it is necessary to verify the hostname?

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

Any help is greatly appreciated. Thanks for reading.

like image 770
Cheesy Avatar asked Sep 27 '22 20:09

Cheesy


2 Answers

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

No, OpenSSL does not remove it.

I don't know about the Indy library.


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

I'm citing this twice for a reason :) Placing server names in the Common Name (CN) is deprecated by both the IETF and CA/B Forums (what the browsers follow).

What you are probably experiencing is something like CN=example.com. In this case, example.com is not a server name; rather it is a domain. So you should not assume it means to match *.example.com.

And if a server answers at https://example.com, you should only accept the certificate if the Subject Alternate Name includes example.com because domains are listed in the CN by the public CAs. Public CAs place DNS names in the SAN because they follow the CA/B Forums.


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

OpenSSL prior to 1.1.0 did not perform hostname matching. The developer had to do it. OpenSSL 1.1.0 and above has the functionality built in. See X509_check_host(3) and friends.

To match a hostname, you should gather all the names from both the Common Name (CN) and the Subject Alternate Name (SAN). Then, its usually as simple as a Regular Expression matching.

The IETF is fast-and-loose, and they allow a hostname to show up in either the CN or the SAN. The CA/B Forum and Browsers are more strict: if a hostname is in the CN, then it must also be present in the SAN (yes, it must be listed twice). Otherwise, the CA/B Forum and Browsers expect all hostnames in the SAN.

I believe OpenSSL and the CA/B Forums only allow a wildcard in the leftmost label. I believe the IETF allows wildcards to show up anywhere.

If you want to see sample code, then check out cURL's implementation. cURL uses OpenSSL, but does not depend on 1.1.0's X509_check_host(3) and friends. cURL has its own implementation.


A quick warning. Hostname matching is a black art. For example....

The IETF allows matching to a Global Top Level Domain (gTLD) like *.com or *.net; and Country Top Level Domain (ccTLD) like *.uk or *.us. I consider this an attack because I know there is no single CA that can claim to "own" or "certify" a gTLD. If I experience one of those certs in the wild, then I reject it.

The CA/B Forums do not allow wildcarding gTLDs or ccTLDs. The browsers attempt to avoid it by using Public Suffix List (PSL). Things have only gotten worse with the vanity domains, like *.google.

There's another thing that browsers attempt to do with the PSL. They attempt to carve out administrative boundaries on subdomains. For example, Amazon owns all of amazon.com, but they delegate authority to subdomains, like example.amazon.com. So the PSL attempts to allow Amazon to control their domain amazon.com, but not your merchant related subdomain of example.amazon.com.

The IETF is attempting to tackle administrative boundaries in the DBOUND Working Group. But things appear to be stalled in committee.

like image 147
jww Avatar answered Oct 16 '22 19:10

jww


Unfortunately I have to stick to XE2-Indy and OpenSSL V1.0.1m due to internal specifications.

To verify the hostname against the Subject CN and Subject Alternate Names, I've done the following (using the approach cURL's implementation):

1. At application startup, I'm trying once to extend the access to methods within the Indy crypto library.

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. The follwing class helps me to access and verify the SAN and 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. In the OnVerifyPeer event of the TIdSSLIOHandlerSocketOpenSSL component, the class can be used as follows:

function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
  AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
  // (...)
    Result := TIdX509Access(Certificate).ValidateHostname(Certificate, IdHttp1.URL.Host) = hvrMatchFound;
  // (...)
end;
like image 33
Cheesy Avatar answered Oct 16 '22 17:10

Cheesy