Certificados de cliente Java sobre HTTPS / SSL

116

Estoy usando Java 6 y estoy tratando de crear una HttpsURLConnectioncontra un servidor remoto, usando un certificado de cliente.
El servidor utiliza un certificado raíz autofirmado y requiere que se presente un certificado de cliente protegido por contraseña. Agregué el certificado raíz del servidor y el certificado del cliente a un almacén de claves de Java predeterminado que encontré en /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home/lib/security/cacerts(OSX 10.5). ¿El nombre del archivo del almacén de claves parece sugerir que el certificado del cliente no debe entrar allí?

De todos modos, agregar el certificado raíz a esta tienda resolvió el infame javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed' problem.

Sin embargo, ahora estoy atascado en cómo usar el certificado de cliente. He intentado dos enfoques y ninguno me lleva a ninguna parte.
Primero, y preferido, intente:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://somehost.dk:3049");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
// The last line fails, and gives:
// javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

Intenté omitir la clase HttpsURLConnection (no es ideal ya que quiero hablar HTTP con el servidor) y hago esto en su lugar:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("somehost.dk", 3049);
InputStream inputstream = sslsocket.getInputStream();
// do anything with the inputstream results in:
// java.net.SocketTimeoutException: Read timed out

Ni siquiera estoy seguro de que el certificado de cliente sea el problema aquí.

ene
fuente
Yo he dado dos certificados de cliente cómo identificar lo que se necesita para poner en almacén de claves y almacén de confianza Podrían ayudarme a identificar este problema como ya ha pasado por el mismo tipo de problema stackoverflow.com/questions/61374276/...
henrycharles

Respuestas:

100

Finalmente lo resolvió;). Tengo una pista fuerte aquí (la respuesta de Gandalf también lo tocó un poco). Los enlaces faltantes fueron (en su mayoría) el primero de los parámetros a continuación, y hasta cierto punto pasé por alto la diferencia entre los almacenes de claves y los almacenes de confianza.

El certificado de servidor autofirmado debe importarse a un almacén de confianza:

keytool -import -alias gridserver -file gridserver.crt -storepass $ PASS -keystore gridserver.keystore

Estas propiedades deben establecerse (ya sea en la línea de comandos o en el código):

-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.trustStoreType=jks
-Djavax.net.ssl.keyStore=clientcertificate.p12
-Djavax.net.ssl.trustStore=gridserver.keystore
-Djavax.net.debug=ssl # very verbose debug
-Djavax.net.ssl.keyStorePassword=$PASS
-Djavax.net.ssl.trustStorePassword=$PASS

Código de ejemplo de trabajo:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://gridserver:3049/cgi-bin/ls.py");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

String string = null;
while ((string = bufferedreader.readLine()) != null) {
    System.out.println("Received " + string);
}
ene
fuente
He usado una URL como: localhost: 8443 / Application_Name / getAttributes . Tengo un método con la asignación de URL / getAttribute. Este método devuelve una lista de elementos. He usado HttpsUrlConnection, el código de respuesta de conexión es 200, pero no me da la lista de atributos cuando uso inputStream, me da el contenido html de mi página de inicio de sesión. He realizado la autenticación y configuré el tipo de contenido como JSON. Por favor sugiera
Deepak
83

Si bien no se recomienda, también puede deshabilitar la validación del certificado SSL por completo:

import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public class SSLTool {

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

    // Ignore differences between given hostname and certificate hostname
    HostnameVerifier hv = new HostnameVerifier() {
      public boolean verify(String hostname, SSLSession session) { return true; }
    };

    // Install the all-trusting trust manager
    try {
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts, new SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
      HttpsURLConnection.setDefaultHostnameVerifier(hv);
    } catch (Exception e) {}
  }
}
neu242
fuente
72
Cabe señalar que deshabilitar la validación de certificados de esta manera abre la conexión a posibles ataques MITM: no usar en producción .
Bruno
3
El código no se compila, afortunadamente. Esta "solución" es radicalmente insegura.
Marqués de Lorne
5
@ neu242, no, realmente no depende de para qué lo uses. Si desea utilizar SSL / TLS, desea proteger su conexión contra ataques MITM, ese es el punto. La autenticación del servidor no es necesaria si puede garantizar que nadie podrá alterar el tráfico, pero las situaciones en las que sospecha que puede haber intrusos que tampoco estarían en condiciones de alterar el tráfico de la red son bastante raras.
Bruno
1
@ neu242 Gracias por el fragmento de código. De hecho, estoy pensando en usar esto en producción para un propósito muy específico (rastreo web), y mencioné su implementación en una pregunta ( stackoverflow.com/questions/13076511/… ). Si tiene tiempo, ¿podría mirarlo y hacerme saber si hay algún riesgo de seguridad que haya pasado por alto?
Sal
1
@Bruno Si solo estoy haciendo ping al servidor, ¿eso realmente me afectaría por ataques MITM?
PhoonOne
21

¿Ha configurado las propiedades del sistema KeyStore y / o TrustStore?

java -Djavax.net.ssl.keyStore=pathToKeystore -Djavax.net.ssl.keyStorePassword=123456

o desde con el código

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

Lo mismo con javax.net.ssl.trustStore

Gandalf
fuente
13

Si está tratando con una llamada de servicio web utilizando el marco de Axis, hay una respuesta mucho más simple. Si todo lo que desea es que su cliente pueda llamar al servicio web SSL e ignorar los errores del certificado SSL, simplemente ponga esta declaración antes de invocar cualquier servicio web:

System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory");

Se aplican las exenciones de responsabilidad habituales acerca de que esto es algo muy malo en un entorno de producción.

Encontré esto en la wiki de Axis .

Mark Meuer
fuente
El OP está lidiando con una HttpsURLConnection, no con Axis
neu242
2
Entiendo. No pretendía dar a entender que mi respuesta fuera mejor en el caso general. Es solo que si está utilizando el marco de Axis, es posible que tenga la pregunta del OP en ese contexto. (Así es como encontré esta pregunta en primer lugar). En ese caso, la forma que he proporcionado es más simple.
Mark Meuer
5

Para mí, esto es lo que funcionó usando Apache HttpComponents ~ HttpClient 4.x:

    KeyStore keyStore  = KeyStore.getInstance("PKCS12");
    FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
    try {
        keyStore.load(instream, "helloworld".toCharArray());
    } finally {
        instream.close();
    }

    // Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, "helloworld".toCharArray())
        //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) //custom trust store
        .build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
    CloseableHttpClient httpclient = HttpClients.custom()
        .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
        .setSSLSocketFactory(sslsf)
        .build();
    try {

        HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");

        System.out.println("executing request" + httpget.getRequestLine());

        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) {
                System.out.println("Response content length: " + entity.getContentLength());
            }
            EntityUtils.consume(entity);
        } finally {
            response.close();
        }
    } finally {
        httpclient.close();
    }

El archivo P12 contiene el certificado de cliente y la clave privada del cliente, creada con BouncyCastle:

public static byte[] convertPEMToPKCS12(final String keyFile, final String cerFile,
    final String password)
    throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException,
    NoSuchProviderException
{
    // Get the private key
    FileReader reader = new FileReader(keyFile);

    PEMParser pem = new PEMParser(reader);
    PEMKeyPair pemKeyPair = ((PEMKeyPair)pem.readObject());
    JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter().setProvider("BC");
    KeyPair keyPair = jcaPEMKeyConverter.getKeyPair(pemKeyPair);

    PrivateKey key = keyPair.getPrivate();

    pem.close();
    reader.close();

    // Get the certificate
    reader = new FileReader(cerFile);
    pem = new PEMParser(reader);

    X509CertificateHolder certHolder = (X509CertificateHolder) pem.readObject();
    java.security.cert.Certificate x509Certificate =
        new JcaX509CertificateConverter().setProvider("BC")
            .getCertificate(certHolder);

    pem.close();
    reader.close();

    // Put them into a PKCS12 keystore and write it to a byte[]
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
    ks.load(null);
    ks.setKeyEntry("key-alias", (Key) key, password.toCharArray(),
        new java.security.cert.Certificate[]{x509Certificate});
    ks.store(bos, password.toCharArray());
    bos.close();
    return bos.toByteArray();
}
EpicPandaForce
fuente
El keyStorees lo que contiene la clave privada y el certificado.
EpicPandaForce
1
Debe incluir estas 2 dependencias para que el código convertPEMtoP12 funcione: <dependency> <groupId> org.bouncycastle </groupId> <artifactId> bcprov-jdk15on </artifactId> <version> 1.53 </version> </dependency> < dependencia> <groupId> org.bouncycastle </groupId> <artifactId> bcpkix-jdk15on </artifactId> <version> 1.53 </version> </dependency>
BirdOfPrey
@EpicPandaForce Me aparece un error: Atrapado: org.codehaus.groovy.runtime.typehandling.GroovyCastException: No se puede convertir el objeto con la clase 'org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject' a la clase 'int' en la línea ks. setKeyEntry - alguna pista de lo que podría estar mal
Vishal Biyani
Sí, estás usando Groovy en lugar de un lenguaje estrictamente escrito. (técnicamente, ese método toma una identificación y un certificado, no solo un certificado)
EpicPandaForce
4

Utilizo el paquete Apache commons HTTP Client para hacer esto en mi proyecto actual y funciona bien con SSL y un certificado autofirmado (después de instalarlo en cacerts como mencionaste). Échale un vistazo aquí:

http://hc.apache.org/httpclient-3.x/tutorial.html

http://hc.apache.org/httpclient-3.x/sslguide.html

Taylor Leese
fuente
1
Este parece un paquete bastante bueno, pero la clase que debería hacer que todo funcione 'AuthSSLProtocolSocketFactory' aparentemente no es parte de la distribución oficial, ni en 4.0beta (a pesar de que las notas de la versión indican que sí), o en 3.1. Lo he pirateado un poco y ahora parece estar permanentemente atascado con un bloqueo de 5 minutos antes de que se caiga la conexión. Es realmente extraño: si cargo la CA y el certificado del cliente en cualquier navegador, simplemente vuela.
Ene
1
Apache HTTP Client 4 puede tomar un SSLContextdirectamente, por lo que puede configurar todo esto de esta manera, en lugar de usar AuthSSLProtocolSocketFactory.
Bruno
1
¿Hay alguna manera de hacer todo el certificado del cliente en la memoria en lugar de hacerlo a través de un almacén de claves externo?
Sridhar Sarnobat
4

Creo que tiene un problema con el certificado de su servidor, no es un certificado válido (creo que esto es lo que significa "handshake_failure" en este caso):

Importe su certificado de servidor a su almacén de claves trustcacerts en el JRE del cliente. Esto se hace fácilmente con keytool :

keytool
    -import
    -alias <provide_an_alias>
    -file <certificate_file>
    -keystore <your_path_to_jre>/lib/security/cacerts
fuenterebeldes
fuente
Intenté limpiar y empezar de nuevo, y el fallo del apretón de manos desapareció. Ahora sólo les dará 5 minutos de silencio muerto antes de que se termina la conexión: o
Ene
1

Usando el siguiente código

-Djavax.net.ssl.keyStoreType=pkcs12

o

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

no es en absoluto necesario. Además, no es necesario crear su propia fábrica de SSL personalizada.

También encontré el mismo problema, en mi caso hubo un problema que completó la cadena de certificados no se importó a los almacenes de confianza. Importe certificados utilizando la utilidad keytool directamente desde el certificado raíz, también puede abrir el archivo cacerts en el bloc de notas y ver si la cadena de certificados completa se importa o no. Verifique el nombre de alias que proporcionó al importar certificados, abra los certificados y vea cuántos contiene, la misma cantidad de certificados debería estar en el archivo cacerts.

Además, el archivo cacerts debe configurarse en el servidor en el que está ejecutando su aplicación, los dos servidores se autenticarán entre sí con claves públicas / privadas.

Ankur Singhal
fuente
Crear su propia fábrica de SSL personalizada es mucho más complicado y propenso a errores que configurar dos propiedades del sistema.
Marqués de Lorne