¿Cómo puedo usar diferentes certificados en conexiones específicas?


147

Un módulo que estoy agregando a nuestra gran aplicación Java tiene que conversar con el sitio web seguro de SSL de otra compañía. El problema es que el sitio usa un certificado autofirmado. Tengo una copia del certificado para verificar que no estoy encontrando un ataque de hombre en el medio, y necesito incorporar este certificado en nuestro código de tal manera que la conexión al servidor sea exitosa.

Aquí está el código básico:

void sendRequest(String dataPacket) { 
    String urlStr = "https://host.example.com/"; 
    URL url = new URL(urlStr); 
    HttpURLConnection conn = (HttpURLConnection)url.openConnection(); 
    conn.setMethod("POST"); 
    conn.setRequestProperty("Content-Length", data.length()); 
    conn.setDoOutput(true); 
    OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream()); 
    o.write(data); 
    o.flush(); 
} 

Sin ninguna manipulación adicional en su lugar para el certificado autofirmado, este muere a conn.getOutputStream() con la siguiente excepción:

Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 
.... 
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 
.... 
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 

Idealmente , mi código necesita enseñarle a Java a aceptar este certificado autofirmado, para este único lugar en la aplicación, y en ningún otro lugar.

Sé que puedo importar el certificado en la tienda de la autoridad certificadora de JRE, y eso permitirá que Java lo acepte. Ese no es un enfoque que quiero tomar si puedo ayudar; parece muy invasivo en todas las máquinas de nuestros clientes para un módulo que no pueden usar; afectaría a todas las demás aplicaciones Java que usan el mismo JRE, y no me gusta eso a pesar de que las probabilidades de que alguna otra aplicación Java acceda a este sitio son nulas. Tampoco es una operación trivial: en UNIX tengo que obtener derechos de acceso para modificar el JRE de esta manera.

También he visto que puedo crear una instancia de TrustManager que realiza algunas comprobaciones personalizadas. Parece que incluso podría crear un TrustManager que delegue en el TrustManager real en todas las instancias excepto este certificado. Pero parece que TrustManager se instala de forma global, y supongo que afectaría todas las demás conexiones de nuestra aplicación, y eso tampoco me suena del todo bien.

¿Cuál es la forma preferida, estándar o mejor para configurar una aplicación Java para que acepte un certificado autofirmado? ¿Puedo lograr todos los objetivos que tengo en mente anteriormente, o voy a tener que comprometerme? ¿Hay alguna opción que implique archivos y directorios y configuraciones, y un código de poca o ninguna?

  0

small, working fix: http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS.html 16 ago. 112011-08-16 14:06:49

+16

@Hasenpriester: por favor no sugiera esta página. Deshabilita toda la verificación de confianza. No solo aceptará el certificado autofirmado que desee, sino que también aceptará cualquier certificado que un atacante de MITM le presente. 23 dic. 112011-12-23 13:23:14

150

Cree usted mismo una fábrica de SSLSocket y configúrela en el HttpsURLConnection antes de conectarla.

... 
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); 
conn.setSSLSocketFactory(sslFactory); 
conn.setMethod("POST"); 
... 

Usted desea crear una SSLSocketFactory y mantenerlo alrededor.Aquí hay un boceto de cómo inicializarlo:

/* Load the keyStore that includes self-signed cert as a "trusted" entry. */ 
KeyStore keyStore = ... 
TrustManagerFactory tmf = 
    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 
tmf.init(keyStore); 
SSLContext ctx = SSLContext.getInstance("TLS"); 
ctx.init(null, tmf.getTrustManagers(), null); 
sslFactory = ctx.getSocketFactory(); 

Si necesita ayuda para crear el almacén de claves, comente.


He aquí un ejemplo de cargar el almacén de claves:

KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 
keyStore.load(trustStore, trustStorePassword); 
trustStore.close(); 

Para crear el almacén de claves con un certificado con formato PEM, puede escribir su propio código usando CertificateFactory, o simplemente importar con keytool de el JDK (keytool no funcionará en para una "entrada clave", pero está bien para una "entrada confiable").

keytool -import -file selfsigned.pem -alias server -keystore server.jks 
+3

¡Muchas gracias! Los otros muchachos me ayudaron a orientarme en la dirección correcta, pero al final tuyo es el enfoque que tomé. Tuve la ardua tarea de convertir el archivo de certificado PEM en un archivo JKS Java keystore, y encontré ayuda para eso aquí: http://stackoverflow.com/questions/722931/ssl-socket-php-code-needs-to- be-converted-to-java 13 may. 092009-05-13 21:37:13

+1

Me alegra que funcionó. Lamento que hayas tenido problemas con la tienda de claves; Debería haberlo incluido en mi respuesta. No es demasiado difícil con CertificateFactory. De hecho, creo que haré una actualización para cualquier persona que venga después. 13 may. 092009-05-13 21:40:51

  0

Wow. ¡Eso significa convertir PEM a JKS es sustancialmente más fácil que lo que hice! ¡Gracias! :) 14 may. 092009-05-14 19:19:00

+1

Todo lo que hace este código es replicar lo que puede lograr estableciendo tres propiedades del sistema descritas en la Guía de referencia de JSSE. 31 may. 122012-05-31 19:36:53

+1

@EJP: Esto fue hace un tiempo, así que no lo recuerdo con certeza, pero supongo que el razonamiento fue que en una "gran aplicación Java", es probable que haya otras conexiones HTTP establecidas. Establecer propiedades globales podría interferir con las conexiones que funcionan, o permitir que esta parte falsifique los servidores. Este es un ejemplo del uso del administrador de confianza integrado caso por caso. 31 may. 122012-05-31 22:26:58

+2

Como dijo el OP, "mi código necesita enseñar a Java a aceptar este certificado autofirmado, para este único lugar en la aplicación, y en ningún otro lugar". 31 may. 122012-05-31 22:29:09

  0

He intentado implementar su solución en vano, por favor ayúdenme http://stackoverflow.com/questions/20190364/how-to-bypass-certificateexception-by-java 25 nov. 132013-11-25 10:59:37

  0

Si desea que se acepte el uso "global" keytool para instalar el certificado como certificado de confianza en el archivo cacerts. Entonces será confiable para cualquier VM iniciada que use ese JRE. 06 dic. 132013-12-06 19:59:08

  0

@KyleM Ese no es un buen enfoque para la aceptación global. 06 dic. 132013-12-06 20:03:33

  0

Es por eso que puse "global" entre comillas y explicó cómo funciona en realidad. Nadie más mencionó hacerlo de esa manera, aunque es un método muy común y aceptado. 06 dic. 132013-12-06 20:07:48

  0

Muchas organizaciones grandes no aceptan ese método. Sería una violación de seguridad con serias consecuencias. 06 dic. 132013-12-06 21:09:37

  0

@erickson Hola, es cuatro años más tarde y el enfoque que nos enseñó ha sido muy bueno y se ha utilizado para varios servidores diferentes con los que nos comunicamos. Ahora tengo que descubrir un nuevo truco (puede hacer una nueva pregunta sobre StackOverflow): me gustaría confiar tanto en el almacén de claves predeterminado como en el almacén de claves personalizado con el certificado autofirmado del servidor. Tuvimos un caso en el que el servidor se actualizó a un certificado real y la conexión se rompió, pero hubiera estado bien si hubiésemos confiado en el almacén de claves predeterminado. 13 feb. 142014-02-13 19:25:02

  0

@erickson La API es un poco engañosa: implica que puede pasar una matriz de TrustManagers a SSLContext.init(), pero javadoc en ese método indica que solo se utiliza el primer TrustManager en la matriz. Me encantaría escuchar cualquier información que puedas compartir. 13 feb. 142014-02-13 19:26:18

  0

@skiphoppy Tendré que experimentar un poco con eso y contactarte. No lo he hecho antes, pero tengo una idea que podría funcionar. 13 feb. 142014-02-13 20:03:55

+1

@skiphoppy Hmm, la única forma que he encontrado es crear un 'KeyStore' temporal (en la memoria) y agregar certs de raíz de todos sus almacenes de claves" reales "a eso, o (casi equivalente) crear un' Set <TrustAnchor> ' de todas las entradas en las tiendas de claves de confianza y úsala para inicializar 'PKIXParameters', y con eso, a su vez,' TrustManagerFactory'. 13 feb. 142014-02-13 21:48:48

  0

Gracias @erickson Todavía estoy jugando con eso y estoy deambulando por un largo laberinto de documentación. Le voy a dar una oportunidad. 13 feb. 142014-02-13 22:03:09


12

Copiamos el almacén de confianza de JRE y agregamos nuestros certificados personalizados a ese almacén de confianza, luego le pedimos a la aplicación que use el almacén de confianza personalizado con una propiedad del sistema. De esta manera, dejamos solo el almacén de confianza de JRE predeterminado.

El inconveniente es que cuando actualiza el JRE no obtiene su nuevo almacén de confianza fusionado automáticamente con el personalizado.

Quizás podría manejar este escenario teniendo un instalador o una rutina de inicio que verifique el truststore/jdk y verifique que no coincida o que actualice automáticamente el almacén de confianza. No sé lo que sucederá si actualiza el truststore mientras la aplicación se está ejecutando.

Esta solución no es 100% elegante o infalible, pero es simple, funciona y no requiere código.


12

que he tenido que hacer algo como esto al utilizar Commons-httpclient acceder a un servidor https interna con un certificado autofirmado. Sí, nuestra solución fue crear un TrustManager personalizado que simplemente pasara todo (registrando un mensaje de depuración).

Esto se reduce a tener nuestra propia SSLSocketFactory que crea conectores SSL desde nuestro SSLContext local, que está configurado para tener solo nuestro TrustManager local asociado. No necesita acercarse a un almacén de claves/certstore.

Así que esto es en nuestro LocalSSLSocketFactory:

static { 
    try { 
     SSL_CONTEXT = SSLContext.getInstance("SSL"); 
     SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null); 
    } catch (NoSuchAlgorithmException e) { 
     throw new RuntimeException("Unable to initialise SSL context", e); 
    } catch (KeyManagementException e) { 
     throw new RuntimeException("Unable to initialise SSL context", e); 
    } 
} 

public Socket createSocket(String host, int port) throws IOException, UnknownHostException { 
    LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) }); 

    return SSL_CONTEXT.getSocketFactory().createSocket(host, port); 
} 

Junto con otros métodos de aplicación SecureProtocolSocketFactory. LocalSSLTrustManager es la implementación ficticia de administrador de confianza antes mencionada.

+7

Si deshabilita toda verificación de confianza, no tiene mucho sentido utilizar SSL/TLS en primer lugar. Está bien que lo pruebes localmente, pero no si quieres conectarte afuera. 23 dic. 112011-12-23 13:26:31

  0

Obtengo esta excepción cuando se ejecuta en Java 7. javax.net.ssl.SSLHandshakeException: no hay suites de cifrado en común ¿Puede ayudar? 19 dic. 132013-12-19 12:55:10


14

Si la creación de un SSLSocketFactory no es una opción, basta con importar la llave en la JVM

  1. Recuperar la clave pública: $openssl s_client -connect dev-server:443, a continuación, crear un archivo dev-server.pem que se parece a

    -----BEGIN CERTIFICATE----- 
    lklkkkllklklklklllkllklkl 
    lklkkkllklklklklllkllklkl 
    lklkkkllklk.... 
    -----END CERTIFICATE----- 
    
  2. Importar la clave: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem. Contraseña: changeit

  3. Reiniciar JVM

Fuente: How to solve javax.net.ssl.SSLHandshakeException?

+1

No creo que esto resuelva la pregunta original, pero resolvió * mi * problema, ¡así que gracias! 15 may. 132013-05-15 15:10:40


11

Fui a través de muchas otras provincias SO y la web para resolver este asunto. Este es el código que funcionó para mí:

   ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes()); 
       CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); 
       X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream); 
       String alias = "alias";//cert.getSubjectX500Principal().getName(); 

       KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); 
       trustStore.load(null); 
       trustStore.setCertificateEntry(alias, cert); 
       KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 
       kmf.init(trustStore, null); 
       KeyManager[] keyManagers = kmf.getKeyManagers(); 

       TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); 
       tmf.init(trustStore); 
       TrustManager[] trustManagers = tmf.getTrustManagers(); 

       SSLContext sslContext = SSLContext.getInstance("TLS"); 
       sslContext.init(keyManagers, trustManagers, null); 
       URL url = new URL(someURL); 
       conn = (HttpsURLConnection) url.openConnection(); 
       conn.setSSLSocketFactory(sslContext.getSocketFactory()); 

app.certificateString es una cadena que contiene el certificado, por ejemplo:

  static public String certificateString= 
      "-----BEGIN CERTIFICATE-----\n" + 
      "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" + 
      "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" + 
      ... a bunch of characters... 
      "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" + 
      "-----END CERTIFICATE-----"; 

He probado que se puede poner cualquier carácter en la cadena de certificados , si está autofirmado, siempre que mantenga la estructura exacta arriba. Obtuve la cadena del certificado con la línea de comando del terminal de mi computadora portátil.