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.
Can anyone confirm that the wildcard character is removed by Indy or the OpenSSL libraries, although it is necessary to verify the hostname?
Has anyone an idea to verify the hostname under these circumstances?
Any help is greatly appreciated. Thanks for reading.
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.
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;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With