Pregunta

Un módulo que estoy agregando a nuestra gran aplicación Java tiene que conversar con el sitio web protegido por SSL de otra empresa.El problema es que el sitio utiliza un certificado autofirmado.Tengo una copia del certificado para verificar que no estoy encontrando un ataque de intermediario 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 manejo adicional 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ñarle a Java a aceptar este certificado autofirmado, para este lugar de la aplicación y en ningún otro lugar.

Sé que puedo importar el certificado al almacén de autoridad de certificación de JRE y eso permitirá que Java lo acepte.Ese no es un enfoque que quiera adoptar si puedo ayudar;parece muy invasivo hacerlo en todas las máquinas de nuestros clientes para un módulo que quizás no utilicen;afectaría a todas las demás aplicaciones Java que utilizan el mismo JRE, y eso no me gusta, aunque las probabilidades de que cualquier 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 delega en el TrustManager real en todos los casos excepto en este certificado.Pero parece que TrustManager se instala globalmente y supongo que afectaría a todas las demás conexiones de nuestra aplicación, y eso tampoco me parece muy bien.

¿Cuál es la forma preferida, estándar o mejor de configurar una aplicación Java para que acepte un certificado autofirmado?¿Puedo lograr todos los objetivos que tengo en mente anteriormente o tendré que hacer concesiones?¿Existe una opción que involucre archivos, directorios y ajustes de configuración, y poco o ningún código?

¿Fue útil?

Solución

Crear un SSLSocket fábrica usted mismo y configúrelo en el HttpsURLConnection antes de conectar.

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

Querrás crear uno SSLSocketFactory y mantenlo cerca.Aquí hay un esquema 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.


A continuación se muestra 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 en formato PEM, puede escribir su propio código usando CertificateFactory, o simplemente importarlo con keytool desde el JDK (herramienta clave no funciona para una "entrada clave", pero está bien para una "entrada confiable").

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

Otros consejos

He leído por un montón de lugares en línea para resolver este asunto. Este es el código que he escrito para que funcione:

                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 es firmado uno mismo, siempre y cuando mantenga la estructura exacta anteriormente. Obtuve el certificado de cadena de línea de comandos con la Terminal de mi portátil.

Si la creación de un SSLSocketFactory no es una opción, simplemente 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. Importe 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: ¿Cómo resolver javax.net.ssl. SSLHandshakeException?

Copiamos almacén de confianza del JRE y añadimos nuestros certificados personalizados a ese almacén de confianza, luego decirle a la aplicación a usar el almacén de confianza a medida con una propiedad del sistema. De esta manera dejar el almacén de confianza predeterminado JRE solo.

La desventaja es que al actualizar el JRE no obtiene su nuevo almacén de confianza automáticamente se fusionó con su encargo uno.

Se podría tal vez manejar esta situación por tener una rutina de instalación o puesta en marcha que verifica el almacén de confianza / JDK y comprueba si hay un desajuste o automáticamente actualiza el almacén de confianza. No sé qué pasa si se actualiza el almacén de confianza mientras se ejecuta la aplicación.

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

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 era crear un TrustManager personalizado que simplemente se pasa todo (registro de un mensaje de depuración).

Esto se reduce a tener nuestra propia SSLSocketFactory que crea sockets SSL de nuestra SSLContext local, que está preparado para tener sólo nuestra TrustManager local asociada con él. No es necesario ir cerca de un almacén de claves / certstore en absoluto.

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 del administrador de confianza ficticio mencionado.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top