Acepte el certificado ssl autofirmado del servidor en el cliente Java

215

Parece una pregunta estándar, pero no pude encontrar instrucciones claras en ningún lado.

Tengo un código Java que intenta conectarse a un servidor con un certificado probablemente autofirmado (o caducado). El código informa el siguiente error:

[HttpMethodDirector] I/O exception (javax.net.ssl.SSLHandshakeException) caught 
when processing request: sun.security.validator.ValidatorException: PKIX path 
building failed: sun.security.provider.certpath.SunCertPathBuilderException: 
unable to find valid certification path to requested target

Según tengo entendido, tengo que usar keytool y decirle a Java que está bien permitir esta conexión.

Todas las instrucciones para solucionar este problema suponen que soy completamente competente con keytool, como

generar clave privada para el servidor e importarla al almacén de claves

¿Hay alguien que pueda publicar instrucciones detalladas?

Estoy ejecutando Unix, por lo que bash script sería lo mejor.

No estoy seguro de si es importante, pero el código se ejecuta en jboss.

Nikita Rybak
fuente
2
Consulte ¿Cómo acepto un certificado autofirmado con una conexión HttpsURLConnection de Java? . Obviamente, sería mejor si puede hacer que el sitio use un certificado válido.
Matthew Flaschen
2
Gracias por el enlace, no lo vi mientras buscaba. Pero ambas soluciones implican un código especial para enviar una solicitud y estoy usando el código existente (amazon ws client for java). Respectivamente, es su sitio el que estoy conectando y no puedo solucionar sus problemas de certificado.
Nikita Rybak
2
@MatthewFlaschen - "Obviamente, sería mejor si puedes hacer que el sitio use un certificado válido ..." - Un certificado autofirmado es un certificado válido si el cliente confía en él. Muchos piensan que otorgar confianza al cartel de CA / Browser es un defecto de seguridad.
jww
3
Relacionado, vea El código más peligroso del mundo: validación de certificados SSL en software que no sea de navegador . (El enlace se proporciona ya que parece estar recibiendo esas respuestas no deseadas que desactivan la validación).
jww

Respuestas:

306

Aquí tiene básicamente dos opciones: agregar el certificado autofirmado a su almacén de confianza JVM o configurar su cliente para

Opción 1

Exporte el certificado desde su navegador e impórtelo en su almacén de confianza JVM (para establecer una cadena de confianza):

<JAVA_HOME>\bin\keytool -import -v -trustcacerts
-alias server-alias -file server.cer
-keystore cacerts.jks -keypass changeit
-storepass changeit 

opcion 2

Deshabilitar validación de certificado:

// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] { 
    new X509TrustManager() {     
        public java.security.cert.X509Certificate[] getAcceptedIssuers() { 
            return new X509Certificate[0];
        } 
        public void checkClientTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
            } 
        public void checkServerTrusted( 
            java.security.cert.X509Certificate[] certs, String authType) {
        }
    } 
}; 

// Install the all-trusting trust manager
try {
    SSLContext sc = SSLContext.getInstance("SSL"); 
    sc.init(null, trustAllCerts, new java.security.SecureRandom()); 
    HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (GeneralSecurityException e) {
} 
// Now you can access an https URL without having the certificate in the truststore
try { 
    URL url = new URL("https://hostname/index.html"); 
} catch (MalformedURLException e) {
} 

Tenga en cuenta que no recomiendo la Opción # 2 en absoluto . Deshabilitar el administrador de confianza derrota algunas partes de SSL y lo hace vulnerable a los ataques de hombre en el medio. Prefiera la Opción # 1 o, mejor aún, haga que el servidor use un certificado "real" firmado por una CA reconocida.

Pascal Thivent
fuente
77
No solo el ataque MIM. Te hace vulnerable a conectarte al sitio equivocado. Es completamente inseguro. Ver RFC 2246. Me opongo a publicar este TrustManager en todo momento. Ni siquiera es correcto wrt su propia especificación.
Marqués de Lorne
10
@EJP Realmente no estoy recomendando la segunda opción (he actualizado mi respuesta para que quede claro). Sin embargo, no publicarlo no resolverá nada (esto es información pública) y en mi humilde opinión no merece un voto negativo.
Pascal Thivent
135
@EJP Es al enseñarles a las personas que los educas, no al ocultar cosas. Por lo tanto, mantener las cosas en secreto o en la oscuridad no es una solución en absoluto. Este código es público, la API de Java es pública, es mejor hablar de ello que ignorarlo. Pero puedo vivir contigo sin estar de acuerdo.
Pascal Thivent
10
La otra opción, la que no menciona, es arreglar el certificado del servidor, ya sea arreglándolo usted mismo o llamando a las personas de soporte pertinentes. Los certificados de host único son realmente muy baratos; flotar con cosas autofirmadas es una tontería (es decir, para aquellos que no están familiarizados con ese idioma inglés, un conjunto de prioridades totalmente estúpido que cuesta mucho ahorrar casi nada).
Donal Fellows
2
@ Rich, 6 años de retraso, pero también puede obtener el certificado del servidor en firefox haciendo clic en el icono de candado -> más información -> Ver certificado -> pestaña Detalles -> Exportar ... Está bien oculto.
Siddhartha
9

Seguí este problema a un proveedor de certificados que no es parte de los hosts de confianza JVM predeterminados a partir de JDK 8u74. El proveedor es www.identrust.com , pero ese no era el dominio al que intentaba conectarme. Ese dominio había obtenido su certificado de este proveedor. Consulte ¿La cobertura de raíz cruzada confiará en la lista predeterminada en JDK / JRE? - Lea un par de entradas. Consulte también Qué navegadores y sistemas operativos admiten Let's Encrypt .

Entonces, para conectarme al dominio que me interesaba, que tenía un certificado emitido identrust.com, hice los siguientes pasos. Básicamente, tenía que obtener el DST Root CA X3certificado identrust.com ( ) para que JVM confiara en él. Pude hacerlo usando Apache HttpComponents 4.5 así:

1: Obtenga el certificado de confianza en las Instrucciones de descarga de la cadena de certificados . Haga clic en el enlace DST Root CA X3 .

2: Guarde la cadena en un archivo llamado "DST Root CA X3.pem". Asegúrese de agregar las líneas "----- BEGIN CERTIFICATE -----" y "----- END CERTIFICATE -----" en el archivo al principio y al final.

3: Cree un archivo de almacén de claves de Java, cacerts.jks con el siguiente comando:

keytool -import -v -trustcacerts -alias IdenTrust -keypass yourpassword -file dst_root_ca_x3.pem -keystore cacerts.jks -storepass yourpassword

4: Copie el almacén de claves cacerts.jks resultante en el directorio de recursos de su aplicación java / (maven).

5: Use el siguiente código para cargar este archivo y adjuntarlo al Apache 4.5 HttpClient. Esto resolverá el problema para todos los dominios que tienen certificados emitidos por indetrust.comutil oracle que incluye el certificado en el almacén de claves predeterminado de JRE.

SSLContext sslcontext = SSLContexts.custom()
        .loadTrustMaterial(new File(CalRestClient.class.getResource("/cacerts.jks").getFile()), "yourpasword".toCharArray(),
                new TrustSelfSignedStrategy())
        .build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.getDefaultHostnameVerifier());
CloseableHttpClient httpclient = HttpClients.custom()
        .setSSLSocketFactory(sslsf)
        .build();

Cuando el proyecto se construye, los cacerts.jks se copiarán en el classpath y se cargarán desde allí. En este momento, no probé con otros sitios ssl, pero si el código anterior "encadena" en este certificado, también funcionarán, pero de nuevo, no lo sé.

Referencia: contexto SSL personalizado y ¿Cómo acepto un certificado autofirmado con una conexión HttpsURLConnection de Java?

K.Nicholas
fuente
La pregunta es sobre un certificado autofirmado. Esta respuesta no lo es.
Marqués de Lorne
44
Lea la pregunta (y la respuesta) un poco más de cerca.
K.Nicholas
9

Apache HttpClient 4.5 admite la aceptación de certificados autofirmados:

SSLContext sslContext = SSLContexts.custom()
    .loadTrustMaterial(new TrustSelfSignedStrategy())
    .build();
SSLConnectionSocketFactory socketFactory =
    new SSLConnectionSocketFactory(sslContext);
Registry<ConnectionSocketFactory> reg =
    RegistryBuilder.<ConnectionSocketFactory>create()
    .register("https", socketFactory)
    .build();
HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(reg);        
CloseableHttpClient httpClient = HttpClients.custom()
    .setConnectionManager(cm)
    .build();
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse sslResponse = httpClient.execute(httpGet);

Esto construye una fábrica de sockets SSL que utilizará TrustSelfSignedStrategy, la registra con un administrador de conexión personalizado y luego realiza un HTTP GET utilizando ese administrador de conexión.

Estoy de acuerdo con aquellos que cantan "no hagas esto en producción", sin embargo, hay casos de uso para aceptar certificados autofirmados fuera de producción; los usamos en pruebas de integración automatizadas, por lo que estamos usando SSL (como en producción) incluso cuando no se ejecuta en el hardware de producción.

spiffy
fuente
6

En lugar de configurar la fábrica de sockets predeterminada (que es IMO es algo malo), esto solo afectará la conexión actual en lugar de cada conexión SSL que intente abrir:

URLConnection connection = url.openConnection();
    // JMD - this is a better way to do it that doesn't override the default SSL factory.
    if (connection instanceof HttpsURLConnection)
    {
        HttpsURLConnection conHttps = (HttpsURLConnection) connection;
        // Set up a Trust all manager
        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)
            {
            }
        } };

        // Get a new SSL context
        SSLContext sc = SSLContext.getInstance("TLSv1.2");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        // Set our connection to use this SSL context, with the "Trust all" manager in place.
        conHttps.setSSLSocketFactory(sc.getSocketFactory());
        // Also force it to trust all hosts
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        // and set the hostname verifier.
        conHttps.setHostnameVerifier(allHostsValid);
    }
InputStream stream = connection.getInputStream();
Jon Daniel
fuente
2
Su checkServerTrustedno implementa la lógica necesaria para confiar en realidad el certificado y aseguran certificados no son de confianza son rechazados. Esto podría ser una buena lectura: el código más peligroso del mundo: validar certificados SSL en software que no sea de navegador .
jww
1
Su getAcceptedIssuers()método no se ajusta a la especificación, y esta 'solución' sigue siendo radicalmente insegura.
Marqués de Lorne
3

Hay una mejor alternativa para confiar en todos los certificados: TrustStorecree uno que confíe específicamente en un certificado determinado y úselo para crear uno SSLContextdesde el cual SSLSocketFactoryconfigurar el HttpsURLConnection. Aquí está el código completo:

File crtFile = new File("server.crt");
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", certificate);

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

Alternativamente, puede cargar KeyStoredirectamente desde un archivo o recuperar el Certificado X.509 de cualquier fuente confiable.

Tenga en cuenta que con este código, los certificados cacertsno se utilizarán. Este particular HttpsURLConnectionsolo confiará en este certificado específico.

Johannes Brodwall
fuente
1

Confíe en todos los certificados SSL: - Puede omitir SSL si desea realizar una prueba en el servidor de prueba. Pero no use este código para la producción.

public static class NukeSSLCerts {
protected static final String TAG = "NukeSSLCerts";

public static void nuke() {
    try {
        TrustManager[] trustAllCerts = new TrustManager[] { 
            new X509TrustManager() {
                public X509Certificate[] getAcceptedIssuers() {
                    X509Certificate[] myTrustedAnchors = new X509Certificate[0];  
                    return myTrustedAnchors;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {}

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {}
            }
        };

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String arg0, SSLSession arg1) {
                return true;
            }
        });
    } catch (Exception e) { 
    }
}

}

Llame a esta función en la función onCreate () en Actividad o en su Clase de aplicación.

NukeSSLCerts.nuke();

Esto se puede usar para Volley en Android.

Ashish Saini
fuente
No estoy completamente seguro de por qué se lo rechaza, a menos que el código no funcione. Quizás es la suposición de que Android está involucrado de alguna manera cuando la pregunta original no etiquetaba a Android.
Lo-Tan
1

La respuesta aceptada está bien, pero me gustaría agregar algo a esto ya que estaba usando IntelliJ en Mac y no pude hacerlo funcionar usando el JAVA_HOME variable de ruta.

Resulta que Java Home era diferente al ejecutar la aplicación desde IntelliJ.

Para averiguar exactamente dónde está, simplemente puede hacer System.getProperty("java.home")lo que leen los certificados de confianza.

Christopher Schneider
fuente
0

Si 'ellos' están utilizando un certificado autofirmado, ellos deben seguir los pasos necesarios para que su servidor sea utilizable. Específicamente, eso significa proporcionarle su certificado fuera de línea de manera confiable. Así que haz que hagan eso. Luego lo importa a su almacén de confianza utilizando la herramienta de claves como se describe en la Guía de referencia de JSSE. Ni siquiera pienses en el inseguro TrustManager publicado aquí.

EDITAR Para el beneficio de los diecisiete (!) Votantes negativos, y numerosos comentaristas a continuación, que claramente no han leído lo que he escrito aquí, esto no es una jeremiad contra los certificados autofirmados. No hay nada de malo con los certificados autofirmados cuando se implementan correctamente. Pero, la forma correcta de implementarlos es que el certificado se entregue de forma segura a través de un proceso fuera de línea, en lugar de hacerlo a través del canal no autenticado que se utilizarán para autenticar. ¿Seguramente esto es obvio? Ciertamente es obvio para todas las organizaciones conscientes de la seguridad para las que he trabajado, desde bancos con miles de sucursales hasta mis propias empresas. La 'solución' basada en el código del lado del cliente de confiar en todosLos certificados, incluidos los certificados autofirmados firmados por absolutamente cualquier persona o cualquier organismo arbitrario que se establezca como CA, ipso facto no es seguro. Es solo jugar a la seguridad. No tiene sentido. Está teniendo una conversación privada, a prueba de manipulaciones, a prueba de respuestas, a prueba de inyecciones con ... alguien. Cualquiera. Un hombre en el medio. Un imitador Cualquiera. También puede usar texto sin formato.

Marqués de Lorne
fuente
2
El hecho de que algún servidor haya decidido usar https no significa que la persona con el cliente se preocupe por la seguridad para sus propios fines.
Gus
3
Me sorprende que esta respuesta haya sido rechazada tanto. Me encantaría entender un poco más por qué. Parece que EJP sugiere que no debería estar haciendo la opción 2 porque permite una falla de seguridad importante. ¿Podría alguien explicar por qué (aparte de la configuración previa a la implementación) por qué esta respuesta es de baja calidad?
cr1pto
2
Bueno, supongo que todo. ¿Qué significa "depende de ellos tomar los pasos necesarios para que su servidor sea utilizable" ? ¿Qué debe hacer un operador de servidor para que sea utilizable? ¿Cuáles son los pasos que tienes en mente? ¿Puedes proporcionar una lista de ellos? Pero retrocediendo a 1,000 pies, ¿cómo se relaciona eso con el problema que preguntó el OP? Quiere saber cómo hacer que el cliente acepte el certificado autofirmado en Java. Es una cuestión simple de conferir confianza en el código, ya que el OP considera que el certificado es aceptable.
jww
2
@EJP: corrígeme si me equivoco, pero aceptar un certificado autofirmado es una decisión de política del lado del cliente. No tiene nada que ver con las operaciones del lado del servidor. El cliente debe tomar la decisión. Las paridades deben trabajar juntas para resolver el problema de distribución de claves, pero eso no tiene nada que ver con hacer que el servidor sea utilizable.
jww
3
@EJP: ciertamente no dije "confía en todos los certificados" . Quizás me estoy perdiendo algo (o estás leyendo demasiado sobre las cosas) ... El OP tiene el certificado, y es aceptable para él. Quiere saber cómo confiar en eso. Probablemente estoy dividiendo pelos, pero el certificado no necesita ser entregado sin conexión. Se debe usar una verificación fuera de banda, pero eso no es lo mismo que "se debe entregar al cliente fuera de línea" . Incluso podría ser la tienda que produce el servidor y el cliente, por lo que tiene los conocimientos necesarios a priori .
jww
0

En lugar de usar keytool como lo sugiere el comentario principal, en RHEL puede usar update-ca-trust a partir de las versiones más recientes de RHEL 6. Deberá tener el certificado en formato pem. Luego

trust anchor <cert.pem>

Edite /etc/pki/ca-trust/source/cert.p11-kit y cambie "categoría de certificado: otra entrada" a "categoría de certificado: autoridad". (O use sed para hacer esto en un script). Luego, haga

update-ca-trust

Un par de advertencias:

  • No pude encontrar "confianza" en mi servidor RHEL 6 y mmm no ofreció instalarlo. Terminé usándolo en un servidor RHEL 7 y copiando el archivo .p11-kit.
  • Para que esto funcione para usted, es posible que deba hacer update-ca-trust enable . Esto reemplazará / etc / pki / java / cacerts con un enlace simbólico que apunta a / etc / pki / ca-trust / extraído / java / cacerts. (Por lo tanto, es posible que desee hacer una copia de seguridad del primero primero).
  • Si su cliente java usa cacerts almacenados en alguna otra ubicación, querrá reemplazarlo manualmente con un enlace simbólico a / etc / pki / ca-trust / extraído / java / cacerts, o reemplazarlo con ese archivo.
usuario66660
fuente
0

Tuve el problema de que estaba pasando una URL a una biblioteca que estaba llamando url.openConnection();Adapte la respuesta de jon-daniel,

public class TrustHostUrlStreamHandler extends URLStreamHandler {

    private static final Logger LOG = LoggerFactory.getLogger(TrustHostUrlStreamHandler.class);

    @Override
    protected URLConnection openConnection(final URL url) throws IOException {

        final URLConnection urlConnection = new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile()).openConnection();

        // adapated from
        // /programming/2893819/accept-servers-self-signed-ssl-certificate-in-java-client
        if (urlConnection instanceof HttpsURLConnection) {
            final HttpsURLConnection conHttps = (HttpsURLConnection) urlConnection;

            try {
                // Set up a Trust all manager
                final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

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

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

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

                // Get a new SSL context
                final SSLContext sc = SSLContext.getInstance("TLSv1.2");
                sc.init(null, trustAllCerts, new java.security.SecureRandom());
                // Set our connection to use this SSL context, with the "Trust all" manager in place.
                conHttps.setSSLSocketFactory(sc.getSocketFactory());
                // Also force it to trust all hosts
                final HostnameVerifier allHostsValid = new HostnameVerifier() {
                    @Override
                    public boolean verify(final String hostname, final SSLSession session) {
                        return true;
                    }
                };

                // and set the hostname verifier.
                conHttps.setHostnameVerifier(allHostsValid);

            } catch (final NoSuchAlgorithmException e) {
                LOG.warn("Failed to override URLConnection.", e);
            } catch (final KeyManagementException e) {
                LOG.warn("Failed to override URLConnection.", e);
            }

        } else {
            LOG.warn("Failed to override URLConnection. Incorrect type: {}", urlConnection.getClass().getName());
        }

        return urlConnection;
    }

}

Con esta clase, es posible crear una nueva URL con:

trustedUrl = new URL(new URL(originalUrl), "", new TrustHostUrlStreamHandler());
trustedUrl.openConnection();

Esto tiene la ventaja de que está localizado y no reemplaza el valor predeterminado URL.openConnection.

David Ryan
fuente