delphi - Cómo verificar el nombre de host del servidor
ssl openssl (2)
Desafortunadamente, tengo que apegarme a XE2-Indy y OpenSSL V1.0.1m debido a las especificaciones internas.
Para verificar el nombre de host contra el CN sujeto y los nombres alternativos del sujeto, hice lo siguiente (usando el enfoque de la implementación de cURL ):
1. Al inicio de la aplicación, intento una vez extender el acceso a los métodos dentro de la biblioteca crypto de 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. La siguiente clase me ayuda a acceder y verificar la SAN y 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. En el evento OnVerifyPeer del componente TIdSSLIOHandlerSocketOpenSSL, la clase se puede usar de la siguiente manera:
function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
// (...)
Result := TIdX509Access(Certificate).ValidateHostname(Certificate, IdHttp1.URL.Host) = hvrMatchFound;
// (...)
end;
Estoy usando Indy TIdHTTP
(incluido con XE2) y las DLL de la biblioteca OpenSSL V1.0.1m para verificar un certificado cuando me conecto a través de HTTPS. Implementé un controlador de eventos para el evento TIdSSLIOHandlerSocketOpenSSL
componente TIdSSLIOHandlerSocketOpenSSL
.
function TForm1.IdSSLIOHandlerSocketOpenSSL1VerifyPeer(Certificate: TIdX509;
AOk: Boolean; ADepth, AError: Integer): Boolean;
begin
(...)
end;
De acuerdo con RFC 2818, capítulo 3.1., Si el nombre de host está disponible para el cliente, el cliente DEBE verificarlo con la identidad del servidor tal como se presenta en el mensaje de Certificado del servidor, para evitar ataques de hombre en el medio.
Ahora tengo un problema para verificar el nombre de host del certificado del servidor:
Aunque un comodín está presente en el campo Nombre común (CN) en el campo Asunto dentro del certificado del servidor (* .google.com), el parámetro Certificate.Subject.OneLine
del evento OnVerifyPeer
devuelve el CN sin ningún comodín (es decir, google. com en lugar de * .google.com).
Como se indica en RFC 2818, capítulo 3.1. el carácter comodín * se usa para unir cualquier componente de nombre de dominio o fragmento de componente.
¿Alguien puede confirmar que Indy o las bibliotecas OpenSSL eliminan el carácter comodín, aunque es necesario verificar el nombre de host?
¿Alguien tiene una idea para verificar el nombre de host en estas circunstancias?
Cualquier ayuda es muy apreciada. Gracias por leer.
¿Alguien puede confirmar que Indy o las bibliotecas OpenSSL eliminan el carácter comodín, aunque es necesario verificar el nombre de host?
No, OpenSSL no lo elimina.
No sé sobre la biblioteca de Indy.
¿Alguien puede confirmar que Indy o las bibliotecas OpenSSL eliminan el carácter comodín, aunque es necesario verificar el nombre de host?
Estoy citando esto dos veces por una razón :) Poner nombres de servidor en el Nombre común (CN) está en desuso tanto en el IETF y CA / B Forums (lo que siguen los navegadores).
Lo que probablemente experimentes es algo así como CN=example.com
. En este caso, example.com
no es un nombre de servidor; más bien es un dominio. Por lo tanto, no debe asumir que significa hacer coincidir *.example.com
.
Y si un servidor responde en https://example.com
, solo debe aceptar el certificado si el Nombre alternativo del sujeto incluye example.com
porque las CA públicas enumeran los dominios en el CN. Las CA públicas colocan nombres DNS en la SAN porque siguen los foros CA / B.
¿Alguien tiene una idea para verificar el nombre de host en estas circunstancias?
OpenSSL anterior a 1.1.0 no realizó la coincidencia de nombre de host. El desarrollador tuvo que hacerlo. OpenSSL 1.1.0 y superior tiene la funcionalidad integrada. Consulte X509_check_host(3)
y amigos.
Para hacer coincidir un nombre de host, debe reunir todos los nombres tanto del Nombre común (CN) como del Nombre alternativo del sujeto (SAN) . Entonces, es generalmente tan simple como una coincidencia de expresiones regulares.
El IETF es rápido y flexible, y permiten que un nombre de host aparezca en el CN o SAN. El CA / B Forum y los navegadores son más estrictos: si un nombre de host está en el CN, entonces también debe estar presente en la SAN (sí, debe aparecer dos veces). De lo contrario, el foro de CA / B y los navegadores esperan todos los nombres de host en la SAN.
Creo que OpenSSL y los foros de CA / B solo permiten un comodín en la etiqueta situada más a la izquierda. Creo que el IETF permite que los comodines se muestren en cualquier lugar.
Si desea ver el código de muestra, entonces consulte la implementación de cURL. cURL usa OpenSSL, pero no depende de X509_check_host(3)
''s X509_check_host(3)
y amigos. cURL tiene su propia implementación.
Una advertencia rápida. La coincidencia de nombre de host es un arte negro. Por ejemplo....
El IETF permite la coincidencia con un dominio de nivel superior global (gTLD) como *.com
o *.net
; y dominio de nivel superior nacional (ccTLD) como *.uk
o *.us
. Considero que esto es un ataque porque sé que no existe una CA única que pueda afirmar que es "propietario" o "certifica" un gTLD. Si experimento uno de esos certs en la naturaleza, entonces lo rechazo.
Los Foros CA / B no permiten gTLD o ccTLD de comodines. Los navegadores intentan evitarlo mediante el uso de Public Suffix List (PSL). Las cosas solo han empeorado con los dominios de tocador, como *.google
.
Hay otra cosa que los navegadores intentan hacer con el PSL. Intentan delimitar los límites administrativos en los subdominios. Por ejemplo, Amazon posee todo amazon.com, pero delegan autoridad a los subdominios, como example.amazon.com. Por lo tanto, la PSL intenta permitir que Amazon controle su dominio amazon.com
, pero no su subdominio relacionado con el comerciante de example.amazon.com
.
El IETF está tratando de abordar los límites administrativos en el Grupo de Trabajo DBOUND . Pero las cosas parecen estar estancadas en el comité.