Alerta de protocolo de enlace SSL: error de nombre no reconocido desde la actualización a Java 1.7.0

225

Actualicé de Java 1.6 a Java 1.7 hoy. Desde entonces, se produce un error cuando intento establecer una conexión con mi servidor web a través de SSL:

javax.net.ssl.SSLProtocolException: handshake alert:  unrecognized_name
    at sun.security.ssl.ClientHandshaker.handshakeAlert(ClientHandshaker.java:1288)
    at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:1904)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1027)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1262)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1289)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1273)
    at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:523)
    at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1296)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
    at java.net.URL.openStream(URL.java:1035)

Aquí está el código:

SAXBuilder builder = new SAXBuilder();
Document document = null;

try {
    url = new URL(https://some url);
    document = (Document) builder.build(url.openStream());
} catch (NoSuchAlgorithmException ex) {
    Logger.getLogger(DownloadLoadiciousComputer.class.getName()).log(Level.SEVERE, null, ex);  
}

Es solo un proyecto de prueba, por eso permito y uso certificados no confiables con el código:

TrustManager[] trustAllCerts = new TrustManager[]{
    new X509TrustManager() {

        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public void checkClientTrusted(
                java.security.cert.X509Certificate[] certs, String authType) {
        }

        public void checkServerTrusted(
                java.security.cert.X509Certificate[] certs, String authType) {
        }
    }
};

try {

    SSLContext sc = SSLContext.getInstance("SSL");
    sc.init(null, trustAllCerts, new java.security.SecureRandom());
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {

    Logger.getLogger(DownloadManager.class.getName()).log(Level.SEVERE, null, e);
} 

Intenté conectarme con éxito https://google.com . donde es mi culpa

Gracias.

pvomhoff
fuente

Respuestas:

304

Java 7 introdujo el soporte SNI que está habilitado por defecto. Descubrí que ciertos servidores mal configurados envían una advertencia de "Nombre no reconocido" en el protocolo de enlace SSL que la mayoría de los clientes ignora ... excepto Java. Como mencionó @Bob Kerns , los ingenieros de Oracle se niegan a "arreglar" este error / función.

Como solución alternativa, sugieren configurar la jsse.enableSNIExtensionpropiedad. Para permitir que sus programas funcionen sin volver a compilar, ejecute su aplicación como:

java -Djsse.enableSNIExtension=false yourClass

La propiedad también se puede establecer en el código Java, pero debe establecerse antes de cualquier acción SSL . Una vez que la biblioteca SSL se ha cargado, puede cambiar la propiedad, pero no tendrá ningún efecto en el estado de SNI . Para deshabilitar SNI en tiempo de ejecución (con las limitaciones mencionadas anteriormente), use:

System.setProperty("jsse.enableSNIExtension", "false");

La desventaja de configurar este indicador es que SNI está deshabilitado en todas partes de la aplicación. Para hacer uso de SNI y aún admitir servidores mal configurados:

  1. Cree un SSLSocketcon el nombre de host al que desea conectarse. Vamos a nombrar esto sslsock.
  2. Intenta correr sslsock.startHandshake(). Esto bloqueará hasta que esté hecho o arrojará una excepción en caso de error. Siempre que ocurra un error startHandshake(), obtenga el mensaje de excepción. Si es igual a handshake alert: unrecognized_name, entonces ha encontrado un servidor mal configurado.
  3. Cuando haya recibido la unrecognized_nameadvertencia (fatal en Java), vuelva a intentar abrir una SSLSocket, pero esta vez sin un nombre de host. Esto deshabilita efectivamente SNI (después de todo, la extensión SNI se trata de agregar un nombre de host al mensaje de ClientHello).

Para el proxy SSL de Webscarab, esta confirmación implementa la configuración alternativa .

Lekensteyn
fuente
55
funciona, gracias! El cliente de subversión IntelliJ IDEA tuvo el mismo error al conectarse a través de HTTPS. Solo necesito actualizar el archivo idea.exe.vmoptions con la línea: -Djsse.enableSNIExtension = false
Dima
2
Para java webstart use esto: javaws -J-Djsse.enableSNIExtension = false yourClass
Torsten
Tuve exactamente el mismo problema con mi cliente de Jersey con el servidor https rest. Resulta que usé tu truco y funcionó !! ¡Gracias!
shahshi15
44
Para aquellos que se preguntan sobre Java 8, Oracle aún no ha cambiado el comportamiento y aún requiere que el programador capture servidores que no son compatibles (correctamente) con SNI: docs.oracle.com/javase/8/docs/technotes/guides/security/jsse / ...
Lekensteyn 01 de
2
@Blauhirn Póngase en contacto con su servicio de alojamiento web, deben corregir su configuración. Si usan Apache, busque algunos cambios que deben hacerse.
Lekensteyn
89

Tuve lo que creo que es el mismo problema. Descubrí que necesitaba ajustar la configuración de Apache para incluir un ServerName o ServerAlias ​​para el host.

Este código falló:

public class a {
   public static void main(String [] a) throws Exception {
      java.net.URLConnection c = new java.net.URL("https://mydomain.com/").openConnection();
      c.setDoOutput(true);
      c.getOutputStream();
   }
}

Y este código funcionó:

public class a {
   public static void main(String [] a) throws Exception {
      java.net.URLConnection c = new java.net.URL("https://google.com/").openConnection();
      c.setDoOutput(true);
      c.getOutputStream();
   }
}

Wireshark reveló que durante el TSL / SSL Hola, la alerta de advertencia (Nivel: Advertencia, Descripción: Nombre no reconocido), el servidor Hola fue enviado desde el servidor al cliente. Fue solo una advertencia, sin embargo, Java 7.1 respondió inmediatamente con un "Fatal, Descripción: Mensaje inesperado", lo que supongo que significa que las bibliotecas Java SSL no quieren ver la advertencia de un nombre no reconocido.

Del Wiki sobre Seguridad de la capa de transporte (TLS):

112 Advertencia de nombre no reconocido solo TLS; El indicador de nombre del servidor del cliente especificó un nombre de host no admitido por el servidor

Esto me llevó a mirar mis archivos de configuración de Apache y descubrí que si agregaba un ServerName o ServerAlias ​​para el nombre enviado desde el lado del cliente / Java, funcionaba correctamente sin ningún error.

<VirtualHost mydomain.com:443>
  ServerName mydomain.com
  ServerAlias www.mydomain.com
David McLaughlin
fuente
3
Esto parece un error en la implementación de TLS de Java.
Paŭlo Ebermann el
1
De hecho, abrí un error para esto. Me asignaron este número (aún no visible): bugs.sun.com/bugdatabase/view_bug.do?bug_id=7127374
eckes
1
Gracias, agregando un ServerAliastrabajado para mí. Había visto la misma alerta de TLS en Wireshark.
Jared Beck
2
Esto funcionó para mí también. (hubiera funcionado mejor si lo hubiera creído y no hubiera ido a buscar otro camino)
usuario final
10
@JosephShraibman, decir que el empleado de Oracle es un idiota es inapropiado. Cuando usa dos ServerNames, Apache responde con una alerta de unrecognized_name advertencia (que también podría ser una alerta fatal ). RFC 6066 dice precisamente esto sobre este tema: " NO SE RECOMIENDA enviar una alerta de nivel de advertencia no reconocida_nombre (112), porque el comportamiento del cliente en respuesta a las alertas de nivel de advertencia es impredecible ". El único error cometido por este empleado es asumir que se trataba de una alerta fatal . Esto es tanto un error de JRE como uno de Apache.
Bruno
36

Puede deshabilitar el envío de registros SNI con la propiedad del sistema jsse.enableSNIExtension = false.

Si puede cambiar el código, es útil usarlo SSLCocketFactory#createSocket()(sin parámetro de host o con un socket conectado). En este caso, no enviará una indicación de nombre_servidor.

revs eckes
fuente
11
Lo que se hace así:System.setProperty ("jsse.enableSNIExtension", "false");
jn1kk
He descubierto que eso no siempre funciona. No sé por qué funciona en algunos casos y no en otros.
Joseph Shraibman
2
Funciona para mí con -Djsse.enableSNIExtension=false. ¿Pero qué más hace? ¿Es perjudicial para el resto de la seguridad de Javas?
ceving
2
No, solo significa que algunos sitios (especialmente servidores web) que usan múltiples nombres de host detrás de una IP compartida no saben qué certificado enviar. Pero a menos que su aplicación Java tenga que conectarse a millones de sitios web, no necesitará esa función. Si encuentra un servidor de este tipo, la validación de su certificado puede fallar y la conexión se cancela. Por lo tanto, no se espera ningún problema de seguridad.
Eckes
17

También nos encontramos con este error en una nueva compilación del servidor Apache.

La solución en nuestro caso fue definir un ServerAliasen el httpd.confque correspondía al nombre de host al que Java intentaba conectarse. Nuestro ServerNamese configuró con el nombre de host interno. Nuestro certificado SSL estaba usando el nombre de host externo, pero eso no fue suficiente para evitar la advertencia.

Para ayudar a depurar, puede usar este comando ssl:

openssl s_client -servername <hostname> -connect <hostname>:443 -state

Si hay un problema con ese nombre de host, imprimirá este mensaje cerca de la parte superior de la salida:

SSL3 alert read: warning:unrecognized name

También debo tener en cuenta que no obtuvimos ese error al usar ese comando para conectarnos al nombre de host interno, aunque no coincidía con el certificado SSL.

Scott McIntyre
fuente
66
Gracias por incluir el ejemplo sobre cómo reproducir el problema usando openssl. Es muy útil
sidehowbarker
2
¿Hay alguna manera para que un cliente openssl vea la diferencia de nombre que origina el mensaje SSL3 alert read: warning:unrecognized name? Veo la alerta impresa, pero el resultado no me ayuda a ver realmente esta diferencia de nombres
mox601
Esto no funciona para mi. Probado con OpenSSL 1.0.1e-fips 11 Feb 2013, OpenSSL 1.0.2k-fips 26 Jan 2017y LibreSSL 2.6.5.
Greg Dubicki
15

En lugar de confiar en el mecanismo de host virtual predeterminado en apache, puede definir un último host virtual de catchall que use un ServerName arbitrario y un ServerAlias ​​comodín, por ejemplo

ServerName catchall.mydomain.com
ServerAlias *.mydomain.com

De esa forma, puede usar SNI y Apache no enviará la advertencia SSL.

Por supuesto, esto solo funciona si puede describir todos sus dominios fácilmente utilizando una sintaxis comodín.

Erik
fuente
Según tengo entendido, apache solo envía esta advertencia en caso de que el cliente use un nombre no configurado como ServerName o ServerAlias. Entonces, si tiene un certificado comodín, especifique todos los subdominios usados ​​o use la *.mydomain.comsintaxis para ServerAlias. Tenga en cuenta que puede agregar múltiples alias usando ServerAlias, por ejemplo ServerAlias *.mydomain.com mydomain.com *.myotherdomain.com.
Michael Paesold
13

Debería ser útil. Para volver a intentar un error SNI en Apache HttpClient 4.4, la forma más fácil que se nos ocurrió (ver HTTPCLIENT-1522 ):

public class SniHttpClientConnectionOperator extends DefaultHttpClientConnectionOperator {

    public SniHttpClientConnectionOperator(Lookup<ConnectionSocketFactory> socketFactoryRegistry) {
        super(socketFactoryRegistry, null, null);
    }

    @Override
    public void connect(
            final ManagedHttpClientConnection conn,
            final HttpHost host,
            final InetSocketAddress localAddress,
            final int connectTimeout,
            final SocketConfig socketConfig,
            final HttpContext context) throws IOException {
        try {
            super.connect(conn, host, localAddress, connectTimeout, socketConfig, context);
        } catch (SSLProtocolException e) {
            Boolean enableSniValue = (Boolean) context.getAttribute(SniSSLSocketFactory.ENABLE_SNI);
            boolean enableSni = enableSniValue == null || enableSniValue;
            if (enableSni && e.getMessage() != null && e.getMessage().equals("handshake alert:  unrecognized_name")) {
                TimesLoggers.httpworker.warn("Server received saw wrong SNI host, retrying without SNI");
                context.setAttribute(SniSSLSocketFactory.ENABLE_SNI, false);
                super.connect(conn, host, localAddress, connectTimeout, socketConfig, context);
            } else {
                throw e;
            }
        }
    }
}

y

public class SniSSLSocketFactory extends SSLConnectionSocketFactory {

    public static final String ENABLE_SNI = "__enable_sni__";

    /*
     * Implement any constructor you need for your particular application -
     * SSLConnectionSocketFactory has many variants
     */
    public SniSSLSocketFactory(final SSLContext sslContext, final HostnameVerifier verifier) {
        super(sslContext, verifier);
    }

    @Override
    public Socket createLayeredSocket(
            final Socket socket,
            final String target,
            final int port,
            final HttpContext context) throws IOException {
        Boolean enableSniValue = (Boolean) context.getAttribute(ENABLE_SNI);
        boolean enableSni = enableSniValue == null || enableSniValue;
        return super.createLayeredSocket(socket, enableSni ? target : "", port, context);
    }
}

y

cm = new PoolingHttpClientConnectionManager(new SniHttpClientConnectionOperator(socketFactoryRegistry), null, -1, TimeUnit.MILLISECONDS);
Shcheklein
fuente
Si lo haces de esta manera. ¿Cómo lidiar con verifyHostname(sslsock, target)en SSLConnectionSocketFactory.createLayeredSocket? Dado que targetse cambia a una cadena vacía, verifyHostnameno tendrá éxito. ¿Me estoy perdiendo algo obvio aquí?
Haozhun
2
Esto era exactamente lo que estaba buscando, ¡muchas gracias! No encontré ninguna otra forma de admitir SNI y servidores que responden con esta advertencia de "nombre no reconocido" al mismo tiempo.
korpe
Esta solución funciona, a menos que esté utilizando un servidor proxy.
mrog
Después de haber realizado una depuración rápida a través del código del cliente Apache, comenzando con verifiedHostname (), no puedo ver cómo puede funcionar esto usando DefaultHostnameVerifier. Sospecho que esta solución requiere que se deshabilite la verificación del nombre de host (por ejemplo, usando un NoopHostnameVerifier), lo cual NO es una buena idea ya que permite ataques de hombre en el medio.
FlyingSheep
11

Utilizar:

  • System.setProperty ("jsse.enableSNIExtension", "false");
  • Reinicia tu Tomcat (importante)
Tomasz Janisiewicz
fuente
6

Encontré este problema con spring boot y jvm 1.7 y 1.8. En AWS, no teníamos la opción de cambiar ServerName y ServerAlias ​​para que coincidan (son diferentes), así que hicimos lo siguiente:

En build.gradle agregamos lo siguiente:

System.setProperty("jsse.enableSNIExtension", "false")
bootRun.systemProperties = System.properties

Eso nos permitió evitar el problema con el "Nombre no reconocido".

Ray Hunter
fuente
5

Lamentablemente, no puede proporcionar propiedades del sistema a la herramienta jarsigner.exe.

He enviado el defecto 7177232 , haciendo referencia al defecto 7127374 de @eckes y explicando por qué se cerró por error.

Mi defecto es específicamente sobre el impacto en la herramienta jarsigner, pero tal vez los lleve a reabrir el otro defecto y abordar el problema correctamente.

ACTUALIZACIÓN: En realidad, resulta que PUEDES suministrar propiedades del sistema a la herramienta Jarsigner, simplemente no está en el mensaje de ayuda. Utilizarjarsigner -J-Djsse.enableSNIExtension=false

Bob Kerns
fuente
1
Gracias Bob por el seguimiento. Lamentablemente, Oracle todavía no entiende todo el asunto. La alerta de SNI no es fatal, puede ser tratada fatalmente. Pero la mayoría de los clientes SSL típicos (es decir, los navegadores) optaron por ignorarlo, ya que no es un problema de seguridad real (porque todavía necesita verificar el certificado del servidor y detectaría puntos finales incorrectos). Por supuesto, es triste que una CA no pueda establecer un servidor https configurado correctamente.
Eckes
2
En realidad, resulta que PUEDES suministrar propiedades del sistema a la herramienta Jarsigner, simplemente no está en el mensaje de ayuda. Está cubierto en la documentación. Tonto por creer el texto en línea. Por supuesto, usaron esto como una excusa para no arreglarlo.
Bob Kerns
Por cierto: actualmente estoy discutiendo este problema en security-dev @ openjdk y en el momento en que lo estaba evaluando parece que Symantec arregló el servidor de sellado de tiempo GEOTrust, ahora acepta correctamente la URL sin advertencia. Pero sigo pensando que eso debería arreglarse. Si desea echar un vistazo, un proyecto de cliente de prueba y la discusión :
eckes
3

Llegué al mismo problema y resultó que dns inverso no estaba configurado correctamente, apuntaba a un nombre de host incorrecto para la IP. Después de corregir dns inverso y reiniciar httpd, la advertencia desaparece. (si no corrijo el DNS inverso, agregar ServerName también me ayudó)

usuario2888387
fuente
2

Mi VirtualHost's ServerNamefue comentado por defecto. Funcionó después de descomentar.

Aram Paronikyan
fuente
Igual que aquí. Falta el nombre del servidor.
Dark Star1
2

Si está creando un cliente con Resttemplate, solo puede establecer el punto final de esta manera: https: // IP / path_to_service y establecer requestFactory.
Con esta solución no necesita reiniciar su TOMCAT o Apache:

public static HttpComponentsClientHttpRequestFactory requestFactory(CloseableHttpClient httpClient) {
    TrustStrategy acceptingTrustStrategy = new TrustStrategy() {
        @Override
        public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            return true;
        }
    };

    SSLContext sslContext = null;
    try {
        sslContext = org.apache.http.ssl.SSLContexts.custom()
                .loadTrustMaterial(null, acceptingTrustStrategy)
                .build();
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }   

    HostnameVerifier hostnameVerifier = new HostnameVerifier() {
        @Override
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    };

    final SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext,hostnameVerifier);

    final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
            .register("http", new PlainConnectionSocketFactory())
            .register("https", csf)
            .build();

    final PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry);
    cm.setMaxTotal(100);
    httpClient = HttpClients.custom()
            .setSSLSocketFactory(csf)
            .setConnectionManager(cm)
            .build();

    HttpComponentsClientHttpRequestFactory requestFactory =
            new HttpComponentsClientHttpRequestFactory();

    requestFactory.setHttpClient(httpClient);

    return requestFactory;
}
Alfredo
fuente
1

También me encontré con este problema al actualizar Java 1.6_29 a 1.7.

De manera alarmante, mi cliente ha descubierto una configuración en el panel de control de Java que resuelve esto.

En la pestaña Avanzado, puede marcar 'Usar el formato ClientHello compatible con SSL 2.0'.

Esto parece resolver el problema.

Estamos utilizando applets de Java en un navegador Internet Explorer.

Espero que esto ayude.

Todo y
fuente
0

Tuve el mismo problema con un servidor Ubuntu Linux que ejecutaba subversion cuando se accedía a través de Eclipse.

Se ha demostrado que el problema tenía que ver con una advertencia cuando Apache (re) comenzó:

[Mon Jun 30 22:27:10 2014] [warn] NameVirtualHost *:80 has no VirtualHosts

... waiting [Mon Jun 30 22:27:11 2014] [warn] NameVirtualHost *:80 has no VirtualHosts

Esto se debe a una nueva entrada en ports.conf, donde NameVirtualHostse ingresó otra directiva junto con la directiva ensites-enabled/000-default .

Después de eliminar la directiva ports.conf, el problema había desaparecido (después de reiniciar Apache, naturalmente)

Gerhard
fuente
0

Solo para agregar una solución aquí. Esto podría ayudar a los usuarios de LAMP

Options +FollowSymLinks -SymLinksIfOwnerMatch

La línea mencionada anteriormente en la configuración del host virtual fue la culpable.

Configuración de host virtual cuando error

<VirtualHost *:80>
    DocumentRoot /var/www/html/load/web
    ServerName dev.load.com
    <Directory "/var/www/html/load/web">
        Options +FollowSymLinks -SymLinksIfOwnerMatch
        AllowOverride All
        Require all granted
        Order Allow,Deny
        Allow from All
    </Directory>
     RewriteEngine on
     RewriteCond %{SERVER_PORT} !^443$
     RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [NC,R=301,L]
</VirtualHost>

Configuración de trabajo

<VirtualHost *:80>
    DocumentRoot /var/www/html/load/web

   ServerName dev.load.com
   <Directory "/var/www/html/load/web">

        AllowOverride All

        Options All

        Order Allow,Deny

        Allow from All

    </Directory>

    # To allow authorization header
    RewriteEngine On
    RewriteCond %{HTTP:Authorization} ^(.*)
    RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]

   # RewriteCond %{SERVER_PORT} !^443$
   # RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [NC,R=301,L]


</VirtualHost>
Grifo
fuente
0

Aquí hay una solución para Appache httpclient 4.5.11. Tuve un problema con el certificado que tiene el tema comodín *.hostname.com. Me devolvió la misma excepción, pero no debo usar la desactivación por propiedad System.setProperty("jsse.enableSNIExtension", "false");porque cometió un error en el cliente de ubicación de Google.

Encontré una solución simple (solo modificando el socket):

import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;

import javax.inject.Named;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.util.List;

@Factory
public class BeanFactory {

    @Bean
    @Named("without_verify")
    public HttpClient provideHttpClient() {
        SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(SSLContexts.createDefault(), NoopHostnameVerifier.INSTANCE) {
            @Override
            protected void prepareSocket(SSLSocket socket) throws IOException {
                SSLParameters parameters = socket.getSSLParameters();
                parameters.setServerNames(List.of());
                socket.setSSLParameters(parameters);
                super.prepareSocket(socket);
            }
        };

        return HttpClients.custom()
                .setSSLSocketFactory(connectionSocketFactory)
                .build();
    }


}
Andrew Sneck
fuente
-1

Hay una manera más fácil en la que puede usar su propio HostnameVerifier para confiar implícitamente en ciertas conexiones. El problema viene con Java 1.7, donde se han agregado extensiones SNI y su error se debe a una configuración incorrecta del servidor.

Puede usar "-Djsse.enableSNIExtension = false" para deshabilitar SNI en toda la JVM o leer mi blog donde le explico cómo implementar un verificador personalizado en la parte superior de una conexión URL.

MagicDude4Eva
fuente