我正在使用Indy TIdHTTP
(随XE2提供)和OpenSSL库DLL V1.0.1m在通过HTTPS连接时验证证书。我已为OnVerifyPeer
组件的TIdSSLIOHandlerSocketOpenSSL
事件实现了事件处理程序。
function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
(...)
end;
根据RFC 2818,第3.1章,如果主机名可用 客户端,客户端必须根据服务器的身份进行检查 在服务器的证书消息中显示,以防止 中间人攻击。
现在我有一个问题需要验证服务器证书的主机名:
虽然通用名称(CN)中存在通配符
服务器证书(* .google.com)中“主题”字段中的字段,
Certificate.Subject.OneLine
事件的参数OnVerifyPeer
返回没有任何通配符的CN(即google.com而不是* .google.com)。
如RFC 2818第3.1章所述。使用通配符* 匹配任何单个域名组件或组件片段。
任何人都可以确认Indy或OpenSSL库删除了通配符,但是有必要验证主机名吗?
有没有人想在这些情况下验证主机名?
非常感谢任何帮助。谢谢你的阅读。
答案 0 :(得分:3)
任何人都可以确认Indy或OpenSSL库删除了通配符,虽然有必要验证主机名吗?
不,OpenSSL不会将其删除。
我不知道Indy图书馆。
任何人都可以确认Indy或OpenSSL库删除了通配符,虽然有必要验证主机名吗?
我之所以引用这两次是有原因的:IETF和CA / B论坛(浏览器遵循的内容)不推荐在公共名称(CN)中放置服务器名称。 / p>
您可能遇到的问题类似于CN=example.com
。在这种情况下,example.com
不是 服务器名称;而 是 一个域名。因此 不应该 认为它意味着匹配*.example.com
。
如果服务器以https://example.com
回答,则只应在主题备用名称包含example.com
时接受证书,因为公共CA在CN中列出了域名。公共CA将DNS名称放在SAN中,因为它们遵循CA / B论坛。
有没有人想在这些情况下验证主机名?
1.1.0之前的OpenSSL 不执行主机名匹配。开发人员必须这样做。 OpenSSL 1.1.0及更高版本具有内置功能。请参阅X509_check_host(3)
和朋友。
要匹配主机名,您应该收集 两者 公用名(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允许匹配*.com
或*.net
等全球顶级域名(gTLD);和国家/地区顶级域名(ccTLD),如*.uk
或*.us
。我认为这是一次攻击,因为我知道没有一个CA可以声称“拥有”或“认证”gTLD。如果我在野外体验其中一种证书,那么我拒绝它。
CA / B论坛不允许通配gTLD或ccTLD。浏览器尝试使用Public Suffix List(PSL)来避免它。虚荣域只会变得更糟,例如*.google
。
浏览器尝试使用PSL还有另外一件事。他们试图在子域上划分出行政边界。例如,亚马逊拥有所有amazon.com,但他们将权限委托给子域名,例如example.amazon.com。因此,PSL尝试允许亚马逊控制其域amazon.com
,但不允许您控制example.amazon.com
的商家相关子域。
IETF正试图解决DBOUND Working Group中的行政边界问题。但事情似乎在委员会中停滞不前。
答案 1 :(得分:1)
不幸的是,由于内部规格,我必须坚持使用XE2-Indy和OpenSSL V1.0.1m。
要根据Subject CN和Subject Alternate Names验证主机名,我已完成以下操作(使用方法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。以下课程帮助我访问和验证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事件中,可以按如下方式使用该类:
function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
// (...)
Result := TIdX509Access(Certificate).ValidateHostname(Certificate, IdHttp1.URL.Host) = hvrMatchFound;
// (...)
end;