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

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 ningún tipo de gestión adicional en el lugar para el certificado autofirmado, este muere en 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ñar 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 a 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 aceptar un certificado autofirmado? ¿Puedo lograr todos los objectives 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?

    Cree una fábrica SSLSocket usted mismo y HttpsURLConnection en HttpsURLConnection antes de conectarse.

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

    SSLSocketFactory crear una SSLSocketFactory y no SSLSocketFactory . 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, coméntelo.


    Aquí hay un ejemplo de cómo 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 de formato PEM, puede escribir su propio código usando CertificateFactory , o simplemente importarlo con keytool desde el JDK (keytool no funcionará para una “entrada clave”, pero está bien para un “confiable” entrada”).

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

    Si la creación de un SSLSocketFactory no es una opción, simplemente importe la clave en la JVM

    1. Recupere la clave pública: $openssl s_client -connect dev-server:443 , luego cree un archivo dev-server.pem que se vea como

       -----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: cambiar

    3. Reiniciar JVM

    Fuente: ¿Cómo resolver javax.net.ssl.SSLHandshakeException?

    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.

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

    Tal vez podría manejar este escenario teniendo un instalador o una rutina de inicio que verifique el almacén de confianza / jdk y compruebe si hay discrepancias o actualizaciones automáticas del 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.

    Tuve que hacer algo como esto cuando uso commons-httpclient para acceder a un servidor https interno 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 los sockets SSL de 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 nuestra 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 que implementan SecureProtocolSocketFactory. LocalSSLTrustManager es la implementación ficticia de administrador de confianza antes mencionada.

    Revisé MUCHOS lugares en SO y en la web para resolver esto. 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 puede poner cualquier carácter en la cadena del certificado, si está auto firmado, siempre y cuando mantenga la estructura exacta arriba. Obtuve la cadena del certificado con la línea de comando del terminal de mi computadora portátil.