«

»

dic 08 2011

Conecta con páginas HTTPS desde tu aplicación

Pongámonos en situación. Tenemos una aplicación android que se conecta a una página web, servicio web, etc. para intercambiar o almacenar datos del usuario de la aplicación y evidentemente queremos que dicha conexión sea lo más segura posible. Existen muchas formas de asegurar la confidencialidad e integridad y algunas de ellas complementarias entre sí. En esta entrada vamos a utilizar el estándar SSL consiguiendo una conexión con certificado entre nuestra aplicación android y una página HTTPS.

Hay que tener cuidado cuando queramos configurar una conexión de este tipo. Con una búsqueda rápida podemos encontrar muchos recursos que te hablan sobre cómo aceptar los certificados de forma automática. Esto no es nada seguro ya que estamos expuestos al ataque Main-in-the-middle. Esto puede solucionarse de varias formas, la más fácil es que el servidor al que te conectas esté registrado en alguna autoridad de certificación como VeriSign. Pero qué ocurre si es nuestro propio servidor y no queremos tener que pagar a una autoridad de certificación. En esta entrada trataremos esta solución, estableciendo una conexión segura con el servidor de la “Asociación de Internautas”. Los pasos que vamos a seguir son:

  1. Descargaremos el certificado del servidor
  2. Crearemos un almacén de claves (keystore) con keytool y el proveedor BouncyCastle e importaremos el certificado
  3. Cargaremos el almacén de claves en nuestra aplicación android y la utilizaremos para realizar conexiones seguras (en este caso con Apache HttpClient que está incluida en el sistema android)

 

Paso 1 – Descargar certificado

Lo primero que tenemos que obtener es el certificado de nuestro servidor en formato X.509. Hay muchas formas de descargarte el certificado, por ejemplo a través del navegador, pero lo recomendable es hacerlo a través del panel de administración de tu dominio (si es el caso) porque así nos aseguramos que no nos están dando un certificado falso.

En cualquier caso, para nuestro ejemplo podremos hacerlo por comandos a través de openssl (si disponemos del programa):

echo | openssl s_client -connect www.internautas.org:443 2>&1 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > mycert.pem

Esto nos crea el fichero mycert.pem con el certificado en la carpeta donde ejecutemos el comando.

Una vez descargado el certificado tendremos un fichero de texto con un contenido parecido al siguiente:

-----BEGIN CERTIFICATE-----
MIIDITCCAoqgAwIBAgIQT52W2WawmStUwpV8tBV9TTANBgkqhkiG9w0BAQUFADBM
MQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkg
THRkLjEWMBQGA1UEAxMNVGhhd3RlIFNHQyBDQTAeFw0xMTEwMjYwMDAwMDBaFw0x
…...
IOkKcGQRCMha8X2e7GmlpdWC1ycenlbN0nbVeSv3JUMcafC4+Q==
-----END CERTIFICATE-----

 

Paso 2 – Crear el almacén de claves

Descargamos la librería BouncyCastle y la guardamos en alguna carpeta accesible. A continuación vamos a una ventana de comandos y nos aseguramos que la herramienta keytool está accesible (viene con el JRE de Java). El comando siguiente crea un almacén de claves llamado “mykst” usando el certificado “mycert.pem” con el proveedor bouncycastle y la contraseña “miclave”:

keytool -import -v -trustcacerts -alias 0 -file mycert.pem -keystore “carpeta_almacen/mykst“ -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath “carpeta_bouncycastle/bcprov-jdk16-145.jar” -storepass miclave

Te pedirá confirmación para aceptar el certificado y nos habrá creado el almacén de claves correspondiente en la carpeta “carpeta_almacen”.

Podemos verificar que se ha creado correctamente con el siguiente comando:

keytool -list -keystore "carpeta_almacen/mykst" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "carpeta_bouncycastle/bcprov-jdk16-145.jar" -storetype BKS -storepass miclave

Lo que nos debe mostrar una salida parecida a la siguiente:

Tipo de almacén de claves: BKS
Proveedor de almacén de claves: BC

Su almacén de claves contiene entrada 1

0, 07-dic-2011, trustedCertEntry,
Huella digital de certificado (MD5): 55:FD:E5:E3:8A:4C:D6:B8:69:EB:6A:49:05:5F:18:48

 

Paso 3 – Usar el certificado en la aplicación

El último paso es utilizar dicho almacén de claves en mi aplicación. Lo primero que tenemos que hacer es copiar el fichero mykst en la carpeta “res/raw” (crearla si no existe) del proyecto.

A continuación tenemos que crear nuestro propio HttpClient para que utilice dicho almacén de claves. El código es el siguiente:

package es.androcode.certificado;

import java.io.InputStream;
import java.security.KeyStore;

import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;

import android.content.Context;

public class MyHttpClient extends DefaultHttpClient {

    final Context context;

    public MyHttpClient(Context context) {
        this.context = context;
    }

    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        // Register for port 443 our SSLSocketFactory with our keystore
        // to the ConnectionManager
        registry.register(new Scheme("https", newSslSocketFactory(), 443));
        return new SingleClientConnManager(getParams(), registry);
    }

    private SSLSocketFactory newSslSocketFactory() {
        try {
            // Get an instance of the Bouncy Castle KeyStore format
            KeyStore trusted = KeyStore.getInstance("BKS");
            // Get the raw resource, which contains the keystore with
            // your trusted certificates (root and any intermediate certs)
            InputStream in = context.getResources().openRawResource(R.raw.mykst);
            try {
                // Initialize the keystore with the provided trusted certificates
                // Also provide the password of the keystore
                trusted.load(in, "miclave".toCharArray());
            } finally {
                in.close();
            }
            // Pass the keystore to the SSLSocketFactory. The factory is responsible
            // for the verification of the server certificate.
            SSLSocketFactory sf = new SSLSocketFactory(trusted);
            // Hostname verification from certificate
            // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506
            sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
            return sf;
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

En este código estamos especificando que para conexiones https utilicemos un SSLSocketFactory personalizado, que utliza nuestro almacén de claves.

Un punto importante es la instrucción:

sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

Que le indica que el dominio de la web que estamos solicitando debe coincidir exactamente con el que indica el certificado. Hay veces que el certificado se ha realizado para otro dominio o no se ha indicado bien éste. En dicho caso lo podemos sustituir por lo siguiente, que no verifica el nombre del dominio pero que sigue siendo válido para nuestros propósitos de seguridad:

sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

Ahora sólo tenemos que utilizar nuestro HttpClient para realizar conexiones seguras con el servidor elegido. Por ejemplo, para realizar una petición GET de la página principal haríamos lo siguiente:

DefaultHttpClient client = new MyHttpClient(getApplicationContext());
HttpGet get = new HttpGet("https://socios.internautas.org/");
// Execute the GET call and obtain the response
HttpResponse getResponse;
try {
    getResponse = client.execute(get);
    HttpEntity responseEntity = getResponse.getEntity();
    InputStream is = responseEntity.getContent();
} catch (ClientProtocolException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

Espero que os haya gustado, a continuación podéis descargar el proyecto con el código fuente.

Información recopilada de las siguientes webs:

 

Acerca del autor

FedeProEx

Ingeniero Informático en la Universidad de Sevilla, programador Java y amante del Heavy Metal. Soy desarrollador android fuera del horario de trabajo con algunas aplicaciones en el market como Tiempo AEMET o aconTags

  • http://www.congdegnu.es DiCrEn

    Muy útil, aunque un poco tarde jeje Me explico, hace cosa de dos semanas casi me vuelvo loco intentandome conectar en mi aplicación por https, al final lo conseguí saltadome el tema certificados y que se tragase todo (por suerte no es nada muy importante) No tengo aquí a mano el código y realmente no recuerdo muy bien como lo hice jeje

    Un saludo y enhorabuena por el blog, lleva ya una temporada entre mis favoritos ;)

    • jdmasmx

      Hola, como resolviste tu detalle con el https? me estoy volviendo loco, llevo casi una semana y no consigo conectarme , o hacer un simple login ala pagina que tiene https

  • felsotca

    Estoy utilizando este mismo ejemplo, pero vieras que tengo problemas al enviar un post o get ya que me ha tirado varios errores.

    Actualmente estoy haciendo lo siguiente a ver si ves algo que está fuera de la normal.

    BufferedReader in = null;
    try {
    MyHttpClient client = new MyHttpClient(context);
    HttpPost post = new HttpPost(new URI(mUrl.toString()));

    String body = buildArgs(args);
    post.setEntity(new StringEntity(body));

    HttpResponse response = client.execute(post);
    in = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
    StringBuffer sb = new StringBuffer(“”);
    String line = “”;
    String NL = System.getProperty(“line.separator”);
    while ((line = in.readLine()) != null) {
    sb.append(line + NL);
    }
    in.close();
    String page = sb.toString();
    System.out.println(page);
    } finally {
    if (in != null) {
    try {
    in.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    Aqui armo el XML sería algo como lo siguiente:

    3
    #
    3
    123
    SALP66V4
    JAVA
    GCk6NwMxigkrvdYfC1tY+9GJXWfb6uoeTPESmYtrMxA=

    Alguno tiene una idea de que estoy haciendo mal?

    Lo que me retorna al hacer ese post es lo siguiente:

    soap:VersionMismatch
    Possible SOAP version mismatch: Envelope namespace was unexpected. Expecting http://schemas.xmlsoap.org/soap/envelope/.

    Saludos y muchas gracias por adelantado.

  • alcibiades

    Me fue de ayuda tu sitio, espero sigas así.
    Quiero dar mi pequeño aporte para los que tienen problemas para conseguir el certificado del sitio web, pueden utilizar esta aplicación, que utiliza un certificado .cer que lo obtienes con el navegador y te lo pasa a .bks .
    http://portecle.sourceforge.net/

  • Xavi (Drakgoku1)

    Tengo un problema me dice error “No peer certificate” a que es debido ?