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

164

Un módulo que estoy agregando a nuestra gran aplicación Java tiene que conversar con el sitio web con seguridad SSL de otra compañía. El problema es que el sitio usa un certificado autofirmado. Tengo una copia del certificado para verificar que no encuentro un ataque de intermediario y necesito incorporar este certificado a 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, esto 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 debe enseñarle a Java a aceptar este certificado autofirmado, para este punto en la aplicación y en ningún otro lugar.

Sé que puedo importar el certificado en el almacén de autoridad de certificación de JRE, y eso permitirá que Java lo acepte. Ese no es un enfoque que quiero adoptar si puedo ayudar; parece muy invasivo hacer en todas las máquinas de nuestros clientes un módulo que no pueden usar; afectaría a todas las demás aplicaciones Java que usan el mismo JRE, y eso no me gusta a pesar de que las posibilidades 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 delegue al TrustManager real en todas las instancias, 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 huele bien.

¿Cuál es la forma preferida, estándar o mejor para configurar una aplicación Java para aceptar un certificado autofirmado? ¿Puedo lograr todas las metas que tengo en mente arriba, o voy a tener que comprometerme? ¿Existe una opción que involucre archivos y directorios y configuraciones de configuración, y código de poco a nada?

skiphoppy
fuente
pequeño, corrección de trabajo: rgagnon.com/javadetails/…
20
@ Hasenpriester: por favor no sugiera esta página. Deshabilita toda verificación de confianza. No solo aceptará el certificado autofirmado que desee, sino que también aceptará cualquier certificado que le presente un atacante MITM.
Bruno

Respuestas:

168

Cree una SSLSocketfábrica usted mismo y configúrela HttpsURLConnectionantes de conectarse.

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

Querrás crear uno SSLSocketFactoryy mantenerlo a mano. Aquí hay un bosquejo 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.


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 utilizando CertificateFactory, o simplemente importarlo keytooldesde el JDK (keytool no funcionará para una "entrada de clave", pero está bien para una "entrada de confianza" )

keytool -import -file selfsigned.pem -alias server -keystore server.jks
erickson
fuente
3
¡Muchas gracias! Los otros muchachos me ayudaron a orientarme en la dirección correcta, pero al final el tuyo es el enfoque que tomé. Tuve una tarea agotadora de convertir el archivo de certificado PEM a un archivo de almacén de claves JKS Java, y encontré ayuda para eso aquí: stackoverflow.com/questions/722931/…
skiphoppy
1
Me alegra que haya funcionado. Lamento que hayas tenido problemas con la tienda de llaves; 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 más tarde.
erickson
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.
Marqués de Lorne
2
@EJP: Esto fue hace un tiempo, así que no lo recuerdo con certeza, pero supongo que la razón era que en una "gran aplicación Java", es probable que se establezcan otras conexiones HTTP. El establecimiento de propiedades globales podría interferir con las conexiones de trabajo o permitir que esta parte falsifique servidores. Este es un ejemplo del uso del administrador de confianza incorporado caso por caso.
erickson
2
Como dijo el OP, "mi código necesita enseñarle a Java a aceptar este certificado autofirmado, para este punto en la aplicación y en ningún otro lugar".
erickson
18

Leí MUCHOS lugares en línea para resolver esto. Este es el código que escribí 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 puede poner cualquier carácter en la cadena del certificado, si es autofirmado, siempre que mantenga la estructura exacta anterior. Obtuve la cadena de certificado con la línea de comando Terminal de mi computadora portátil.

Josh
fuente
1
Gracias por compartir @Josh. Creé
Master Drools
14

Si crear una SSLSocketFactoryno es una opción, solo importe la clave en la JVM

  1. Recupere la clave pública:, $openssl s_client -connect dev-server:443luego 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: changeit

  3. Reiniciar JVM

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

usuario454322
fuente
1
No creo que esto resuelva la pregunta original, pero resolvió mi problema, ¡así que gracias!
Ed Norris
Tampoco es una buena idea hacer esto: ahora tiene este certificado autofirmado de todo el sistema adicional aleatorio en el que todos los procesos Java confiarán de forma predeterminada.
user268396
12

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

La desventaja es que cuando actualiza el JRE no obtiene su nuevo almacén de confianza combinado 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 verifique si no hay una coincidencia o actualiza automáticamente el almacén de confianza. No sé qué sucede si actualiza el almacén de confianza mientras se ejecuta la aplicación.

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

Sr. Brillante y Nuevo 安 宇
fuente
12

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 nuestro propio SSLSocketFactory que crea sockets SSL a partir de nuestro SSLContext local, que está configurado para tener solo nuestro TrustManager local asociado. No necesita acercarse a un almacén de claves / certstore en absoluto.

Entonces esto está 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 de administrador de confianza ficticia mencionada anteriormente.

araqnid
fuente
8
Si deshabilita toda la verificación de confianza, no tiene mucho sentido usar SSL / TLS en primer lugar. Está bien probarlo localmente, pero no si quieres conectarte afuera.
Bruno
Recibo esta excepción cuando lo ejecuto en Java 7. javax.net.ssl.SSLHandshakeException: no hay conjuntos de cifrado en común ¿Pueden ayudarme?
Uri Lukach