很遗憾,由于内部规格要求,我必须坚持使用XE2-Indy和OpenSSL V1.0.1m。
为了验证主机名与Subject CN和Subject Alternate Names是否匹配,我已经采取了以下措施(使用
cURL实现的方法):
1. 在应用程序启动时,我尝试一次性扩展Indy加密库中的方法访问权限。
function ExtendIndyCryptoLibrary(): Boolean;
var
hIdCrypto: HMODULE;
begin
Result := False;
if not IdSSLOpenSSL.LoadOpenSSLLibrary() then
Exit;
hIdCrypto := IdSSLOpenSSLHeaders.GetCryptLibHandle();
if hIdCrypto = 0 then
Exit();
@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
function TIdX509Access.Hostmatch(Hostname, Pattern: String): Boolean;
begin
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;
pcrit := nil;
pidx := nil;
psan_names := X509_get_ext_d2i(FX509, NID_subject_alt_name, pcrit, pidx);
if psan_names <> nil then
begin
san_names_nb := sk_num(PSTACK(psan_names));
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
DnsName := String(pcurrent_name.d.dNSName.data);
if Hostmatch(Hostname, DnsName) then
begin
Result := hvrMatchFound;
Break;
end;
end;
end;
end
else
Result := hvrNoSANPresent;
sk_free(PSTACK(psan_names));
end;
function TIdX509Access.MatchesCN(Certificate: TIdX509;
Hostname: String): THostnameValidationResult;
var
TempList: TStringList;
Cn: String;
begin
Result := hvrMatchNotFound;
TempList := TStringList.Create();
TempList.Delimiter := '/';
TempList.DelimitedText := Certificate.Subject.OneLine;
Cn := Trim(TempList.Values['CN']);
FreeAndNil(TempList);
if Hostmatch(Hostname, Cn) then
Result := hvrMatchFound;
end;
function TIdX509Access.ValidateHostname(Certificate: TIdX509;
Hostname: String): THostnameValidationResult;
begin
Result := MatchesSAN(Hostname);
if Result = hvrNoSANPresent then
begin
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;
TIdX509
类只是在Indy的验证回调函数中包装了一个由OpenSSL提供的PX509
句柄。TIdX509
不会对证书数据进行任何处理,它会按原样呈现。Subject
属性包装了来自OpenSSL的X509_get_subject_name()
函数的PX509_NAME
句柄,而OneLine
属性返回OpenSSL的X509_NAME_oneline()
函数返回的任何值。因此,是OpenSSL本身剥离了通配符。 - Remy LebeauX509_check_host()
和certificate_host_name_override()
函数。您可以将原始的PX509
句柄(即TIdX509.FX509
成员 - 您需要使用访问器类才能到达它)和您连接到的主机名传递给它们。 - Remy Lebeautype TIdX509Access = class(TIdX509) end; var HostName: AnsiString; HostName := ...; if x509_check_host(TIdX509Access(Certificate).FX509, PAnsiChar(HostName), Length(HostName), 0, nil) = 1 then ...
- Remy Lebeau