validar solo regular numeros nombre letras expresion cadenas java ssl jsse

java - solo - regex nombre



¿Cómo debo hacer la validación del nombre de host cuando uso JSSE? (2)

Estoy escribiendo un cliente en Java (necesita trabajar tanto en el JRE de escritorio como en Android) para un protocolo propietario (específico de mi empresa) transferido a través de TLS. Estoy intentando descubrir la mejor manera de escribir un cliente TLS en Java y, en particular, asegurarme de que la validación del nombre de host sea correcta. ( Editar: me refiero a verificar que el nombre de host coincida con el certificado X.509, para evitar los ataques de hombre en el medio).

JSSE es la API obvia para escribir un cliente de TLS, pero noté en el documento " El código más peligroso del mundo " (así como a partir de la experimentación) que JSSE no valida el nombre de host cuando se usa la API SSLSocketFactory. (Que es lo que tengo que usar, ya que mi protocolo no es HTTPS).

Entonces, parece que al usar JSSE, tengo que hacer la validación del nombre de host yo mismo. Y, en lugar de escribir ese código desde cero (ya que casi con seguridad lo entendería mal), parece que debería "tomar prestado" un código existente que funciona. Por lo tanto, el candidato más probable que he encontrado es utilizar la biblioteca Apache HttpComponents (irónico, ya que en realidad no estoy haciendo HTTP) y usar la clase org.apache.http.conn.ssl.SSLSocketFactory en lugar del javax estándar Clase .net.ssl.SSLSocketFactory .

Mi pregunta es: ¿es esto un curso de acción razonable? ¿O he entendido mal las cosas, me he salido de las profundidades, y de hecho hay una forma mucho más fácil de obtener la validación del nombre de host en JSSE, sin tener que recurrir a una biblioteca de terceros como HttpComponents?

También miré BouncyCastle, que tiene una API que no es JSSE para TLS, pero parece ser aún más limitada, ya que ni siquiera valida la cadena de certificados, y mucho menos la validación del nombre de host, por lo que parecía no -motor de arranque.

Editar: esta pregunta ha sido respondida para Java 7, pero todavía tengo curiosidad por saber cuál es la "mejor práctica" para Java 6 y Android. (En particular, tengo que dar soporte a Android para mi aplicación).

Editado de nuevo: para hacer que mi propuesta de "tomar prestado de HttpComponents de Apache" sea más concreta, he creado una pequeña biblioteca que contiene las implementaciones de HostnameVerifier (en particular, StrictHostnameVerifier y BrowserCompatHostnameVerifier) ​​extraídas de Apache HttpComponents. (Me di cuenta de que todo lo que necesito son los verificadores, y no necesito el SSLSocketFactory de Apache como pensaba originalmente). Si me dejan en mis propios dispositivos, esta es la solución que usaré. Pero primero, ¿hay alguna razón por la que no debería hacerlo de esta manera? (Suponiendo que mi objetivo es hacer mi validación de nombre de host de la misma manera que https. Me doy cuenta de que está abierto al debate, y se ha discutido en el hilo de la lista de criptografía, pero por ahora me quedo con un nombre de host similar a HTTPS validación, aunque no estoy haciendo HTTPS.)

Asumiendo que no hay nada "incorrecto" en mi solución, mi pregunta es esta: ¿hay una "mejor" manera de hacerlo, mientras aún permanece portátil en Java 6, Java 7 y Android? (Donde "mejor" significa más idiomático, ya ampliamente utilizado y / o que necesita menos código externo).


Java 7 (y superior)

Puede utilizar implícitamente el X509ExtendedTrustManager introducido en Java 7 usando esto (vea esta respuesta :

SSLParameters sslParams = new SSLParameters(); sslParams.setEndpointIdentificationAlgorithm("HTTPS"); sslSocket.setSSLParameters(sslParams); // also works on SSLEngine

Androide

Estoy menos familiarizado con Android, pero Apache HTTP Client debe incluirse con él, por lo que no es realmente una biblioteca adicional. Como tal, debería poder usar org.apache.http.conn.ssl.StrictHostnameVerifier . (No he probado este código)

SSLSocketFactory ssf = (SSLSocketFactory) SSLSocketFactory.getDefault(); // It''s important NOT to resolve the IP address first, but to use the intended name. SSLSocket socket = (SSLSocket) ssf.createSocket("my.host.name", 443); socket.startHandshake(); SSLSession session = socket.getSession(); StrictHostnameVerifier verifier = new StrictHostnameVerifier(); if (!verifier.verify(session.getPeerHost(), session)) { // throw some exception or do something similar. }

Otro

Desafortunadamente, el verificador debe implementarse manualmente. El Oracle JRE obviamente tiene alguna implementación de verificador de nombre de host, pero hasta donde sé, no está disponible a través de la API pública.

Hay más detalles sobre las reglas en esta respuesta reciente .

Aquí hay una implementación que he escrito. Sin duda podría hacer que se lo revise ... Comentarios y comentarios bienvenidos.

public void verifyHostname(SSLSession sslSession) throws SSLPeerUnverifiedException { try { String hostname = sslSession.getPeerHost(); X509Certificate serverCertificate = (X509Certificate) sslSession .getPeerCertificates()[0]; Collection<List<?>> subjectAltNames = serverCertificate .getSubjectAlternativeNames(); if (isIpv4Address(hostname)) { /* * IP addresses are not handled as part of RFC 6125. We use the * RFC 2818 (Section 3.1) behaviour: we try to find it in an IP * address Subject Alt. Name. */ for (List<?> sanItem : subjectAltNames) { /* * Each item in the SAN collection is a 2-element list. See * <a href= * "http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames%28%29" * >X509Certificate.getSubjectAlternativeNames()</a>. The * first element in each list is a number indicating the * type of entry. Type 7 is for IP addresses. */ if ((sanItem.size() == 2) && ((Integer) sanItem.get(0) == 7) && (hostname.equalsIgnoreCase((String) sanItem .get(1)))) { return; } } throw new SSLPeerUnverifiedException( "No IP address in the certificate did not match the requested host name."); } else { boolean anyDnsSan = false; for (List<?> sanItem : subjectAltNames) { /* * Each item in the SAN collection is a 2-element list. See * <a href= * "http://docs.oracle.com/javase/7/docs/api/java/security/cert/X509Certificate.html#getSubjectAlternativeNames%28%29" * >X509Certificate.getSubjectAlternativeNames()</a>. The * first element in each list is a number indicating the * type of entry. Type 2 is for DNS names. */ if ((sanItem.size() == 2) && ((Integer) sanItem.get(0) == 2)) { anyDnsSan = true; if (matchHostname(hostname, (String) sanItem.get(1))) { return; } } } /* * If there were not any DNS Subject Alternative Name entries, * we fall back on the Common Name in the Subject DN. */ if (!anyDnsSan) { String commonName = getCommonName(serverCertificate); if (commonName != null && matchHostname(hostname, commonName)) { return; } } throw new SSLPeerUnverifiedException( "No host name in the certificate did not match the requested host name."); } } catch (CertificateParsingException e) { /* * It''s quite likely this exception would have been thrown in the * trust manager before this point anyway. */ throw new SSLPeerUnverifiedException( "Unable to parse the remote certificate to verify its host name: " + e.getMessage()); } } public boolean isIpv4Address(String hostname) { String[] ipSections = hostname.split("//."); if (ipSections.length != 4) { return false; } for (String ipSection : ipSections) { try { int num = Integer.parseInt(ipSection); if (num < 0 || num > 255) { return false; } } catch (NumberFormatException e) { return false; } } return true; } public boolean matchHostname(String hostname, String certificateName) { if (hostname.equalsIgnoreCase(certificateName)) { return true; } /* * Looking for wildcards, only on the left-most label. */ String[] certificateNameLabels = certificateName.split("."); String[] hostnameLabels = certificateName.split("."); if (certificateNameLabels.length != hostnameLabels.length) { return false; } /* * TODO: It could also be useful to check whether there is a minimum * number of labels in the name, to protect against CAs that would issue * wildcard certificates too loosely (e.g. *.com). */ /* * We check that whatever is not in the first label matches exactly. */ for (int i = 1; i < certificateNameLabels.length; i++) { if (!hostnameLabels[i].equalsIgnoreCase(certificateNameLabels[i])) { return false; } } /* * We allow for a wildcard in the first label. */ if ("*".equals(certificateNameLabels[0])) { // TODO match wildcard that are only part of the label. return true; } return false; } public String getCommonName(X509Certificate cert) { try { LdapName ldapName = new LdapName(cert.getSubjectX500Principal() .getName()); /* * Looking for the "most specific CN" (i.e. the last). */ String cn = null; for (Rdn rdn : ldapName.getRdns()) { if ("CN".equalsIgnoreCase(rdn.getType())) { cn = rdn.getValue().toString(); } } return cn; } catch (InvalidNameException e) { return null; } } /* BouncyCastle implementation, should work with Android. */ public String getCommonName(X509Certificate cert) { String cn = null; X500Name x500name = X500Name.getInstance(cert.getSubjectX500Principal() .getEncoded()); for (RDN rdn : x500name.getRDNs(BCStyle.CN)) { // We''ll assume there''s only one AVA in this RDN. cn = IETFUtils.valueToString(rdn.getFirst().getValue()); } return cn; }

Hay dos implementaciones de getCommonName : una que usa javax.naming.ldap y otra que usa BouncyCastle, dependiendo de lo que esté disponible.

Las sutilezas principales son sobre:

  • Coincidencia de la dirección IP solo en SAN ( Esta pregunta trata acerca de la coincidencia de direcciones IP y los nombres alternativos del sujeto). Quizás se podría hacer algo acerca de la coincidencia de IPv6 también.
  • Coincidencia comodín.
  • Solo volver a caer en el CN ​​si no hay DNS SAN.
  • Qué significa realmente el CN ​​"más específico". Supuse que este es el último aquí. (Ni siquiera estoy considerando un solo CN RDN con múltiples Aserciones de Valor-Atributo (AVA): BouncyCastle puede tratar con ellos, pero de todos modos este es un caso extremadamente raro hasta donde yo sé).
  • No he comprobado en absoluto lo que debería ocurrir para los nombres de dominio internacionalizados (no ASCII) (ver RFC 6125 ).

EDITAR :

Para hacer que mi propuesta de "tomar prestado de HttpComponents de Apache" sea más concreta, he creado una pequeña biblioteca que contiene las implementaciones de HostnameVerifier (en particular, StrictHostnameVerifier y BrowserCompatHostnameVerifier) ​​extraídas de Apache HttpComponents. [...] Pero primero, ¿hay alguna razón por la que no debería hacerlo de esta manera?

Sí, hay razones para no hacerlo de esta manera.

En primer lugar, ha bifurcado una biblioteca de forma efectiva, y ahora deberá mantenerla, en función de los cambios posteriores realizados en estas clases en los componentes HttpComponents originales de Apache. No estoy en contra de crear una biblioteca (lo he hecho yo mismo, y no te estoy desalentando para que lo hagas), pero debes tener esto en cuenta. ¿Estás realmente tratando de ahorrar algo de espacio? Sin duda, hay herramientas que pueden eliminar el código no utilizado para su producto final si necesita recuperar espacio (se le ocurre a ProGuard).

En segundo lugar, incluso el StrictHostnameVerifier no es compatible con RFC 2818 o RFC 6125. Por lo que puedo decir de su código:

  • Aceptará las direcciones IP en el CN, cuando no debería.
  • No solo recurrirá al CN cuando no haya SAN DNS, sino que también tratará al CN como primera opción. Esto podría conducir a un cert con CN=cn.example.com y una SAN para www.example.com pero ninguna SAN para cn.example.com será válida para cn.example.com cuando no debería.
  • Soy un poco escéptico sobre la forma en que se extrae el CN. La serialización de serie de DN de sujeto puede ser un poco divertida, especialmente si algunos RDN incluyen comas, y el caso incómodo en que algunos RDN pueden tener múltiples AVA.

Es difícil ver una "mejor manera" general. Dar estos comentarios a la biblioteca de Apache HttpComponents sería una forma de hacerlo. Copiar y pegar el código que escribí anteriormente tampoco parece una buena manera (los fragmentos de código en SO generalmente no se mantienen, no se prueban al 100% y pueden ser propensos a errores).

Una mejor manera podría ser tratar de convencer al equipo de desarrollo de Android para que admita los mismos SSLParameters y X509ExtendedTrustManager que se hizo para Java 7. Esto aún deja el problema de los dispositivos heredados.


Hay muchas buenas razones para requerir que el cliente jsse (usted) proporcione su propio StrictHostnameVerifier. Si confías en el servidor de nombres de tu compañía, escribir uno debería ser bastante sencillo.

  1. obtenga la IP para el nombre de host del proveedor de DNS que su host está configurado para usar.
  2. devuelve el resultado de hacer coincidir los nombres.
  3. opcionalmente, verifique que la búsqueda inversa para el IP devuelve el nombre correcto.

si lo necesita, le proporcionaré un verificador. Si quieres que proporcione las "buenas razones", también puedo hacerlo.